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.common.utils.Utils.*; 020 021import java.lang.reflect.*; 022import java.util.*; 023 024import org.apache.juneau.*; 025import org.apache.juneau.common.utils.*; 026 027/** 028 * POJO model searcher. 029 * 030 * <p> 031 * This class is designed to provide searches across arrays and collections of maps or beans. 032 * It allows you to quickly filter beans and maps using simple yet sophisticated search arguments. 033 * </p> 034 * 035 * <h5 class='section'>Example:</h5> 036 * <p class='bjava'> 037 * MyBean[] <jv>arrayOfBeans</jv> = ...; 038 * ObjectSearcher <jv>searcher</jv> = ObjectSearcher.<jsm>create</jsm>(); 039 * 040 * <jc>// Returns a list of beans whose 'foo' property is 'X' and 'bar' property is 'Y'.</jc> 041 * List<MyBean> <jv>result</jv> = <jv>searcher</jv>.run(<jv>arrayOfBeans</jv>, <js>"foo=X,bar=Y"</js>); 042 * </p> 043 * <p> 044 * The tool can be used against the following data types: 045 * </p> 046 * <ul> 047 * <li>Arrays/collections of maps or beans. 048 * </ul> 049 * <p> 050 * The default searcher is configured with the following matcher factories that provides the capabilities of matching 051 * against various data types. This list is extensible: 052 * </p> 053 * <ul class='javatreec'> 054 * <li class='jc'>{@link StringMatcherFactory} 055 * <li class='jc'>{@link NumberMatcherFactory} 056 * <li class='jc'>{@link TimeMatcherFactory} 057 * </ul> 058 * <p> 059 * The {@link StringMatcherFactory} class provides searching based on the following patterns: 060 * </p> 061 * <ul> 062 * <li><js>"property=foo"</js> - Simple full word match 063 * <li><js>"property=fo*"</js>, <js>"property=?ar"</js> - Meta-character matching 064 * <li><js>"property=foo bar"</js>(implicit), <js>"property=^foo ^bar"</js>(explicit) - Multiple OR'ed patterns 065 * <li><js>"property=+fo* +*ar"</js> - Multiple AND'ed patterns 066 * <li><js>"property=fo* -bar"</js> - Negative patterns 067 * <li><js>"property='foo bar'"</js> - Patterns with whitespace 068 * <li><js>"property=foo\\'bar"</js> - Patterns with single-quotes 069 * <li><js>"property=/foo\\s+bar"</js> - Regular expression match 070 * </ul> 071 * <p> 072 * The {@link NumberMatcherFactory} class provides searching based on the following patterns: 073 * </p> 074 * <ul> 075 * <li><js>"property=1"</js> - A single number 076 * <li><js>"property=1 2"</js> - Multiple OR'ed numbers 077 * <li><js>"property=-1 -2"</js> - Multiple OR'ed negative numbers 078 * <li><js>"property=1-2"</js>,<js>"property=-2--1"</js> - A range of numbers (whitespace ignored) 079 * <li><js>"property=1-2 4-5"</js> - Multiple OR'ed ranges 080 * <li><js>"property=<1"</js>,<js>"property=<=1"</js>,<js>"property=>1"</js>,<js>"property=>=1"</js> - Open-ended ranges 081 * <li><js>"property=!1"</js>,<js>"property=!1-2"</js> - Negation 082 * </ul> 083 * <p> 084 * The {@link TimeMatcherFactory} class provides searching based on the following patterns: 085 * </p> 086 * <ul> 087 * <li><js>"property=2011"</js> - A single year 088 * <li><js>"property=2011 2013 2015"</js> - Multiple years 089 * <li><js>"property=2011-01"</js> - A single month 090 * <li><js>"property=2011-01-01"</js> - A single day 091 * <li><js>"property=2011-01-01T12"</js> - A single hour 092 * <li><js>"property=2011-01-01T12:30"</js> - A single minute 093 * <li><js>"property=2011-01-01T12:30:45"</js> - A single second 094 * <li><js>"property=>2011"</js>,<js>"property=>=2011"</js>,<js>"property=<2011"</js>,<js>"property=<=2011"</js> - Open-ended ranges 095 * <li><js>"property=>2011"</js>,<js>"property=>=2011"</js>,<js>"property=<2011"</js>,<js>"property=<=2011"</js> - Open-ended ranges 096 * <li><js>"property=2011 - 2013-06-30"</js> - Closed ranges 097 * </ul> 098 * 099 * <h5 class='section'>See Also:</h5><ul> 100 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ObjectTools">Object Tools</a> 101 102 * </ul> 103 */ 104@SuppressWarnings({"rawtypes"}) 105public class ObjectSearcher implements ObjectTool<SearchArgs> { 106 107 //----------------------------------------------------------------------------------------------------------------- 108 // Static 109 //----------------------------------------------------------------------------------------------------------------- 110 111 /** 112 * Default reusable searcher. 113 */ 114 public static final ObjectSearcher DEFAULT = new ObjectSearcher(); 115 116 /** 117 * Static creator. 118 * 119 * @param factories 120 * The matcher factories to use. 121 * <br>If not specified, uses the following: 122 * <ul> 123 * <li>{@link StringMatcherFactory#DEFAULT} 124 * <li>{@link NumberMatcherFactory#DEFAULT} 125 * <li>{@link TimeMatcherFactory#DEFAULT} 126 * </ul> 127 * @return A new {@link ObjectSearcher} object. 128 */ 129 public static ObjectSearcher create(MatcherFactory...factories) { 130 return new ObjectSearcher(factories); 131 } 132 133 //----------------------------------------------------------------------------------------------------------------- 134 // Instance 135 //----------------------------------------------------------------------------------------------------------------- 136 137 final MatcherFactory[] factories; 138 139 /** 140 * Constructor. 141 * 142 * @param factories 143 * The matcher factories to use. 144 * <br>If not specified, uses the following: 145 * <ul> 146 * <li>{@link NumberMatcherFactory#DEFAULT} 147 * <li>{@link TimeMatcherFactory#DEFAULT} 148 * <li>{@link StringMatcherFactory#DEFAULT} 149 * </ul> 150 */ 151 public ObjectSearcher(MatcherFactory...factories) { 152 this.factories = factories.length == 0 ? new MatcherFactory[]{NumberMatcherFactory.DEFAULT, TimeMatcherFactory.DEFAULT, StringMatcherFactory.DEFAULT} : factories; 153 } 154 155 /** 156 * Convenience method for executing the searcher. 157 * 158 * @param <R> The return type. 159 * @param input The input. 160 * @param searchArgs The search arguments. See {@link SearchArgs} for format. 161 * @return A list of maps/beans matching the 162 */ 163 @SuppressWarnings("unchecked") 164 public <R> List<R> run(Object input, String searchArgs) { 165 Object r = run(BeanContext.DEFAULT_SESSION, input, SearchArgs.create(searchArgs)); 166 if (r instanceof List) 167 return (List<R>)r; 168 if (r instanceof Collection) 169 return new ArrayList<R>((Collection)r); 170 if (isArray(r)) 171 return Arrays.asList((R[])r); 172 return null; 173 } 174 175 @Override /* ObjectTool */ 176 public Object run(BeanSession session, Object input, SearchArgs args) { 177 178 ClassMeta<?> type = session.getClassMetaForObject(input); 179 Map<String,String> search = args.getSearch(); 180 181 if (search.isEmpty() || type == null || ! type.isCollectionOrArray()) 182 return input; 183 184 List<Object> l = null; 185 RowMatcher rowMatcher = new RowMatcher(session, search); 186 187 if (type.isCollection()) { 188 Collection<?> c = (Collection)input; 189 l = Utils.listOfSize(c.size()); 190 List<Object> l2 = l; 191 c.forEach(x -> { 192 if (rowMatcher.matches(x)) 193 l2.add(x); 194 }); 195 196 } else /* isArray */ { 197 int size = Array.getLength(input); 198 l = Utils.listOfSize(size); 199 for (int i = 0; i < size; i++) { 200 Object o = Array.get(input, i); 201 if (rowMatcher.matches(o)) 202 l.add(o); 203 } 204 } 205 206 return l; 207 } 208 209 //==================================================================================================== 210 // MapMatcher 211 //==================================================================================================== 212 /* 213 * Matches on a Map only if all specified entry matchers match. 214 */ 215 private class RowMatcher { 216 217 Map<String,ColumnMatcher> entryMatchers = new HashMap<>(); 218 BeanSession bs; 219 220 @SuppressWarnings("unchecked") 221 RowMatcher(BeanSession bs, Map query) { 222 this.bs = bs; 223 query.forEach((k,v) -> entryMatchers.put(Utils.s(k), new ColumnMatcher(bs, Utils.s(v)))); 224 } 225 226 boolean matches(Object o) { 227 if (o == null) 228 return false; 229 ClassMeta<?> cm = bs.getClassMetaForObject(o); 230 if (cm.isMapOrBean()) { 231 Map m = cm.isMap() ? (Map)o : bs.toBeanMap(o); 232 for (Map.Entry<String,ColumnMatcher> e : entryMatchers.entrySet()) { 233 String key = e.getKey(); 234 Object val = null; 235 if (m instanceof BeanMap) { 236 val = ((BeanMap)m).getRaw(key); 237 } else { 238 val = m.get(key); 239 } 240 if (! e.getValue().matches(val)) 241 return false; 242 } 243 return true; 244 } 245 if (cm.isCollection()) { 246 for (Object o2 : (Collection)o) 247 if (! matches(o2)) 248 return false; 249 return true; 250 } 251 if (cm.isArray()) { 252 for (int i = 0; i < Array.getLength(o); i++) 253 if (! matches(Array.get(o, i))) 254 return false; 255 return true; 256 } 257 return false; 258 } 259 } 260 261 //==================================================================================================== 262 // ObjectMatcher 263 //==================================================================================================== 264 /* 265 * Matcher that uses the correct matcher based on object type. 266 * Used for objects when we can't determine the object type beforehand. 267 */ 268 private class ColumnMatcher { 269 270 String searchPattern; 271 AbstractMatcher[] matchers; 272 BeanSession bs; 273 274 ColumnMatcher(BeanSession bs, String searchPattern) { 275 this.bs = bs; 276 this.searchPattern = searchPattern; 277 this.matchers = new AbstractMatcher[factories.length]; 278 } 279 280 boolean matches(Object o) { 281 ClassMeta<?> cm = bs.getClassMetaForObject(o); 282 if (cm == null) 283 return false; 284 if (cm.isCollection()) { 285 for (Object o2 : (Collection)o) 286 if (matches(o2)) 287 return true; 288 return false; 289 } 290 if (cm.isArray()) { 291 for (int i = 0; i < Array.getLength(o); i++) 292 if (matches(Array.get(o, i))) 293 return true; 294 return false; 295 } 296 for (int i = 0; i < factories.length; i++) { 297 if (factories[i].canMatch(cm)) { 298 if (matchers[i] == null) 299 matchers[i] = factories[i].create(searchPattern); 300 return matchers[i].matches(cm, o); 301 } 302 } 303 return false; 304 } 305 } 306}