View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html.xpath;
16  
17  import java.util.ArrayList;
18  import java.util.List;
19  
20  import javax.xml.transform.TransformerException;
21  
22  import org.htmlunit.html.DomNode;
23  import org.htmlunit.xpath.XPathContext;
24  import org.htmlunit.xpath.objects.XBoolean;
25  import org.htmlunit.xpath.objects.XNodeSet;
26  import org.htmlunit.xpath.objects.XNumber;
27  import org.htmlunit.xpath.objects.XObject;
28  import org.htmlunit.xpath.objects.XString;
29  import org.htmlunit.xpath.xml.utils.PrefixResolver;
30  import org.w3c.dom.Document;
31  import org.w3c.dom.Node;
32  import org.w3c.dom.NodeList;
33  
34  /**
35   * Collection of XPath utility methods.
36   *
37   * @author Ahmed Ashour
38   * @author Chuck Dumont
39   * @author Ronald Brill
40   */
41  public final class XPathHelper {
42  
43      private static final ThreadLocal<Boolean> PROCESS_XPATH_ = new ThreadLocal<>() {
44          @Override
45          protected synchronized Boolean initialValue() {
46              return Boolean.FALSE;
47          }
48      };
49  
50      /**
51       * Private to avoid instantiation.
52       */
53      private XPathHelper() {
54          // Empty.
55      }
56  
57      /**
58       * Evaluates an XPath expression from the specified node, returning the resultant nodes.
59       *
60       * @param <T> the type class
61       * @param contextNode the node to start searching from
62       * @param xpathExpr the XPath expression
63       * @param prefixResolver the prefix resolver to use for resolving namespace prefixes, or null
64       * @return the list of objects found
65       */
66      public static <T> List<T> getByXPath(final DomNode contextNode, final String xpathExpr,
67              final PrefixResolver prefixResolver) {
68          if (xpathExpr == null) {
69              throw new IllegalArgumentException("Null is not a valid XPath expression");
70          }
71  
72          PrefixResolver resolver = prefixResolver;
73          if (resolver == null) {
74              final Node xpathExpressionContext;
75              if (contextNode.getNodeType() == Node.DOCUMENT_NODE) {
76                  xpathExpressionContext = ((Document) contextNode).getDocumentElement();
77              }
78              else {
79                  xpathExpressionContext = contextNode;
80              }
81  
82              resolver = new HtmlUnitPrefixResolver(xpathExpressionContext);
83          }
84  
85          try {
86              final boolean caseSensitive = contextNode.getPage().hasCaseSensitiveTagNames();
87              final XPathAdapter xpath = new XPathAdapter(xpathExpr, resolver, caseSensitive);
88              return getByXPath(contextNode, xpath, prefixResolver);
89          }
90          catch (final Exception e) {
91              throw new RuntimeException("Could not retrieve XPath >" + xpathExpr + "< on " + contextNode, e);
92          }
93      }
94  
95      /**
96       * Evaluates the specified XPath expression against the given node and returns the result
97       * as a list. Depending on the XPath result type, the list contains matching nodes,
98       * a single {@link Double}, {@link Boolean}, or {@link String} value.
99       *
100      * @param <T> the type of nodes expected
101      * @param node the start node
102      * @param xpath the {@link XPathAdapter} to search for
103      * @param prefixResolver the {@link PrefixResolver} to be used
104      * @return a list of nodes matching the given xpath, or a singleton list containing
105      *         the evaluated number, boolean, or string value
106      * @throws TransformerException in case of error
107      */
108     public static <T> List<T> getByXPath(final Node node, final XPathAdapter xpath,
109             final PrefixResolver prefixResolver) throws TransformerException {
110         final List<T> list = new ArrayList<>();
111 
112         PROCESS_XPATH_.set(Boolean.TRUE);
113         try {
114             final XPathContext xpathSupport = new XPathContext();
115             final int ctxtNode = xpathSupport.getDTMHandleFromNode(node);
116             final XObject result = xpath.execute(xpathSupport, ctxtNode, prefixResolver);
117 
118             if (result instanceof XNodeSet) {
119                 final NodeList nodelist = result.nodelist();
120                 final int length = nodelist.getLength();
121                 for (int i = 0; i < length; i++) {
122                     list.add((T) nodelist.item(i));
123                 }
124             }
125             else if (result instanceof XNumber) {
126                 list.add((T) Double.valueOf(result.num()));
127             }
128             else if (result instanceof XBoolean) {
129                 list.add((T) Boolean.valueOf(result.bool()));
130             }
131             else if (result instanceof XString) {
132                 list.add((T) result.str());
133             }
134             else {
135                 throw new RuntimeException("Unprocessed " + result.getClass().getName());
136             }
137         }
138         finally {
139             PROCESS_XPATH_.set(Boolean.FALSE);
140         }
141 
142         return list;
143     }
144 
145     /**
146      * Returns whether the thread is currently evaluating XPath expression or no.
147      * @return whether the thread is currently evaluating XPath expression or no
148      */
149     public static boolean isProcessingXPath() {
150         return PROCESS_XPATH_.get().booleanValue();
151     }
152 
153 }