View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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<Boolean>() {
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       * @param <T> the type of nodes expected
97       * @param node the start node
98       * @param xpath the {@link XPathAdapter} to search for
99       * @param prefixResolver the {@link PrefixResolver} to be used
100      * @return a list of nodes matching the given xpath
101      * @throws TransformerException in case of error
102      */
103     public static <T> List<T> getByXPath(final Node node, final XPathAdapter xpath,
104             final PrefixResolver prefixResolver) throws TransformerException {
105         final List<T> list = new ArrayList<>();
106 
107         PROCESS_XPATH_.set(Boolean.TRUE);
108         try {
109             final XPathContext xpathSupport = new XPathContext();
110             final int ctxtNode = xpathSupport.getDTMHandleFromNode(node);
111             final XObject result = xpath.execute(xpathSupport, ctxtNode, prefixResolver);
112 
113             if (result instanceof XNodeSet) {
114                 final NodeList nodelist = result.nodelist();
115                 final int length = nodelist.getLength();
116                 for (int i = 0; i < length; i++) {
117                     list.add((T) nodelist.item(i));
118                 }
119             }
120             else if (result instanceof XNumber) {
121                 list.add((T) Double.valueOf(result.num()));
122             }
123             else if (result instanceof XBoolean) {
124                 list.add((T) Boolean.valueOf(result.bool()));
125             }
126             else if (result instanceof XString) {
127                 list.add((T) result.str());
128             }
129             else {
130                 throw new RuntimeException("Unproccessed " + result.getClass().getName());
131             }
132         }
133         finally {
134             PROCESS_XPATH_.set(Boolean.FALSE);
135         }
136 
137         return list;
138     }
139 
140     /**
141      * Returns whether the thread is currently evaluating XPath expression or no.
142      * @return whether the thread is currently evaluating XPath expression or no
143      */
144     public static boolean isProcessingXPath() {
145         return PROCESS_XPATH_.get().booleanValue();
146     }
147 
148 }