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.objecttools;
018
019import static org.apache.juneau.commons.lang.StateEnum.*;
020
021import java.util.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.commons.lang.*;
025
026/**
027 * Number matcher factory for the {@link ObjectSearcher} class.
028 *
029 * <p>
030 *    The class provides searching based on the following patterns:
031 * </p>
032 * <ul>
033 *    <li><js>"property=1"</js> - A single number
034 *    <li><js>"property=1 2"</js> - Multiple OR'ed numbers
035 *    <li><js>"property=-1 -2"</js> - Multiple OR'ed negative numbers
036 *    <li><js>"property=1-2"</js>,<js>"property=-2--1"</js>  - A range of numbers (whitespace ignored)
037 *    <li><js>"property=1-2 4-5"</js> - Multiple OR'ed ranges
038 *    <li><js>"property=&lt;1"</js>,<js>"property=&lt;=1"</js>,<js>"property=&gt;1"</js>,<js>"property=&gt;=1"</js> - Open-ended ranges
039 *    <li><js>"property=!1"</js>,<js>"property=!1-2"</js> - Negation
040 * </ul>
041 *
042 * <h5 class='section'>See Also:</h5><ul>
043 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ObjectTools">Object Tools</a>
044 * </ul>
045 */
046public class NumberMatcherFactory extends MatcherFactory {
047
048   /**
049    * A construct representing a single search pattern.
050    */
051   private static class NumberMatcher extends AbstractMatcher {
052      // @formatter:off
053      private static final AsciiSet
054         SNUM = AsciiSet.of("-0123456789."),
055         NUM = AsciiSet.of("0123456789."),
056         WS = AsciiSet.of(" \t");
057      // @formatter:on
058      NumberRange[] numberRanges;
059      String pattern;
060
061      public NumberMatcher(String s) {
062
063         s = s.trim();
064         pattern = s;
065
066         var l = new LinkedList<NumberRange>();
067
068         // Possible patterns:
069         // 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456, !123, !123-456, 123 - 456 (one token), 123 -456 (two separate tokens)
070
071         // Possible states:
072         // S1 = Looking for start (WS=S1, [!]=S1, [>]=S2, [<]=S3, SNUM=S6)
073         // S2 = Found [>], looking for [=]/SNUM ([=]=S4, WS=S5, SNUM=S6)
074         // S3 = Found [<], looking for [=]/SNUM ([=]=S5, WS=S5, SNUM=S6)
075         // S4 = Found [=], looking for SNUN (WS=S5, SNUM=S6)
076         // S5 = Found [... ], looking for SNUM (SNUM=S6)
077         // S6 = Found [1], looking for [-]/WS (WS=S7, [-]=S8)
078         // S7 = Found [123 ], looking for [-]/SNUM (if -, could be 123 - 456 or 123 -456) ([-]=S9, SNUM=S7)
079         // S8 = Found [123-], looking for SNUM (Could be 123- 456 or 123-456) (SNUM=S11)
080         // S9 = Found [123 -], looking for WS/SNUM (If WS, then it's 123 - 456, otherwise 123 -456) (WS=S10, SNUM=S6)
081         // S10 = Found [123 - ], looking for SNUM (SNUM=S12)
082         // S11 = Found [123 - 4], looking for WS (WS=S1)
083
084         StateEnum state = S1;
085         var mark = 0;
086         var isNot = false;
087         var eq = Equality.NONE;
088         var n1 = (Integer)null;
089         var n2 = (Integer)null;
090
091         int i;
092         for (i = 0; i < s.length(); i++) {
093            var c = s.charAt(i);
094            if (state == S1) {
095               if (c == '!') {
096                  isNot = true;
097               } else if (WS.contains(c)) {
098                  state = S1;
099               } else if (c == '>') {
100                  state = S2;
101                  eq = Equality.GT;
102               } else if (c == '<') {
103                  state = S3;
104                  eq = Equality.LT;
105               } else if (SNUM.contains(c)) {
106                  state = S6;
107                  mark = i;
108               } else {
109                  break;
110               }
111            } else if (state == S2) {
112               if (c == '=') {
113                  state = S4;
114                  eq = Equality.GTE;
115               } else if (WS.contains(c)) {
116                  state = S5;
117               } else if (SNUM.contains(c)) {
118                  state = S6;
119                  mark = i;
120               } else {
121                  break;
122               }
123            } else if (state == S3) {
124               if (c == '=') {
125                  state = S4;
126                  eq = Equality.LTE;
127               } else if (WS.contains(c)) {
128                  state = S5;
129               } else if (SNUM.contains(c)) {
130                  state = S6;
131                  mark = i;
132               } else {
133                  break;
134               }
135            } else if (state == S4) {
136               if (WS.contains(c)) {
137                  state = S5;
138               } else if (SNUM.contains(c)) {
139                  mark = i;
140                  state = S6;
141               } else {
142                  break;
143               }
144            } else if (state == S5) {
145               if (WS.contains(c)) {
146                  state = S5;
147               } else if (SNUM.contains(c)) {
148                  state = S6;
149                  mark = i;
150               } else {
151                  break;
152               }
153            } else if (state == S6) {
154               if (NUM.contains(c)) {
155                  state = S6;
156               } else if (WS.contains(c)) {
157                  state = S7;
158                  n1 = Integer.parseInt(s.substring(mark, i));
159               } else if (c == '-') {
160                  state = S8;
161                  n1 = Integer.parseInt(s.substring(mark, i));
162               } else {
163                  break;
164               }
165            } else if (state == S7) {
166               if (WS.contains(c)) {
167                  state = S7;
168               } else if (c == '-') {
169                  state = S9;
170               } else if (SNUM.contains(c)) {
171                  state = S6;
172                  l.add(new NumberRange(eq, n1, isNot));
173                  eq = Equality.NONE;
174                  n1 = null;
175                  isNot = false;
176                  mark = i;
177               } else {
178                  break;
179               }
180            } else if (state == S8) {
181               if (WS.contains(c)) {
182                  state = S8;
183               } else if (SNUM.contains(c)) {
184                  state = S11;
185                  mark = i;
186               } else {
187                  break;
188               }
189            } else if (state == S9) {
190               if (WS.contains(c)) {
191                  state = S10;
192               } else if (NUM.contains(c)) {
193                  state = S6;
194                  l.add(new NumberRange(eq, n1, isNot));
195                  eq = Equality.NONE;
196                  n1 = null;
197                  isNot = false;
198                  mark = i - 1;
199               } else {
200                  break;
201               }
202            } else if (state == S10) {
203               if (WS.contains(c)) {
204                  state = S10;
205               } else if (SNUM.contains(c)) {
206                  state = S11;
207                  mark = i;
208               } else {
209                  break;
210               }
211            } else /* (state == S11) */ {
212               if (SNUM.contains(c)) {
213                  state = S11;
214               } else if (WS.contains(c)) {
215                  state = S1;
216                  n2 = Integer.parseInt(s.substring(mark, i));
217                  l.add(new NumberRange(eq, n1, n2, isNot));
218                  eq = Equality.NONE;
219                  n1 = n2 = null;
220                  isNot = false;
221               } else {
222                  break;
223               }
224            }
225         }
226
227         if (i != s.length())
228            throw new PatternException("Invalid range pattern ({0}): {1}", state, s);
229
230         if (state == S1) {
231            // No tokens found.
232         } else if (state == S2 || state == S3 || state == S4 || state == S8 || state == S9) {
233            throw new PatternException("Invalid range pattern (E{0}): {1}", state, s);
234         } else if (state == S6) {
235            n1 = Integer.parseInt(s.substring(mark).trim());
236            l.add(new NumberRange(eq, n1, isNot));
237         } else /* (state == S11) */ {
238            n2 = Integer.parseInt(s.substring(mark).trim());
239            l.add(new NumberRange(eq, n1, n2, isNot));
240         }
241
242         numberRanges = l.toArray(new NumberRange[l.size()]);
243      }
244
245      @Override /* Overridden from Matcher */
246      public boolean matches(ClassMeta<?> cm, Object o) {
247         var n = (Number)o;
248         if (numberRanges.length == 0)
249            return true;
250         for (var numberRange : numberRanges)
251            if (numberRange.matches(n))
252               return true;
253         return false;
254      }
255
256      @Override /* Overridden from Object */
257      public String toString() {
258         return pattern;
259      }
260   }
261
262   /**
263    * A construct representing a single search range in a single search pattern.
264    * All possible forms of search patterns are boiled down to these number ranges.
265    */
266   private static class NumberRange {
267      int start;
268      int end;
269      boolean isNot;
270
271      public NumberRange(Equality eq, Integer num, boolean isNot) {
272         this(eq, num, null, isNot);
273      }
274
275      public NumberRange(Equality eq, Integer start, Integer end, boolean isNot) {
276         this.isNot = isNot;
277
278         // 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456
279         if (eq == Equality.NONE && end == null) { // 123
280            this.start = start;
281            this.end = this.start;
282         } else if (eq == Equality.GT) {
283            this.start = start + 1;
284            this.end = Integer.MAX_VALUE;
285         } else if (eq == Equality.GTE) {
286            this.start = start;
287            this.end = Integer.MAX_VALUE;
288         } else if (eq == Equality.LT) {
289            this.start = Integer.MIN_VALUE;
290            this.end = start - 1;
291         } else if (eq == Equality.LTE) {
292            this.start = Integer.MIN_VALUE;
293            this.end = start;
294         } else {
295            this.start = start;
296            this.end = end;
297         }
298      }
299
300      public boolean matches(Number n) {
301         var i = n.longValue();
302         var b = (i >= start && i <= end);
303         if (isNot)
304            b = ! b;
305         return b;
306      }
307   }
308
309   /**
310    * Default reusable matcher.
311    */
312   public static final NumberMatcherFactory DEFAULT = new NumberMatcherFactory();
313
314   @Override
315   public boolean canMatch(ClassMeta<?> cm) {
316      return cm.isNumber();
317   }
318
319   @Override
320   public AbstractMatcher create(String pattern) {
321      return new NumberMatcher(pattern);
322   }
323}