001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 regexp to match check names against. */
051    private final Pattern checkRegexp;
052
053    /** The regexp to match message names against. */
054    private final Pattern messageRegexp;
055
056    /** Module id filter. */
057    private final String moduleId;
058
059    /** Xpath expression. */
060    private final XPathExpression xpathExpression;
061
062    /** Xpath query. */
063    private final String xpathQuery;
064
065    /** Indicates if all properties are set to null. */
066    private final boolean isEmptyConfig;
067
068    /**
069     * Creates a {@code XpathElement} instance.
070     *
071     * @param files regular expression for names of filtered files
072     * @param checks regular expression for filtered check classes
073     * @param message regular expression for messages.
074     * @param moduleId the module id
075     * @param query the xpath query
076     * @throws IllegalArgumentException if the xpath query is not expected.
077     */
078    public XpathFilterElement(String files, String checks,
079                       String message, String moduleId, String query) {
080        this(Optional.ofNullable(files).map(Pattern::compile).orElse(null),
081             Optional.ofNullable(checks).map(CommonUtil::createPattern).orElse(null),
082             Optional.ofNullable(message).map(Pattern::compile).orElse(null),
083             moduleId,
084             query);
085    }
086
087    /**
088     * Creates a {@code XpathElement} instance.
089     *
090     * @param files regular expression for names of filtered files
091     * @param checks regular expression for filtered check classes
092     * @param message regular expression for messages.
093     * @param moduleId the module id
094     * @param query the xpath query
095     * @throws IllegalArgumentException if the xpath query is not correct.
096     */
097    public XpathFilterElement(Pattern files, Pattern checks, Pattern message,
098                           String moduleId, String query) {
099        fileRegexp = files;
100        checkRegexp = checks;
101        messageRegexp = message;
102        this.moduleId = moduleId;
103        xpathQuery = query;
104        if (xpathQuery == null) {
105            xpathExpression = null;
106        }
107        else {
108            final XPathEvaluator xpathEvaluator = new XPathEvaluator(
109                    Configuration.newConfiguration());
110            try {
111                xpathExpression = xpathEvaluator.createExpression(xpathQuery);
112            }
113            catch (XPathException ex) {
114                throw new IllegalArgumentException("Incorrect xpath query: " + xpathQuery, ex);
115            }
116        }
117        isEmptyConfig = fileRegexp == null
118                             && checkRegexp == null
119                             && messageRegexp == null
120                             && moduleId == null
121                             && xpathExpression == null;
122    }
123
124    @Override
125    public boolean accept(TreeWalkerAuditEvent event) {
126        return isEmptyConfig
127                || !isFileNameAndModuleAndModuleNameMatching(event)
128                || !isMessageNameMatching(event)
129                || !isXpathQueryMatching(event);
130    }
131
132    /**
133     * Is matching by file name, module id and Check name.
134     *
135     * @param event event
136     * @return true if it is matching
137     */
138    private boolean isFileNameAndModuleAndModuleNameMatching(TreeWalkerAuditEvent event) {
139        return event.getFileName() != null
140                && (fileRegexp == null || fileRegexp.matcher(event.getFileName()).find())
141                && event.getViolation() != null
142                && (moduleId == null || moduleId.equals(event.getModuleId()))
143                && (checkRegexp == null || checkRegexp.matcher(event.getSourceName()).find());
144    }
145
146    /**
147     * Is matching by message.
148     *
149     * @param event event
150     * @return true if it is matching or not set.
151     */
152    private boolean isMessageNameMatching(TreeWalkerAuditEvent event) {
153        return messageRegexp == null || messageRegexp.matcher(event.getMessage()).find();
154    }
155
156    /**
157     * Is matching by xpath query.
158     *
159     * @param event event
160     * @return true if it is matching or not set.
161     */
162    private boolean isXpathQueryMatching(TreeWalkerAuditEvent event) {
163        boolean isMatching;
164        if (xpathExpression == null) {
165            isMatching = true;
166        }
167        else {
168            isMatching = false;
169            final List<AbstractNode> nodes = getItems(event)
170                .stream().map(AbstractNode.class::cast)
171                .collect(Collectors.toUnmodifiableList());
172            for (AbstractNode abstractNode : nodes) {
173                isMatching = abstractNode.getTokenType() == event.getTokenType()
174                        && abstractNode.getLineNumber() == event.getLine()
175                        && abstractNode.getColumnNumber() == event.getColumnCharIndex();
176                if (isMatching) {
177                    break;
178                }
179            }
180        }
181        return isMatching;
182    }
183
184    /**
185     * Returns list of nodes matching xpath expression given event.
186     *
187     * @param event {@code TreeWalkerAuditEvent} object
188     * @return list of nodes matching xpath expression given event
189     * @throws IllegalStateException if the xpath query could not be evaluated.
190     */
191    private List<Item> getItems(TreeWalkerAuditEvent event) {
192        final RootNode rootNode;
193        if (event.getRootAst() == null) {
194            rootNode = null;
195        }
196        else {
197            rootNode = new RootNode(event.getRootAst());
198        }
199        final List<Item> items;
200        try {
201            final XPathDynamicContext xpathDynamicContext =
202                    xpathExpression.createDynamicContext(rootNode);
203            items = xpathExpression.evaluate(xpathDynamicContext);
204        }
205        catch (XPathException ex) {
206            throw new IllegalStateException("Cannot initialize context and evaluate query: "
207                    + xpathQuery, ex);
208        }
209        return items;
210    }
211
212    @Override
213    public int hashCode() {
214        return Objects.hash(getPatternSafely(fileRegexp), getPatternSafely(checkRegexp),
215                getPatternSafely(messageRegexp), moduleId, xpathQuery);
216    }
217
218    @Override
219    public boolean equals(Object other) {
220        if (this == other) {
221            return true;
222        }
223        if (other == null || getClass() != other.getClass()) {
224            return false;
225        }
226        final XpathFilterElement xpathFilter = (XpathFilterElement) other;
227        return Objects.equals(getPatternSafely(fileRegexp),
228                    getPatternSafely(xpathFilter.fileRegexp))
229                && Objects.equals(getPatternSafely(checkRegexp),
230                    getPatternSafely(xpathFilter.checkRegexp))
231                && Objects.equals(getPatternSafely(messageRegexp),
232                    getPatternSafely(xpathFilter.messageRegexp))
233                && Objects.equals(moduleId, xpathFilter.moduleId)
234                && Objects.equals(xpathQuery, xpathFilter.xpathQuery);
235    }
236
237    /**
238     * Util method to get pattern String value from Pattern object safely, return null if
239     * pattern object is null.
240     *
241     * @param pattern pattern object
242     * @return value of pattern or null
243     */
244    private static String getPatternSafely(Pattern pattern) {
245        String result = null;
246        if (pattern != null) {
247            result = pattern.pattern();
248        }
249        return result;
250    }
251}