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    @Override protected void reset() {
035        for (Evaluator evaluator : evaluators) {
036            evaluator.reset();
037        }
038        super.reset();
039    }
040
041    @Override protected int cost() {
042        return cost;
043    }
044
045    @Nullable Evaluator rightMostEvaluator() {
046        return num > 0 ? evaluators.get(num - 1) : null;
047    }
048    
049    void replaceRightMostEvaluator(Evaluator replacement) {
050        evaluators.set(num - 1, replacement);
051        updateEvaluators();
052    }
053
054    void updateEvaluators() {
055        // used so we don't need to bash on size() for every match test
056        num = evaluators.size();
057
058        // sort the evaluators by lowest cost first, to optimize the evaluation order
059        cost = 0;
060        for (Evaluator evaluator : evaluators) {
061            cost += evaluator.cost();
062        }
063        sortedEvaluators.clear();
064        sortedEvaluators.addAll(evaluators);
065        sortedEvaluators.sort(Comparator.comparingInt(Evaluator::cost));
066    }
067
068    public static final class And extends CombiningEvaluator {
069        public And(Collection<Evaluator> evaluators) {
070            super(evaluators);
071        }
072
073        And(Evaluator... evaluators) {
074            this(Arrays.asList(evaluators));
075        }
076
077        @Override
078        public boolean matches(Element root, Element element) {
079            for (int i = 0; i < num; i++) {
080                Evaluator s = sortedEvaluators.get(i);
081                if (!s.matches(root, element))
082                    return false;
083            }
084            return true;
085        }
086
087        @Override
088        public String toString() {
089            return StringUtil.join(evaluators, "");
090        }
091    }
092
093    public static final class Or extends CombiningEvaluator {
094        /**
095         * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR.
096         * @param evaluators initial OR clause (these are wrapped into an AND evaluator).
097         */
098        public Or(Collection<Evaluator> evaluators) {
099            super();
100            if (num > 1)
101                this.evaluators.add(new And(evaluators));
102            else // 0 or 1
103                this.evaluators.addAll(evaluators);
104            updateEvaluators();
105        }
106
107        Or(Evaluator... evaluators) { this(Arrays.asList(evaluators)); }
108
109        Or() {
110            super();
111        }
112
113        public void add(Evaluator e) {
114            evaluators.add(e);
115            updateEvaluators();
116        }
117
118        @Override
119        public boolean matches(Element root, Element node) {
120            for (int i = 0; i < num; i++) {
121                Evaluator s = sortedEvaluators.get(i);
122                if (s.matches(root, node))
123                    return true;
124            }
125            return false;
126        }
127
128        @Override
129        public String toString() {
130            return StringUtil.join(evaluators, ", ");
131        }
132    }
133}