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}