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