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.javascript.host.dom;
16  
17  import java.util.List;
18  
19  import org.htmlunit.html.DomAttr;
20  import org.htmlunit.html.DomNode;
21  import org.htmlunit.javascript.HtmlUnitScriptable;
22  import org.htmlunit.javascript.JavaScriptEngine;
23  import org.htmlunit.javascript.configuration.JsxClass;
24  import org.htmlunit.javascript.configuration.JsxConstant;
25  import org.htmlunit.javascript.configuration.JsxConstructor;
26  import org.htmlunit.javascript.configuration.JsxFunction;
27  import org.htmlunit.javascript.configuration.JsxGetter;
28  
29  /**
30   * A JavaScript object for {@code XPathResult}.
31   *
32   * @author Ahmed Ashour
33   * @author Chuck Dumont
34   * @author Ronald Brill
35   */
36  @JsxClass
37  public class XPathResult extends HtmlUnitScriptable {
38  
39      /**
40       * This code does not represent a specific type.
41       * An evaluation of an XPath expression will never produce this type. If this type is requested,
42       * then the evaluation returns whatever type naturally results from evaluation of the expression.
43       */
44      @JsxConstant
45      public static final int ANY_TYPE = 0;
46  
47      /**
48       * The result is a number.
49       */
50      @JsxConstant
51      public static final int NUMBER_TYPE = 1;
52  
53      /**
54       * The result is a string.
55       */
56      @JsxConstant
57      public static final int STRING_TYPE = 2;
58  
59      /**
60       * The result is a boolean.
61       */
62      @JsxConstant
63      public static final int BOOLEAN_TYPE = 3;
64  
65      /**
66       * The result is a node set that will be accessed iteratively, which may not produce nodes in a particular order.
67       * This is the default type returned if the result is a node set and {@link #ANY_TYPE} is requested.
68       */
69      @JsxConstant
70      public static final int UNORDERED_NODE_ITERATOR_TYPE = 4;
71  
72      /**
73       * The result is a node set that will be accessed iteratively, which will produce document-ordered nodes.
74       */
75      @JsxConstant
76      public static final int ORDERED_NODE_ITERATOR_TYPE = 5;
77  
78      /**
79       * The result is a node set that will be accessed as a snapshot list of nodes
80       * that may not be in a particular order.
81       */
82      @JsxConstant
83      public static final int UNORDERED_NODE_SNAPSHOT_TYPE = 6;
84  
85      /**
86       * The result is a node set that will be accessed as a snapshot list of nodes
87       * that will be in original document order.
88       */
89      @JsxConstant
90      public static final int ORDERED_NODE_SNAPSHOT_TYPE = 7;
91  
92      /**
93       * The result is a node set and will be accessed as a single node, which may be null if the node set is empty.
94       * If there is more than one node in the actual result,
95       * the single node returned might not be the first in document order.
96       */
97      @JsxConstant
98      public static final int ANY_UNORDERED_NODE_TYPE = 8;
99  
100     /**
101      * The result is a node set and will be accessed as a single node, which may be null if the node set is empty.
102      * If there are more than one node in the actual result,
103      * the single node returned will be the first in document order.
104      */
105     @JsxConstant
106     public static final int FIRST_ORDERED_NODE_TYPE = 9;
107 
108     private List<?> result_;
109     private int resultType_;
110 
111     /**
112      * The index of the next result.
113      */
114     private int iteratorIndex_;
115 
116     /**
117      * Creates an instance.
118      */
119     @JsxConstructor
120     public void jsConstructor() {
121         throw JavaScriptEngine.typeErrorIllegalConstructor();
122     }
123 
124     /**
125      * Initializes this XPath result.
126      *
127      * @param result the evaluation result
128      * @param type the requested result type; if a specific type is specified,
129      *        the result will be returned as the corresponding type
130      */
131     void init(final List<?> result, final int type) {
132         result_ = result;
133         resultType_ = type;
134 
135         if (type == ANY_TYPE) {
136             resultType_ = UNORDERED_NODE_ITERATOR_TYPE;
137 
138             if (result_.size() == 1) {
139                 final Object o = result_.get(0);
140                 if (o instanceof Number) {
141                     resultType_ = NUMBER_TYPE;
142                 }
143                 else if (o instanceof String) {
144                     resultType_ = STRING_TYPE;
145                 }
146                 else if (o instanceof Boolean) {
147                     resultType_ = BOOLEAN_TYPE;
148                 }
149             }
150         }
151 
152         iteratorIndex_ = 0;
153     }
154 
155     /**
156      * Returns the type of this result.
157      *
158      * @return the code representing the type of this result
159      */
160     @JsxGetter
161     public int getResultType() {
162         return resultType_;
163     }
164 
165     /**
166      * Returns the number of nodes in this snapshot.
167      *
168      * @return the number of nodes in the result snapshot
169      */
170     @JsxGetter
171     public int getSnapshotLength() {
172         if (resultType_ != UNORDERED_NODE_SNAPSHOT_TYPE && resultType_ != ORDERED_NODE_SNAPSHOT_TYPE) {
173             throw JavaScriptEngine.reportRuntimeError("Cannot get snapshotLength for type: " + resultType_);
174         }
175         return result_.size();
176     }
177 
178     /**
179      * Returns the value of this single-node result.
180      *
181      * @return the value of this single-node result, which may be {@code null}
182      */
183     @JsxGetter
184     public Node getSingleNodeValue() {
185         if (resultType_ != ANY_UNORDERED_NODE_TYPE && resultType_ != FIRST_ORDERED_NODE_TYPE) {
186             throw JavaScriptEngine.reportRuntimeError("Cannot get singleNodeValue for type: " + resultType_);
187         }
188         if (!result_.isEmpty()) {
189             return ((DomNode) result_.get(0)).getScriptableObject();
190         }
191         return null;
192     }
193 
194     /**
195      * Returns whether this iterator has become invalid.
196      *
197      * @return {@code true} if {@code resultType} is
198      *         {@link #UNORDERED_NODE_ITERATOR_TYPE} or
199      *         {@link #ORDERED_NODE_ITERATOR_TYPE} and the document has been
200      *         modified since this result was returned
201      */
202     @JsxGetter
203     public boolean isInvalidIteratorState() {
204         return false;
205     }
206 
207     /**
208      * Returns the next node in the result.
209      *
210      * @return the next node, or {@code null} if there are no more nodes
211      */
212     @JsxFunction
213     public Node iterateNext() {
214         if (resultType_ != UNORDERED_NODE_ITERATOR_TYPE && resultType_ != ORDERED_NODE_ITERATOR_TYPE) {
215             throw JavaScriptEngine.reportRuntimeError("Cannot get iterateNext for type: " + resultType_);
216         }
217         if (iteratorIndex_ < result_.size()) {
218             return ((DomNode) result_.get(iteratorIndex_++)).getScriptableObject();
219         }
220         return null;
221     }
222 
223     /**
224      * Returns the node at the specified index in the snapshot.
225      *
226      * @param index the index into the snapshot collection
227      * @return the node at the specified index, or {@code null} if the index
228      *         is out of range
229      */
230     @JsxFunction
231     public Node snapshotItem(final int index) {
232         if (resultType_ != UNORDERED_NODE_SNAPSHOT_TYPE && resultType_ != ORDERED_NODE_SNAPSHOT_TYPE) {
233             throw JavaScriptEngine.reportRuntimeError("Cannot get snapshotLength for type: " + resultType_);
234         }
235         if (index >= 0 && index < result_.size()) {
236             return ((DomNode) result_.get(index)).getScriptableObject();
237         }
238         return null;
239     }
240 
241     /**
242      * Returns the value of this numeric result.
243      *
244      * @return the value of this numeric result
245      */
246     @JsxGetter
247     public double getNumberValue() {
248         if (resultType_ != NUMBER_TYPE) {
249             throw JavaScriptEngine.reportRuntimeError("Cannot get numberValue for type: " + resultType_);
250         }
251 
252         if (result_.size() == 1) {
253             final Object o = result_.get(0);
254             if (o instanceof Number) {
255                 return ((Double) o).doubleValue();
256             }
257             if (o instanceof Boolean boolean1) {
258                 return boolean1.booleanValue() ? 1 : 0;
259             }
260         }
261 
262         final String asString = asString();
263         double answer;
264         try {
265             answer = Double.parseDouble(asString);
266         }
267         catch (final NumberFormatException e) {
268             answer = Double.NaN;
269         }
270         return answer;
271     }
272 
273     /**
274      * Returns the value of this boolean result.
275      *
276      * @return the value of this boolean result
277      */
278     @JsxGetter
279     @SuppressWarnings("PMD.BooleanGetMethodName")
280     public boolean getBooleanValue() {
281         if (resultType_ != BOOLEAN_TYPE) {
282             throw JavaScriptEngine.reportRuntimeError("Cannot get booleanValue for type: " + resultType_);
283         }
284 
285         if (result_.size() == 1) {
286             final Object o = result_.get(0);
287             if (o instanceof Number) {
288                 final double d = ((Double) o).doubleValue();
289                 if (Double.isNaN(d) || Double.isInfinite(d)) {
290                     return true;
291                 }
292 
293                 return 0.0 != d;
294             }
295             if (o instanceof String string) {
296                 return !string.isEmpty();
297             }
298             if (o instanceof Boolean boolean1) {
299                 return boolean1.booleanValue();
300             }
301         }
302 
303         return !result_.isEmpty();
304     }
305 
306     /**
307      * Returns the value of this string result.
308      *
309      * @return the value of this string result
310      */
311     @JsxGetter
312     public String getStringValue() {
313         if (resultType_ != STRING_TYPE) {
314             throw JavaScriptEngine.reportRuntimeError("Cannot get stringValue for type: " + resultType_);
315         }
316         return asString();
317     }
318 
319     private String asString() {
320         if (result_.isEmpty()) {
321             return "";
322         }
323 
324         final Object resultObj = result_.get(0);
325         if (resultObj instanceof DomAttr attr) {
326             return attr.getValue();
327         }
328         if (resultObj instanceof DomNode node) {
329             return node.asNormalizedText();
330         }
331         return resultObj.toString();
332     }
333 }