1 package org.jsoup.select; 2 3 import org.jsoup.internal.StringUtil; 4 import org.jsoup.nodes.Element; 5 import org.jspecify.annotations.Nullable; 6 7 import java.util.ArrayList; 8 import java.util.Arrays; 9 import java.util.Collection; 10 import java.util.Collections; 11 import java.util.Comparator; 12 13 /** 14 * Base combining (and, or) evaluator. 15 */ 16 public abstract class CombiningEvaluator extends Evaluator { 17 final ArrayList<Evaluator> evaluators; // maintain original order so that #toString() is sensible 18 final ArrayList<Evaluator> sortedEvaluators; // cost ascending order 19 int num = 0; 20 int cost = 0; 21 CombiningEvaluator()22 CombiningEvaluator() { 23 super(); 24 evaluators = new ArrayList<>(); 25 sortedEvaluators = new ArrayList<>(); 26 } 27 CombiningEvaluator(Collection<Evaluator> evaluators)28 CombiningEvaluator(Collection<Evaluator> evaluators) { 29 this(); 30 this.evaluators.addAll(evaluators); 31 updateEvaluators(); 32 } 33 reset()34 @Override protected void reset() { 35 for (Evaluator evaluator : evaluators) { 36 evaluator.reset(); 37 } 38 super.reset(); 39 } 40 cost()41 @Override protected int cost() { 42 return cost; 43 } 44 rightMostEvaluator()45 @Nullable Evaluator rightMostEvaluator() { 46 return num > 0 ? evaluators.get(num - 1) : null; 47 } 48 replaceRightMostEvaluator(Evaluator replacement)49 void replaceRightMostEvaluator(Evaluator replacement) { 50 evaluators.set(num - 1, replacement); 51 updateEvaluators(); 52 } 53 updateEvaluators()54 void updateEvaluators() { 55 // used so we don't need to bash on size() for every match test 56 num = evaluators.size(); 57 58 // sort the evaluators by lowest cost first, to optimize the evaluation order 59 cost = 0; 60 for (Evaluator evaluator : evaluators) { 61 cost += evaluator.cost(); 62 } 63 sortedEvaluators.clear(); 64 sortedEvaluators.addAll(evaluators); 65 Collections.sort(sortedEvaluators, costComparator); 66 } 67 68 private static final Comparator<Evaluator> costComparator = (o1, o2) -> o1.cost() - o2.cost(); 69 // ^ comparingInt, sortedEvaluators.sort not available in targeted version 70 71 public static final class And extends CombiningEvaluator { And(Collection<Evaluator> evaluators)72 And(Collection<Evaluator> evaluators) { 73 super(evaluators); 74 } 75 And(Evaluator... evaluators)76 And(Evaluator... evaluators) { 77 this(Arrays.asList(evaluators)); 78 } 79 80 @Override matches(Element root, Element element)81 public boolean matches(Element root, Element element) { 82 for (int i = 0; i < num; i++) { 83 Evaluator s = sortedEvaluators.get(i); 84 if (!s.matches(root, element)) 85 return false; 86 } 87 return true; 88 } 89 90 @Override toString()91 public String toString() { 92 return StringUtil.join(evaluators, ""); 93 } 94 } 95 96 public static final class Or extends CombiningEvaluator { 97 /** 98 * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR. 99 * @param evaluators initial OR clause (these are wrapped into an AND evaluator). 100 */ Or(Collection<Evaluator> evaluators)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 Or(Evaluator... evaluators)110 Or(Evaluator... evaluators) { this(Arrays.asList(evaluators)); } 111 Or()112 Or() { 113 super(); 114 } 115 add(Evaluator e)116 public void add(Evaluator e) { 117 evaluators.add(e); 118 updateEvaluators(); 119 } 120 121 @Override matches(Element root, Element node)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 toString()132 public String toString() { 133 return StringUtil.join(evaluators, ", "); 134 } 135 } 136 } 137