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}