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&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   //-----------------------------------------------------------------------------------------------------------------
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}