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&lt;MyBean&gt; <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=&lt;1"</js>,<js>"property=&lt;=1"</js>,<js>"property=&gt;1"</js>,<js>"property=&gt;=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=&gt;2011"</js>,<js>"property=&gt;=2011"</js>,<js>"property=&lt;2011"</js>,<js>"property=&lt;=2011"</js> - Open-ended ranges
095 *    <li><js>"property=&gt;2011"</js>,<js>"property=&gt;=2011"</js>,<js>"property=&lt;2011"</js>,<js>"property=&lt;=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}