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=<1"</js>,<js>"property=<=1"</js>,<js>"property=>1"</js>,<js>"property=>=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}