001package org.jsoup.select;
002
003import org.jsoup.internal.StringUtil;
004import org.jsoup.nodes.Element;
005import org.jsoup.nodes.LeafNode;
006import org.jsoup.nodes.Node;
007import org.jspecify.annotations.Nullable;
008
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.Collection;
012import java.util.Comparator;
013import java.util.List;
014
015/**
016 * Base combining (and, or) evaluator.
017 */
018public abstract class CombiningEvaluator extends Evaluator {
019    final ArrayList<Evaluator> evaluators; // maintain original order so that #toString() is sensible
020    final List<Evaluator> sortedEvaluators; // cost ascending order
021    int num = 0;
022    int cost = 0;
023    boolean wantsNodes;
024
025    CombiningEvaluator() {
026        super();
027        evaluators = new ArrayList<>();
028        sortedEvaluators = new ArrayList<>();
029    }
030
031    CombiningEvaluator(Collection<Evaluator> evaluators) {
032        this();
033        this.evaluators.addAll(evaluators);
034        updateEvaluators();
035    }
036
037    public void add(Evaluator e) {
038        evaluators.add(e);
039        updateEvaluators();
040    }
041
042    @Override protected void reset() {
043        for (Evaluator evaluator : evaluators) {
044            evaluator.reset();
045        }
046        super.reset();
047    }
048
049    @Override protected int cost() {
050        return cost;
051    }
052
053    @Override
054    boolean wantsNodes() {
055        return wantsNodes;
056    }
057
058    void updateEvaluators() {
059        // used so we don't need to bash on size() for every match test
060        num = evaluators.size();
061
062        // sort the evaluators by lowest cost first, to optimize the evaluation order
063        cost = 0;
064        for (Evaluator evaluator : evaluators) {
065            cost += evaluator.cost();
066        }
067        sortedEvaluators.clear();
068        sortedEvaluators.addAll(evaluators);
069        sortedEvaluators.sort(Comparator.comparingInt(Evaluator::cost));
070
071        // any want nodes?
072        for (Evaluator evaluator : evaluators) {
073            if (evaluator.wantsNodes()) {
074                wantsNodes = true;
075                break;
076            }
077        }
078    }
079
080    public static final class And extends CombiningEvaluator {
081        public And(Collection<Evaluator> evaluators) {
082            super(evaluators);
083        }
084
085        And(Evaluator... evaluators) {
086            this(Arrays.asList(evaluators));
087        }
088
089        @Override
090        public boolean matches(Element root, Element el) {
091            for (int i = 0; i < num; i++) {
092                Evaluator eval = sortedEvaluators.get(i);
093                if (!eval.matches(root, el))
094                    return false;
095            }
096            return true;
097        }
098
099        @Override
100        public boolean matches(Element root, LeafNode leaf) {
101            for (int i = 0; i < num; i++) {
102                Evaluator eval = sortedEvaluators.get(i);
103                if (!eval.matches(root, leaf))
104                    return false;
105            }
106            return true;
107        }
108
109        @Override
110        public String toString() {
111            return StringUtil.join(evaluators, "");
112        }
113    }
114
115    public static final class Or extends CombiningEvaluator {
116        /**
117         * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR.
118         * @param evaluators initial OR clause (these are wrapped into an AND evaluator).
119         */
120        public Or(Collection<Evaluator> evaluators) {
121            super();
122            if (num > 1)
123                this.evaluators.add(new And(evaluators));
124            else // 0 or 1
125                this.evaluators.addAll(evaluators);
126            updateEvaluators();
127        }
128
129        Or(Evaluator... evaluators) { this(Arrays.asList(evaluators)); }
130
131        Or() {
132            super();
133        }
134
135        @Override
136        public boolean matches(Element root, Element element) {
137            for (int i = 0; i < num; i++) {
138                Evaluator eval = sortedEvaluators.get(i);
139                if (eval.matches(root, element))
140                    return true;
141            }
142            return false;
143        }
144
145        @Override
146        public boolean matches(Element root, LeafNode leaf) {
147            for (int i = 0; i < num; i++) {
148                Evaluator eval = sortedEvaluators.get(i);
149                if (eval.matches(root, leaf))
150                    return true;
151            }
152            return false;
153        }
154
155        @Override
156        public String toString() {
157            return StringUtil.join(evaluators, ", ");
158        }
159    }
160}