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