001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.filters;
021
022import java.util.List;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.regex.Pattern;
026import java.util.stream.Collectors;
027
028import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
029import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
032import com.puppycrawl.tools.checkstyle.xpath.RootNode;
033import net.sf.saxon.Configuration;
034import net.sf.saxon.om.Item;
035import net.sf.saxon.sxpath.XPathDynamicContext;
036import net.sf.saxon.sxpath.XPathEvaluator;
037import net.sf.saxon.sxpath.XPathExpression;
038import net.sf.saxon.trans.XPathException;
039
040/**
041 * This filter element is immutable and processes {@link TreeWalkerAuditEvent}
042 * objects based on the criteria of file, check, module id, xpathQuery.
043 *
044 */
045public class XpathFilterElement implements TreeWalkerFilter {
046
047    /** The regexp to match file names against. */
048    private final Pattern fileRegexp;
049
050    /** The pattern for file names. */
051    private final String filePattern;
052
053    /** The regexp to match check names against. */
054    private final Pattern checkRegexp;
055
056    /** The pattern for check class names. */
057    private final String checkPattern;
058
059    /** The regexp to match message names against. */
060    private final Pattern messageRegexp;
061
062    /** The pattern for message names. */
063    private final String messagePattern;
064
065    /** Module id filter. */
066    private final String moduleId;
067
068    /** Xpath expression. */
069    private final XPathExpression xpathExpression;
070
071    /** Xpath query. */
072    private final String xpathQuery;
073
074    /** Indicates if all properties are set to null. */
075    private final boolean isEmptyConfig;
076
077    /**
078     * Creates a {@code XpathElement} instance.
079     *
080     * @param files regular expression for names of filtered files
081     * @param checks regular expression for filtered check classes
082     * @param message regular expression for messages.
083     * @param moduleId the module id
084     * @param query the xpath query
085     * @throws IllegalArgumentException if the xpath query is not expected.
086     */
087    public XpathFilterElement(String files, String checks,
088                       String message, String moduleId, String query) {
089        this(Optional.ofNullable(files).map(Pattern::compile).orElse(null),
090             Optional.ofNullable(checks).map(CommonUtil::createPattern).orElse(null),
091             Optional.ofNullable(message).map(Pattern::compile).orElse(null),
092             moduleId,
093             query);
094    }
095
096    /**
097     * Creates a {@code XpathElement} instance.
098     *
099     * @param files regular expression for names of filtered files
100     * @param checks regular expression for filtered check classes
101     * @param message regular expression for messages.
102     * @param moduleId the module id
103     * @param query the xpath query
104     * @throws IllegalArgumentException if the xpath query is not correct.
105     */
106    public XpathFilterElement(Pattern files, Pattern checks, Pattern message,
107                           String moduleId, String query) {
108        if (files == null) {
109            filePattern = null;
110            fileRegexp = null;
111        }
112        else {
113            filePattern = files.pattern();
114            fileRegexp = files;
115        }
116        if (checks == null) {
117            checkPattern = null;
118            checkRegexp = null;
119        }
120        else {
121            checkPattern = checks.pattern();
122            checkRegexp = checks;
123        }
124        if (message == null) {
125            messagePattern = null;
126            messageRegexp = null;
127        }
128        else {
129            messagePattern = message.pattern();
130            messageRegexp = message;
131        }
132        this.moduleId = moduleId;
133        xpathQuery = query;
134        if (xpathQuery == null) {
135            xpathExpression = null;
136        }
137        else {
138            final XPathEvaluator xpathEvaluator = new XPathEvaluator(
139                    Configuration.newConfiguration());
140            try {
141                xpathExpression = xpathEvaluator.createExpression(xpathQuery);
142            }
143            catch (XPathException ex) {
144                throw new IllegalArgumentException("Incorrect xpath query: " + xpathQuery, ex);
145            }
146        }
147        isEmptyConfig = fileRegexp == null
148                             && checkRegexp == null
149                             && messageRegexp == null
150                             && moduleId == null
151                             && xpathExpression == null;
152    }
153
154    @Override
155    public boolean accept(TreeWalkerAuditEvent event) {
156        return isEmptyConfig
157                || !isFileNameAndModuleAndModuleNameMatching(event)
158                || !isMessageNameMatching(event)
159                || !isXpathQueryMatching(event);
160    }
161
162    /**
163     * Is matching by file name, module id and Check name.
164     *
165     * @param event event
166     * @return true if it is matching
167     */
168    private boolean isFileNameAndModuleAndModuleNameMatching(TreeWalkerAuditEvent event) {
169        return event.getFileName() != null
170                && (fileRegexp == null || fileRegexp.matcher(event.getFileName()).find())
171                && event.getViolation() != null
172                && (moduleId == null || moduleId.equals(event.getModuleId()))
173                && (checkRegexp == null || checkRegexp.matcher(event.getSourceName()).find());
174    }
175
176    /**
177     * Is matching by message.
178     *
179     * @param event event
180     * @return true if it is matching or not set.
181     */
182    private boolean isMessageNameMatching(TreeWalkerAuditEvent event) {
183        return messageRegexp == null || messageRegexp.matcher(event.getMessage()).find();
184    }
185
186    /**
187     * Is matching by xpath query.
188     *
189     * @param event event
190     * @return true if it is matching or not set.
191     */
192    private boolean isXpathQueryMatching(TreeWalkerAuditEvent event) {
193        boolean isMatching;
194        if (xpathExpression == null) {
195            isMatching = true;
196        }
197        else {
198            isMatching = false;
199            final List<AbstractNode> nodes = getItems(event)
200                    .stream().map(AbstractNode.class::cast).collect(Collectors.toList());
201            for (AbstractNode abstractNode : nodes) {
202                isMatching = abstractNode.getTokenType() == event.getTokenType()
203                        && abstractNode.getLineNumber() == event.getLine()
204                        && abstractNode.getColumnNumber() == event.getColumnCharIndex();
205                if (isMatching) {
206                    break;
207                }
208            }
209        }
210        return isMatching;
211    }
212
213    /**
214     * Returns list of nodes matching xpath expression given event.
215     *
216     * @param event {@code TreeWalkerAuditEvent} object
217     * @return list of nodes matching xpath expression given event
218     * @throws IllegalStateException if the xpath query could not be evaluated.
219     */
220    private List<Item> getItems(TreeWalkerAuditEvent event) {
221        final RootNode rootNode;
222        if (event.getRootAst() == null) {
223            rootNode = null;
224        }
225        else {
226            rootNode = new RootNode(event.getRootAst());
227        }
228        final List<Item> items;
229        try {
230            final XPathDynamicContext xpathDynamicContext =
231                    xpathExpression.createDynamicContext(rootNode);
232            items = xpathExpression.evaluate(xpathDynamicContext);
233        }
234        catch (XPathException ex) {
235            throw new IllegalStateException("Cannot initialize context and evaluate query: "
236                    + xpathQuery, ex);
237        }
238        return items;
239    }
240
241    @Override
242    public int hashCode() {
243        return Objects.hash(filePattern, checkPattern, messagePattern,
244            moduleId, xpathQuery);
245    }
246
247    @Override
248    public boolean equals(Object other) {
249        if (this == other) {
250            return true;
251        }
252        if (other == null || getClass() != other.getClass()) {
253            return false;
254        }
255        final XpathFilterElement xpathFilter = (XpathFilterElement) other;
256        return Objects.equals(filePattern, xpathFilter.filePattern)
257                && Objects.equals(checkPattern, xpathFilter.checkPattern)
258                && Objects.equals(messagePattern, xpathFilter.messagePattern)
259                && Objects.equals(moduleId, xpathFilter.moduleId)
260                && Objects.equals(xpathQuery, xpathFilter.xpathQuery);
261    }
262
263}