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