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