001package org.jsoup.parser;
002
003import org.jsoup.Connection;
004import org.jsoup.helper.Validate;
005import org.jsoup.nodes.Document;
006import org.jsoup.nodes.Element;
007import org.jsoup.nodes.Node;
008import org.jsoup.select.Evaluator;
009import org.jsoup.select.EvaluatorInternals;
010import org.jsoup.select.NodeVisitor;
011import org.jsoup.select.Selector;
012import org.jspecify.annotations.Nullable;
013
014import java.io.Closeable;
015import java.io.IOException;
016import java.io.Reader;
017import java.io.StringReader;
018import java.io.UncheckedIOException;
019import java.util.HashSet;
020import java.util.Iterator;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.NoSuchElementException;
024import java.util.Queue;
025import java.util.Spliterator;
026import java.util.Spliterators;
027import java.util.stream.Stream;
028import java.util.stream.StreamSupport;
029
030/**
031 A StreamParser provides a progressive parse of its input. As each Element is completed, it is emitted via a Stream or
032 Iterator interface. Elements returned will be complete with all their children, and an (empty) next sibling, if
033 applicable.
034 <p>To conserve memory, you can {@link Node#remove() remove()} Elements (or their children) from the DOM during the
035 parse. This provides a mechanism to parse an input document that would otherwise be too large to fit into memory, yet
036 still providing a DOM interface to the document and its elements.</p>
037 <p>
038 Additionally, the parser provides a {@link #selectFirst(String query)} / {@link #selectNext(String query)}, which will
039 run the parser until a hit is found, at which point the parse is suspended. It can be resumed via another
040 {@code select()} call, or via the {@link #stream()} or {@link #iterator()} methods.
041 </p>
042 <p>Once the input has been fully read, the input Reader will be closed. Or, if the whole document does not need to be
043 read, call {@link #stop()} and {@link #close()}.</p>
044 <p>The {@link #document()} method will return the Document being parsed into, which will be only partially complete
045 until the input is fully consumed.</p>
046 <p>A StreamParser can be reused via a new {@link #parse(Reader, String)}, but is not thread-safe for concurrent inputs.
047 New parsers should be used in each thread.</p>
048 <p>If created via {@link Connection.Response#streamParser()}, or another Reader that is I/O backed, the iterator and
049 stream consumers will throw an {@link java.io.UncheckedIOException} if the underlying Reader errors during read.</p>
050 <p>For examples, see the jsoup
051 <a href="https://jsoup.org/cookbook/input/streamparser-dom-sax">StreamParser cookbook.</a></p>
052 <p>
053 Selectors that depend on knowing all siblings (e.g. {@code :last-child}, {@code :last-of-type}, {@code :nth-last-child},
054 {@code :only-child} and their negations) cannot be correctly evaluated while streaming, because the parser does not know
055 if a later sibling will appear. For those cases, run {@link #complete()} first to finish the parse (which is effectively
056 the same as using {@code Jsoup.parse(...)} unless you have already removed nodes during streaming).
057 </p>
058 @since 1.18.1 */
059public class StreamParser implements Closeable {
060    final private Parser parser;
061    final TreeBuilder treeBuilder;
062    final private ElementIterator it = new ElementIterator();
063    @Nullable private Document document;
064    private boolean stopped = true;
065
066    /**
067     Construct a new StreamParser, using the supplied base Parser.
068     The supplied parser provides configuration and collects parse errors; streaming uses an independent tree builder.
069     @param parser the configured base parser
070     */
071    public StreamParser(Parser parser) {
072        this.parser = parser;
073        treeBuilder = parser.getTreeBuilder().newInstance();
074        treeBuilder.nodeListener(it);
075    }
076
077    /**
078     Provide the input for a Document parse. The input is not read until a consuming operation is called.
079     @param input the input to be read.
080     @param baseUri the URL of this input, for absolute link resolution
081     @return this parser, for chaining
082     */
083    public StreamParser parse(Reader input, String baseUri) {
084        close(); // probably a no-op, but ensures any previous reader is closed
085        it.reset();
086        treeBuilder.initialiseParse(input, baseUri, parser); // reader is not read, so no chance of IO error
087        document = treeBuilder.doc;
088        return this;
089    }
090
091    /**
092     Provide the input for a Document parse. The input is not read until a consuming operation is called.
093     @param input the input to be read
094     @param baseUri the URL of this input, for absolute link resolution
095     @return this parser
096     */
097    public StreamParser parse(String input, String baseUri) {
098        return parse(new StringReader(input), baseUri);
099    }
100
101    /**
102     Provide the input for a fragment parse. The input is not read until a consuming operation is called.
103     @param input the input to be read
104     @param context the optional fragment context element
105     @param baseUri the URL of this input, for absolute link resolution
106     @return this parser
107     @see #completeFragment()
108     */
109    public StreamParser parseFragment(Reader input, @Nullable Element context, String baseUri) {
110        parse(input, baseUri);
111        treeBuilder.initialiseParseFragment(context);
112        return this;
113    }
114
115    /**
116     Provide the input for a fragment parse. The input is not read until a consuming operation is called.
117     @param input the input to be read
118     @param context the optional fragment context element
119     @param baseUri the URL of this input, for absolute link resolution
120     @return this parser
121     @see #completeFragment()
122     */
123    public StreamParser parseFragment(String input, @Nullable Element context, String baseUri) {
124        return parseFragment(new StringReader(input), context, baseUri);
125    }
126
127    /**
128     Creates a {@link Stream} of {@link Element}s, with the input being parsed as each element is consumed. Each
129     Element returned will be complete (that is, all of its children will be included, and if it has a next sibling, that
130     (empty) sibling will exist at {@link Element#nextElementSibling()}). The stream will be emitted in document order as
131     each element is closed. That means that child elements will be returned prior to their parents.
132     <p>The stream will start from the current position of the backing iterator and the parse.</p>
133     <p>When consuming the stream, if the Reader that the Parser is reading throws an I/O exception (for example a
134     SocketTimeoutException), that will be emitted as an {@link UncheckedIOException}</p>
135     @return a stream of Element objects
136     @throws UncheckedIOException if the underlying Reader excepts during a read (in stream consuming methods)
137     */
138    public Stream<Element> stream() {
139        return StreamSupport.stream(
140            Spliterators.spliteratorUnknownSize(
141                it, Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED),
142            false);
143    }
144
145    /**
146     Returns an {@link Iterator} of {@link Element}s, with the input being parsed as each element is consumed. Each
147     Element returned will be complete (that is, all of its children will be included, and if it has a next sibling, that
148     (empty) sibling will exist at {@link Element#nextElementSibling()}). The elements will be emitted in document order as
149     each element is closed. That means that child elements will be returned prior to their parents.
150     <p>The iterator will start from the current position of the parse.</p>
151     <p>The iterator is backed by this StreamParser, and the resources it holds.</p>
152     @return a stream of Element objects
153     */
154    public Iterator<Element> iterator() {
155        //noinspection ReturnOfInnerClass
156        return it;
157    }
158
159    /**
160     Flags that the parse should be stopped; the backing iterator will not return any more Elements.
161     @return this parser
162     */
163    public StreamParser stop() {
164        stopped = true;
165        return this;
166    }
167
168    /**
169     Closes the input and releases resources including the underlying parser and reader.
170     <p>The parser will also be closed when the input is fully read.</p>
171     <p>The parser can be reused with another call to {@link #parse(Reader, String)}.</p>
172    */
173    @Override public void close() {
174        it.deferOpenElements();
175        stopped = true;
176        treeBuilder.closeParse(); // closes the reader, frees resources
177    }
178
179    /**
180     Get the current {@link Document} as it is being parsed. It will be only partially complete until the input is fully
181     read. Structural changes (e.g. insert, remove) may be made to the Document contents.
182     @return the (partial) Document
183     */
184    public Document document() {
185        Validate.notNull(document, "Must run parse() before calling.");
186        return document;
187    }
188
189    /**
190     Runs the parser until the input is fully read, and returns the completed Document.
191     @return the completed Document
192     @throws IOException if an I/O error occurs
193     */
194    public Document complete() throws IOException {
195        Document doc = document();
196        treeBuilder.runParser();
197        return doc;
198    }
199
200    /**
201     When initialized as a fragment parse, runs the parser until the input is fully read, and returns the completed
202     fragment child nodes.
203     @return the completed child nodes
204     @throws IOException if an I/O error occurs
205     @see #parseFragment(Reader, Element, String)
206     */
207    public List<Node> completeFragment() throws IOException {
208        treeBuilder.runParser();
209        return treeBuilder.completeParseFragment();
210    }
211
212    /**
213     Finds the first Element that matches the provided query. If the parsed Document does not already have a match, the
214     input will be parsed until the first match is ready for stream emission, or the input is completely read.
215     @param query the {@link org.jsoup.select.Selector} query.
216     @return the first matching {@link Element}, or {@code null} if there's no match
217     @throws IOException if an I/O error occurs
218     @see #selectFirst(Evaluator)
219     */
220    public @Nullable Element selectFirst(String query) throws IOException {
221        return selectFirst(Selector.evaluatorOf(query));
222    }
223
224    /**
225     Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}. This
226     is useful if you want to simply abort processing on a failed match.
227     @param query the {@link org.jsoup.select.Selector} query.
228     @return the first matching element
229     @throws IllegalArgumentException if no match is found
230     @throws IOException if an I/O error occurs
231     */
232    public Element expectFirst(String query) throws IOException {
233        return Validate.expectNotNull(
234            selectFirst(query),
235            "No elements matched the query '%s' in the document."
236            , query
237        );
238    }
239
240    /**
241     Finds the first Element that matches the provided query. If the parsed Document does not already have a match, the
242     input will be parsed until the first match is ready for stream emission, or the input is completely read.
243     <p>By providing a compiled evaluator vs. a CSS selector, this method may be more efficient when executing the same
244     query against multiple documents.</p>
245     @param eval the {@link org.jsoup.select.Selector} evaluator.
246     @return the first matching {@link Element}, or {@code null} if there's no match
247     @throws IOException if an I/O error occurs
248     @see Selector#evaluatorOf(String css)
249     */
250    public @Nullable Element selectFirst(Evaluator eval) throws IOException {
251        final Document doc = document();
252
253        // a ready match can be returned without advancing the iterator
254        Element first = doc.selectFirst(eval);
255        if (first != null && it.isPending(first)) {
256            if (!it.awaitReady(first)) return null;
257            first = doc.selectFirst(eval);
258        }
259        if (first != null && !it.isPending(first)) return first;
260
261        // use the iterator for linear progress
262        Element emitted = selectNext(eval);
263        if (emitted == null) {
264            if (!treeBuilder.isComplete()) return null;
265            return doc.selectFirst(eval);
266        }
267
268        // reconcile the iterator's child-first emission order with document order
269        while ((first = doc.selectFirst(eval)) != null && it.isPending(first)) {
270            if (!it.awaitReady(first)) return null;
271        }
272        return first != null ? first : emitted;
273    }
274
275    /**
276     Finds the next Element that matches the provided query. The input will be parsed until the next match is found, or
277     the input is completely read.
278     @param query the {@link org.jsoup.select.Selector} query.
279     @return the next matching {@link Element}, or {@code null} if there's no match
280     @throws IOException if an I/O error occurs
281     @see #selectNext(Evaluator)
282     */
283    public @Nullable Element selectNext(String query) throws IOException {
284        return selectNext(Selector.evaluatorOf(query));
285    }
286
287    /**
288     Just like {@link #selectFirst(String)}, but if there is no match, throws an {@link IllegalArgumentException}. This
289     is useful if you want to simply abort processing on a failed match.
290     @param query the {@link org.jsoup.select.Selector} query.
291     @return the first matching element
292     @throws IllegalArgumentException if no match is found
293     @throws IOException if an I/O error occurs
294     */
295    public Element expectNext(String query) throws IOException {
296        return Validate.expectNotNull(
297            selectNext(query),
298            "No elements matched the query '%s' in the document."
299            , query
300        );
301    }
302
303    /**
304     Finds the next Element that matches the provided query. The input will be parsed until the next match is found, or
305     the input is completely read.
306     <p>By providing a compiled evaluator vs a CSS selector, this method may be more efficient when executing the same
307     query against multiple documents.</p>
308     @param eval the {@link org.jsoup.select.Selector} evaluator.
309     @return the next matching {@link Element}, or {@code null} if there's no match
310     @throws IOException if an I/O error occurs
311     @see Selector#evaluatorOf(String css)
312     */
313    public @Nullable Element selectNext(Evaluator eval) throws IOException {
314        try {
315            final Document doc = document(); // validates the parse was initialized, keeps stack trace out of stream
316            return EvaluatorInternals.stream(eval, doc, stream())
317                .findFirst()
318                .orElse(null);
319        } catch (UncheckedIOException e) {
320            // Reader threw an IO exception emitted via Iterator's next()
321            throw e.getCause();
322        }
323    }
324
325    final class ElementIterator implements Iterator<Element>, NodeVisitor {
326        final private Queue<Element> emitQueue = new LinkedList<>(); // ready elements waiting for iterator delivery, in emission order
327        final private HashSet<Element> deferredEls = new HashSet<>(); // off-stack elements not yet ready for emission
328        private @Nullable Element current;  // most recently emitted
329        private @Nullable Element next;     // element waiting to be picked up
330        private @Nullable Element tail;     // The last tailed element (</html>), on hold for final pop
331
332        void reset() {
333            emitQueue.clear();
334            deferredEls.clear();
335            current = next = tail = null;
336            stopped = false;
337        }
338
339        /** Defers open Elements before an early close releases the tree-builder stack. */
340        void deferOpenElements() {
341            treeBuilder.copyOpenElementsTo(deferredEls);
342        }
343
344        /** Tests whether an Element has reached its iterator emission point. */
345        boolean isReady(Element element) {
346            return current == element || next == element || emitQueue.contains(element);
347        }
348
349        /** Tests whether an Element still needs parser advancement before selection. */
350        boolean isPending(Element element) {
351            // the document is pending until EOF; other pending elements are open or explicitly deferred
352            return !treeBuilder.isComplete() &&
353                (element == treeBuilder.doc || treeBuilder.isOpen(element) || deferredEls.contains(element));
354        }
355
356        /** Advances parsing until an Element is ready; returns false if parsing was stopped. */
357        boolean awaitReady(Element element) throws IOException {
358            try {
359                while (isPending(element) && !stopped) {
360                    if (!hasNext()) break;
361                    if (isPending(element)) next(); // hasNext may have buffered the target itself
362                }
363                return !isPending(element);
364            } catch (UncheckedIOException e) {
365                // preserve the checked I/O contract when iterator advancement reads input
366                throw e.getCause();
367            }
368        }
369
370        /** Marks an Element ready and queues it for iterator emission. */
371        void queueForEmission(Element element) {
372            deferredEls.remove(element);
373            emitQueue.add(element);
374        }
375
376        // Iterator Interface:
377        /**
378         {@inheritDoc}
379         @throws UncheckedIOException if the underlying Reader errors during a read
380         */
381        @Override public boolean hasNext() {
382            maybeFindNext();
383            return next != null;
384        }
385
386        /**
387         {@inheritDoc}
388         @throws UncheckedIOException if the underlying Reader errors during a read
389         */
390        @Override public Element next() {
391            maybeFindNext();
392            if (next == null) throw new NoSuchElementException();
393            current = next;
394            next = null;
395            return current;
396        }
397
398        private void maybeFindNext() {
399            if (stopped || next != null) return;
400
401            // drain the current queue before stepping to get more
402            if (!emitQueue.isEmpty()) {
403                next = emitQueue.remove();
404                return;
405            }
406
407            // step the parser, which will hit the node listeners to add to the queue:
408            while (treeBuilder.stepParser()) {
409                if (!emitQueue.isEmpty()) {
410                    next = emitQueue.remove();
411                    return;
412                }
413            }
414            close();
415
416            // send the final element out:
417            if (tail != null) {
418                deferredEls.remove(tail);
419                next = tail;
420                tail = null;
421            }
422        }
423
424        @Override public void remove() {
425            if (current == null) throw new NoSuchElementException();
426            current.remove();
427        }
428
429        // NodeVisitor Interface:
430        @Override public void head(Node node, int depth) {
431            if (node instanceof Element) {
432                Element prev = node.previousElementSibling();
433                // We prefer to wait until an element has a next sibling before emitting it; otherwise, get it in tail
434                if (prev != null) {
435                    queueForEmission(prev);
436                    if (tail == prev) tail = null; // queueing marks it ready
437                }
438            }
439        }
440
441        @Override public void tail(Node node, int depth) {
442            if (node instanceof Element) {
443                tail = (Element) node; // kept for final hit
444                if (!isReady(tail)) deferredEls.add(tail);
445                Element lastChild = tail.lastElementChild(); // won't get a nextsib, so emit that:
446                if (lastChild != null) queueForEmission(lastChild);
447                if (tail == treeBuilder.doc) deferredEls.clear(); // completed document makes stale recovery state irrelevant
448            }
449        }
450    }
451}