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}