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