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.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Utils.*; 021 022import java.lang.reflect.*; 023import java.util.*; 024 025import org.apache.juneau.*; 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 * Matcher that uses the correct matcher based on object type. 108 * Used for objects when we can't determine the object type beforehand. 109 */ 110 private class ColumnMatcher { 111 112 String searchPattern; 113 AbstractMatcher[] matchers; 114 BeanSession bs; 115 116 ColumnMatcher(BeanSession bs, String searchPattern) { 117 this.bs = bs; 118 this.searchPattern = searchPattern; 119 this.matchers = new AbstractMatcher[factories.length]; 120 } 121 122 boolean matches(Object o) { 123 var cm = bs.getClassMetaForObject(o); 124 if (cm == null) 125 return false; 126 if (cm.isCollection()) { 127 for (var o2 : (Collection)o) 128 if (matches(o2)) 129 return true; 130 return false; 131 } 132 if (cm.isArray()) { 133 for (var i = 0; i < Array.getLength(o); i++) 134 if (matches(Array.get(o, i))) 135 return true; 136 return false; 137 } 138 for (var i = 0; i < factories.length; i++) { 139 if (factories[i].canMatch(cm)) { 140 if (matchers[i] == null) 141 matchers[i] = factories[i].create(searchPattern); 142 return matchers[i].matches(cm, o); 143 } 144 } 145 return false; 146 } 147 } 148 149 /* 150 * Matches on a Map only if all specified entry matchers match. 151 */ 152 private class RowMatcher { 153 154 Map<String,ColumnMatcher> entryMatchers = new HashMap<>(); 155 BeanSession bs; 156 157 @SuppressWarnings("unchecked") 158 RowMatcher(BeanSession bs, Map query) { 159 this.bs = bs; 160 query.forEach((k, v) -> entryMatchers.put(s(k), new ColumnMatcher(bs, s(v)))); 161 } 162 163 boolean matches(Object o) { 164 if (o == null) 165 return false; 166 var cm = bs.getClassMetaForObject(o); 167 if (cm.isMapOrBean()) { 168 Map m = cm.isMap() ? (Map)o : bs.toBeanMap(o); 169 for (var e : entryMatchers.entrySet()) { 170 String key = e.getKey(); 171 var val = (Object)null; 172 if (m instanceof BeanMap m2) { 173 val = m2.getRaw(key); 174 } else { 175 val = m.get(key); 176 } 177 if (! e.getValue().matches(val)) 178 return false; 179 } 180 return true; 181 } 182 if (cm.isCollection()) { 183 for (var o2 : (Collection)o) 184 if (! matches(o2)) 185 return false; 186 return true; 187 } 188 if (cm.isArray()) { 189 for (var i = 0; i < Array.getLength(o); i++) 190 if (! matches(Array.get(o, i))) 191 return false; 192 return true; 193 } 194 return false; 195 } 196 } 197 198 /** 199 * Default reusable searcher. 200 */ 201 public static final ObjectSearcher DEFAULT = new ObjectSearcher(); 202 203 /** 204 * Static creator. 205 * 206 * @param factories 207 * The matcher factories to use. 208 * <br>If not specified, uses the following: 209 * <ul> 210 * <li>{@link StringMatcherFactory#DEFAULT} 211 * <li>{@link NumberMatcherFactory#DEFAULT} 212 * <li>{@link TimeMatcherFactory#DEFAULT} 213 * </ul> 214 * @return A new {@link ObjectSearcher} object. 215 */ 216 public static ObjectSearcher create(MatcherFactory...factories) { 217 return new ObjectSearcher(factories); 218 } 219 220 final MatcherFactory[] factories; 221 222 /** 223 * Constructor. 224 * 225 * @param factories 226 * The matcher factories to use. 227 * <br>If not specified, uses the following: 228 * <ul> 229 * <li>{@link NumberMatcherFactory#DEFAULT} 230 * <li>{@link TimeMatcherFactory#DEFAULT} 231 * <li>{@link StringMatcherFactory#DEFAULT} 232 * </ul> 233 */ 234 public ObjectSearcher(MatcherFactory...factories) { 235 this.factories = factories.length == 0 ? a(NumberMatcherFactory.DEFAULT, TimeMatcherFactory.DEFAULT, StringMatcherFactory.DEFAULT) : factories; 236 } 237 238 @Override /* Overridden from ObjectTool */ 239 public Object run(BeanSession session, Object input, SearchArgs args) { 240 241 var type = session.getClassMetaForObject(input); 242 Map<String,String> search = args.getSearch(); 243 244 if (search.isEmpty() || type == null || ! type.isCollectionOrArray()) 245 return input; 246 247 var l = (List<Object>)null; 248 var rowMatcher = new RowMatcher(session, search); 249 250 if (type.isCollection()) { 251 Collection<?> c = (Collection)input; 252 l = listOfSize(c.size()); 253 List<Object> l2 = l; 254 c.forEach(x -> { 255 if (rowMatcher.matches(x)) 256 l2.add(x); 257 }); 258 259 } else /* isArray */ { 260 var size = Array.getLength(input); 261 l = listOfSize(size); 262 for (var i = 0; i < size; i++) { 263 var o = Array.get(input, i); 264 if (rowMatcher.matches(o)) 265 l.add(o); 266 } 267 } 268 269 return l; 270 } 271 272 /** 273 * Convenience method for executing the searcher. 274 * 275 * @param <R> The return type. 276 * @param input The input. 277 * @param searchArgs The search arguments. See {@link SearchArgs} for format. 278 * @return A list of maps/beans matching the 279 */ 280 @SuppressWarnings("unchecked") 281 public <R> List<R> run(Object input, String searchArgs) { 282 Object r = run(BeanContext.DEFAULT_SESSION, input, SearchArgs.create(searchArgs)); 283 if (r instanceof List r2) 284 return r2; 285 if (r instanceof Collection r3) 286 return toList(r3); 287 if (isArray(r)) 288 return l((R[])r); 289 return null; 290 } 291}