001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.utils; 018 019import static org.apache.juneau.commons.lang.StateEnum.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022import static org.apache.juneau.commons.utils.StringUtils.*; 023 024import java.text.*; 025import java.util.*; 026import java.util.regex.*; 027 028import org.apache.juneau.commons.lang.*; 029 030/** 031 * Utility class for matching strings against string expressions. 032 * 033 * <p> 034 * Supports the following string constructs: 035 * <ul> 036 * <li><js>"foo"</js> - Single arguments. 037 * <li><js>"foo,bar,baz"</js> - Multiple OR'ed arguments. 038 * <li><js>"foo | bar | bqz"</js> - Multiple OR'ed arguments, pipe syntax. 039 * <li><js>"foo || bar || bqz"</js> - Multiple OR'ed arguments, Java-OR syntax. 040 * <li><js>"fo*"</js> - Patterns including <js>'*'</js> and <js>'?'</js>. 041 * <li><js>"fo* & *oo"</js> - Multiple AND'ed arguments, ampersand syntax. 042 * <li><js>"fo* && *oo"</js> - Multiple AND'ed arguments, Java-AND syntax. 043 * <li><js>"fo* || (*oo || bar)"</js> - Parenthesis. 044 * </ul> 045 * 046 * <h5 class='section'>Notes:</h5><ul> 047 * <li class='note'>AND operations take precedence over OR operations (as expected). 048 * <li class='note'>Whitespace is ignored. 049 * <li class='note'><jk>null</jk> or empty expressions always match as <jk>false</jk>. 050 * </ul> 051 * 052 */ 053public class StringExpressionMatcher { 054 055 static class And extends Exp { 056 Exp[] clauses; 057 058 And(List<Exp> clauses) { 059 this.clauses = clauses.toArray(new Exp[clauses.size()]); 060 } 061 062 @Override /* Overridden from Object */ 063 public String toString() { 064 return "(& " + join(clauses, " ") + ')'; 065 } 066 067 @Override /* Overridden from Exp */ 068 void appendTokens(Set<String> set) { 069 for (var clause : clauses) 070 clause.appendTokens(set); 071 } 072 073 @Override /* Overridden from Exp */ 074 boolean matches(String input) { 075 for (var e : clauses) 076 if (! e.matches(input)) 077 return false; 078 return true; 079 } 080 } 081 082 static class Eq extends Exp { 083 final String operand; 084 085 Eq(String operand) { 086 this.operand = operand; 087 } 088 089 @Override /* Overridden from Object */ 090 public String toString() { 091 return "[= " + operand + "]"; 092 } 093 094 @Override /* Overridden from Exp */ 095 void appendTokens(Set<String> set) { 096 set.add(operand); 097 } 098 099 @Override /* Overridden from Exp */ 100 boolean matches(String input) { 101 return eq(operand, input); 102 } 103 } 104 105 abstract static class Exp { 106 @SuppressWarnings({ 107 "java:S1172", // set unused in base no-op; subclasses that override do use it 108 "unused" // set unused in base no-op; subclasses that override do use it 109 }) 110 void appendTokens(Set<String> set) {} 111 112 abstract boolean matches(String input); 113 } 114 115 static class Match extends Exp { 116 final Pattern p; 117 final String operand; 118 119 Match(String operand) { 120 this.operand = operand; 121 p = getMatchPattern(operand); 122 } 123 124 @Override /* Overridden from Object */ 125 public String toString() { 126 return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]"; 127 } 128 129 @Override /* Overridden from Exp */ 130 void appendTokens(Set<String> set) { 131 set.add(operand); 132 } 133 134 @Override /* Overridden from Exp */ 135 boolean matches(String input) { 136 return p.matcher(input).matches(); 137 } 138 } 139 140 static class Never extends Exp { 141 @Override /* Overridden from Object */ 142 public String toString() { 143 return "(NEVER)"; 144 } 145 146 @Override 147 boolean matches(String input) { 148 return false; 149 } 150 } 151 152 static class Or extends Exp { 153 Exp[] clauses; 154 155 Or(List<Exp> clauses) { 156 this.clauses = clauses.toArray(new Exp[clauses.size()]); 157 } 158 159 @Override /* Overridden from Object */ 160 public String toString() { 161 return "(| " + join(clauses, " ") + ')'; 162 } 163 164 @Override /* Overridden from Exp */ 165 void appendTokens(Set<String> set) { 166 for (var clause : clauses) 167 clause.appendTokens(set); 168 } 169 170 @Override 171 boolean matches(String input) { 172 for (var e : clauses) 173 if (e.matches(input)) 174 return true; 175 return false; 176 } 177 } 178 179 // @formatter:off 180 private static final AsciiSet WS = AsciiSet.of(" \t"); 181 private static final AsciiSet OP = AsciiSet.of(",|&"); 182 private static final AsciiSet META = AsciiSet.of("*?"); 183 // @formatter:on 184 185 private static Exp parseOperand(String operand) { 186 boolean hasMeta = false; 187 for (var i = 0; i < operand.length() && ! hasMeta; i++) { 188 var c = operand.charAt(i); 189 hasMeta |= META.contains(c); 190 } 191 return hasMeta ? new Match(operand) : new Eq(operand); 192 } 193 194 private final Exp exp; 195 196 /** 197 * Constructor. 198 * 199 * @param expression The string expression. Can be <jk>null</jk> or empty, in which case the matcher never matches. 200 * @throws ParseException Malformed input encountered. 201 */ 202 public StringExpressionMatcher(String expression) throws ParseException { 203 this.exp = parse(expression); 204 } 205 206 /** 207 * Returns all the tokens used in this expression. 208 * 209 * @return All the tokens used in this expression. 210 */ 211 public Set<String> getTokens() { 212 var set = new TreeSet<String>(); 213 exp.appendTokens(set); 214 return set; 215 } 216 217 /** 218 * Returns <jk>true</jk> if the specified string matches this expression. 219 * 220 * @param input The input string. 221 * @return 222 * <jk>true</jk> if the specified string matches this expression. 223 * <br>Always <jk>false</jk> if the string is <jk>null</jk>. 224 */ 225 public boolean matches(String input) { 226 return nn(input) && exp.matches(input); 227 } 228 229 @Override /* Overridden from Object */ 230 public String toString() { 231 return exp.toString(); 232 } 233 234 @SuppressWarnings({ 235 "java:S125", // Inline /*...*/ documents implicit else condition 236 "java:S135", // Multiple break statements necessary for state machine error handling 237 "java:S3776" // Cognitive complexity acceptable for parser state machine 238 }) 239 private Exp parse(String expression) throws ParseException { 240 if (ib(expression)) 241 return new Never(); 242 243 expression = expression.trim(); 244 245 List<Exp> ors = list(); 246 List<Exp> ands = list(); 247 248 StateEnum state = S1; 249 int i = 0; 250 int mark = -1; 251 int pDepth = 0; 252 boolean error = false; 253 254 for (i = 0; i < expression.length(); i++) { 255 var c = expression.charAt(i); 256 if (state == S1) { 257 // S1 = Looking for start 258 if (! WS.contains(c)) { 259 if (c == '(') { 260 state = S2; 261 pDepth = 0; 262 mark = i + 1; 263 } else if (OP.contains(c)) { 264 error = true; 265 break; 266 } else { 267 state = S3; 268 mark = i; 269 } 270 } 271 } else if (state == S2) { 272 // S2 = Found [(], looking for [)]. 273 if (c == '(') 274 pDepth++; 275 if (c == ')') { 276 if (pDepth > 0) 277 pDepth--; 278 else { 279 ands.add(parse(expression.substring(mark, i))); 280 mark = -1; 281 state = S4; 282 } 283 } 284 } else if (state == S3) { 285 // S3 = Found [A], looking for end of A. 286 if (WS.contains(c) || OP.contains(c)) { 287 ands.add(parseOperand(expression.substring(mark, i))); 288 mark = -1; 289 if (WS.contains(c)) { 290 state = S4; 291 } else { 292 i--; 293 state = S5; 294 } 295 } 296 } else if (state == S4) { 297 // S4 = Found [A ], looking for & or | or ,. 298 if (! WS.contains(c)) { 299 if (OP.contains(c)) { 300 i--; 301 state = S5; 302 } else { 303 error = true; 304 break; 305 } 306 } 307 } else if (state == S5) { // NOSONAR - State check necessary for state machine 308 // S5 = Found & or | or , 309 if (c == '&') { 310 state = S6; 311 } else /* (c == '|' || c == ',') */ { 312 if (ands.size() == 1) { 313 ors.add(ands.get(0)); 314 } else { 315 ors.add(new And(ands)); 316 } 317 ands.clear(); 318 if (c == '|') { 319 state = S7; 320 } else { 321 state = S1; 322 } 323 } 324 } else if (state == S6) { 325 // S6 = Found &, looking for & or other 326 if (! WS.contains(c)) { 327 if (c != '&') 328 i--; 329 state = S1; 330 } 331 } else /* (state == S7) */ { 332 // S7 = Found |, looking for | or other 333 if (! WS.contains(c)) { 334 if (c != '|') 335 i--; 336 state = S1; 337 } 338 } 339 } 340 341 if (error) 342 throw new ParseException("Invalid character in expression '" + expression + "' at position " + i + ". state=" + state, i); 343 344 if (state == S1) 345 throw new ParseException("Could not find beginning of clause in '" + expression + "'", i); 346 if (state == S2) 347 throw new ParseException("Could not find matching parenthesis in expression '" + expression + "'", i); 348 if (state == S5 || state == S6 || state == S7) 349 throw new ParseException("Dangling clause in expression '" + expression + "'", i); 350 351 if (mark != -1) 352 ands.add(parseOperand(expression.substring(mark, expression.length()))); 353 if (ands.size() == 1) 354 ors.add(ands.get(0)); 355 else 356 ors.add(new And(ands)); 357 358 if (ors.size() == 1) 359 return ors.get(0); 360 return new Or(ors); 361 } 362}