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}