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.Collections;
011import java.util.Comparator;
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 ArrayList<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        Collections.sort(sortedEvaluators, costComparator);
066    }
067
068    private static final Comparator<Evaluator> costComparator = (o1, o2) -> o1.cost() - o2.cost();
069    // ^ comparingInt, sortedEvaluators.sort not available in targeted version
070
071    public static final class And extends CombiningEvaluator {
072        And(Collection<Evaluator> evaluators) {
073            super(evaluators);
074        }
075
076        And(Evaluator... evaluators) {
077            this(Arrays.asList(evaluators));
078        }
079
080        @Override
081        public boolean matches(Element root, Element element) {
082            for (int i = 0; i < num; i++) {
083                Evaluator s = sortedEvaluators.get(i);
084                if (!s.matches(root, element))
085                    return false;
086            }
087            return true;
088        }
089
090        @Override
091        public String toString() {
092            return StringUtil.join(evaluators, "");
093        }
094    }
095
096    public static final class Or extends CombiningEvaluator {
097        /**
098         * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR.
099         * @param evaluators initial OR clause (these are wrapped into an AND evaluator).
100         */
101        Or(Collection<Evaluator> evaluators) {
102            super();
103            if (num > 1)
104                this.evaluators.add(new And(evaluators));
105            else // 0 or 1
106                this.evaluators.addAll(evaluators);
107            updateEvaluators();
108        }
109
110        Or(Evaluator... evaluators) { this(Arrays.asList(evaluators)); }
111
112        Or() {
113            super();
114        }
115
116        public void add(Evaluator e) {
117            evaluators.add(e);
118            updateEvaluators();
119        }
120
121        @Override
122        public boolean matches(Element root, Element node) {
123            for (int i = 0; i < num; i++) {
124                Evaluator s = sortedEvaluators.get(i);
125                if (s.matches(root, node))
126                    return true;
127            }
128            return false;
129        }
130
131        @Override
132        public String toString() {
133            return StringUtil.join(evaluators, ", ");
134        }
135    }
136}