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* &amp; *oo"</js> - Multiple AND'ed arguments, ampersand syntax.
042 *    <li><js>"fo* &amp;&amp; *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}