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.checks.coding;
021
022import java.util.List;
023import java.util.stream.Collectors;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
030import com.puppycrawl.tools.checkstyle.xpath.RootNode;
031import net.sf.saxon.Configuration;
032import net.sf.saxon.om.Item;
033import net.sf.saxon.sxpath.XPathDynamicContext;
034import net.sf.saxon.sxpath.XPathEvaluator;
035import net.sf.saxon.sxpath.XPathExpression;
036import net.sf.saxon.trans.XPathException;
037
038/**
039 * <div>
040 * Evaluates Xpath query and report violation on all matching AST nodes. This check allows
041 * user to implement custom checks using Xpath. If Xpath query is not specified explicitly,
042 * then the check does nothing.
043 * </div>
044 *
045 * <p>
046 * It is recommended to define custom message for violation to explain what is not allowed and what
047 * to use instead, default message might be too abstract. To customize a message you need to
048 * add {@code message} element with <b>matchxpath.match</b> as {@code key} attribute and
049 * desired message as {@code value} attribute.
050 * </p>
051 *
052 * <p>
053 * Please read more about Xpath syntax at
054 * <a href="https://www.saxonica.com/html/documentation10/expressions/index.html">Xpath Syntax</a>.
055 * Information regarding Xpath functions can be found at
056 * <a href="https://www.saxonica.com/html/documentation10/functions/fn/index.html">
057 *     XSLT/XPath Reference</a>.
058 * Note, that <b>@text</b> attribute can be used only with token types that are listed in
059 * <a href="https://github.com/checkstyle/checkstyle/search?q=%22TOKEN_TYPES_WITH_TEXT_ATTRIBUTE+%3D+Arrays.asList%22">
060 *     XpathUtil</a>.
061 * </p>
062 * <ul>
063 * <li>
064 * Property {@code query} - Specify Xpath query.
065 * Type is {@code java.lang.String}.
066 * Default value is {@code ""}.
067 * </li>
068 * </ul>
069 *
070 * <p>
071 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
072 * </p>
073 *
074 * <p>
075 * Violation Message Keys:
076 * </p>
077 * <ul>
078 * <li>
079 * {@code matchxpath.match}
080 * </li>
081 * </ul>
082 *
083 * @since 8.39
084 */
085@StatelessCheck
086public class MatchXpathCheck extends AbstractCheck {
087
088    /**
089     * A key is pointing to the warning message text provided by user.
090     */
091    public static final String MSG_KEY = "matchxpath.match";
092
093    /** Specify Xpath query. */
094    private String query = "";
095
096    /** Xpath expression. */
097    private XPathExpression xpathExpression;
098
099    /**
100     * Setter to specify Xpath query.
101     *
102     * @param query Xpath query.
103     * @throws IllegalStateException if creation of xpath expression fails
104     * @since 8.39
105     */
106    public void setQuery(String query) {
107        this.query = query;
108        if (!query.isEmpty()) {
109            try {
110                final XPathEvaluator xpathEvaluator =
111                        new XPathEvaluator(Configuration.newConfiguration());
112                xpathExpression = xpathEvaluator.createExpression(query);
113            }
114            catch (XPathException ex) {
115                throw new IllegalStateException("Creating Xpath expression failed: " + query, ex);
116            }
117        }
118    }
119
120    @Override
121    public int[] getDefaultTokens() {
122        return getRequiredTokens();
123    }
124
125    @Override
126    public int[] getAcceptableTokens() {
127        return getRequiredTokens();
128    }
129
130    @Override
131    public int[] getRequiredTokens() {
132        return CommonUtil.EMPTY_INT_ARRAY;
133    }
134
135    @Override
136    public boolean isCommentNodesRequired() {
137        return true;
138    }
139
140    @Override
141    public void beginTree(DetailAST rootAST) {
142        if (!query.isEmpty()) {
143            final List<DetailAST> matchingNodes = findMatchingNodesByXpathQuery(rootAST);
144            matchingNodes.forEach(node -> log(node, MSG_KEY));
145        }
146    }
147
148    /**
149     * Find nodes that match query.
150     *
151     * @param rootAST root node
152     * @return list of matching nodes
153     * @throws IllegalStateException if evaluation of xpath query fails
154     */
155    private List<DetailAST> findMatchingNodesByXpathQuery(DetailAST rootAST) {
156        try {
157            final RootNode rootNode = new RootNode(rootAST);
158            final XPathDynamicContext xpathDynamicContext =
159                    xpathExpression.createDynamicContext(rootNode);
160            final List<Item> matchingItems = xpathExpression.evaluate(xpathDynamicContext);
161            return matchingItems.stream()
162                    .map(item -> (DetailAST) ((AbstractNode) item).getUnderlyingNode())
163                    .collect(Collectors.toUnmodifiableList());
164        }
165        catch (XPathException ex) {
166            throw new IllegalStateException("Evaluation of Xpath query failed: " + query, ex);
167        }
168    }
169}