001package org.jsoup.select;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.nodes.Element;
005import org.jsoup.nodes.LeafNode;
006import org.jsoup.nodes.Node;
007import org.jsoup.nodes.TextNode;
008import org.jspecify.annotations.Nullable;
009
010import java.util.stream.Collectors;
011import java.util.stream.Stream;
012
013import static java.util.stream.Collectors.toCollection;
014
015/**
016 * Collects a list of elements that match the supplied criteria.
017 *
018 * @author Jonathan Hedley
019 */
020public class Collector {
021
022    private Collector() {}
023
024    /**
025     Build a list of elements, by visiting the root and every descendant of root, and testing it against the Evaluator.
026     @param eval Evaluator to test elements against
027     @param root root of tree to descend
028     @return list of matches; empty if none
029     */
030    public static Elements collect(Evaluator eval, Element root) {
031        Stream<Element> stream = eval.wantsNodes() ?
032            streamNodes(eval, root, Element.class) :
033            stream(eval, root);
034        Elements els = stream.collect(toCollection(Elements::new));
035        eval.reset(); // drops any held memos
036        return els;
037    }
038
039    /**
040     Obtain a Stream of elements by visiting the root and every descendant of root and testing it against the evaluator.
041
042     @param evaluator Evaluator to test elements against
043     @param root root of tree to descend
044     @return A {@link Stream} of matches
045     @since 1.19.1
046     */
047    public static Stream<Element> stream(Evaluator evaluator, Element root) {
048        evaluator.reset();
049        return root.stream().filter(evaluator.asPredicate(root));
050    }
051
052    /**
053     Obtain a Stream of nodes, of the specified type, by visiting the root and every descendant of root and testing it
054     against the evaluator.
055
056     @param evaluator Evaluator to test elements against
057     @param root root of tree to descend
058     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
059     @param <T> the type of node to collect
060     @return A {@link Stream} of matches
061     @since 1.21.1
062     */
063    public static <T extends Node> Stream<T> streamNodes(Evaluator evaluator, Element root, Class<T> type) {
064        evaluator.reset();
065        return root.nodeStream(type).filter(evaluator.asNodePredicate(root));
066    }
067
068    /**
069     Finds the first Element that matches the Evaluator that descends from the root, and stops the query once that first
070     match is found.
071     @param eval Evaluator to test elements against
072     @param root root of tree to descend
073     @return the first match; {@code null} if none
074     */
075    public static @Nullable Element findFirst(Evaluator eval, Element root) {
076        Element el = stream(eval, root).findFirst().orElse(null);
077        eval.reset();
078        return el;
079    }
080
081    /**
082     Finds the first Node that matches the Evaluator that descends from the root, and stops the query once that first
083     match is found.
084
085     @param eval Evaluator to test elements against
086     @param root root of tree to descend
087     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
088     @return the first match; {@code null} if none
089     @since 1.21.1
090     */
091    public static <T extends Node> @Nullable T findFirstNode(Evaluator eval, Element root, Class<T> type) {
092        T node = streamNodes(eval, root, type).findFirst().orElse(null);
093        eval.reset();
094        return node;
095    }
096
097    /**
098     Build a list of nodes that match the supplied criteria, by visiting the root and every descendant of root, and
099     testing it against the Evaluator.
100
101     @param evaluator Evaluator to test elements against
102     @param root root of tree to descend
103     @param type the type of node to collect (e.g. {@link Element}, {@link LeafNode}, {@link TextNode} etc)
104     @param <T> the type of node to collect
105     @return list of matches; empty if none
106     */
107    public static <T extends Node> Nodes<T> collectNodes(Evaluator evaluator, Element root, Class<T> type) {
108        return streamNodes(evaluator, root, type).collect(toCollection(Nodes::new));
109    }
110}