All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juneau.objecttools.ObjectSearcher Maven / Gradle / Ivy

// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.objecttools;

import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;

import java.lang.reflect.*;
import java.util.*;

import org.apache.juneau.*;

/**
 * POJO model searcher.
 *
 * 

* This class is designed to provide searches across arrays and collections of maps or beans. * It allows you to quickly filter beans and maps using simple yet sophisticated search arguments. *

* *
Example:
*

* MyBean[] arrayOfBeans = ...; * ObjectSearcher searcher = ObjectSearcher.create(); * * // Returns a list of beans whose 'foo' property is 'X' and 'bar' property is 'Y'. * List<MyBean> result = searcher.run(arrayOfBeans, "foo=X,bar=Y"); *

*

* The tool can be used against the following data types: *

*
    *
  • Arrays/collections of maps or beans. *
*

* The default searcher is configured with the following matcher factories that provides the capabilities of matching * against various data types. This list is extensible: *

*
    *
  • {@link StringMatcherFactory} *
  • {@link NumberMatcherFactory} *
  • {@link TimeMatcherFactory} *
*

* The {@link StringMatcherFactory} class provides searching based on the following patterns: *

*
    *
  • "property=foo" - Simple full word match *
  • "property=fo*", "property=?ar" - Meta-character matching *
  • "property=foo bar"(implicit), "property=^foo ^bar"(explicit) - Multiple OR'ed patterns *
  • "property=+fo* +*ar" - Multiple AND'ed patterns *
  • "property=fo* -bar" - Negative patterns *
  • "property='foo bar'" - Patterns with whitespace *
  • "property=foo\\'bar" - Patterns with single-quotes *
  • "property=/foo\\s+bar" - Regular expression match *
*

* The {@link NumberMatcherFactory} class provides searching based on the following patterns: *

*
    *
  • "property=1" - A single number *
  • "property=1 2" - Multiple OR'ed numbers *
  • "property=-1 -2" - Multiple OR'ed negative numbers *
  • "property=1-2","property=-2--1" - A range of numbers (whitespace ignored) *
  • "property=1-2 4-5" - Multiple OR'ed ranges *
  • "property=<1","property=<=1","property=>1","property=>=1" - Open-ended ranges *
  • "property=!1","property=!1-2" - Negation *
*

* The {@link TimeMatcherFactory} class provides searching based on the following patterns: *

*
    *
  • "property=2011" - A single year *
  • "property=2011 2013 2015" - Multiple years *
  • "property=2011-01" - A single month *
  • "property=2011-01-01" - A single day *
  • "property=2011-01-01T12" - A single hour *
  • "property=2011-01-01T12:30" - A single minute *
  • "property=2011-01-01T12:30:45" - A single second *
  • "property=>2011","property=>=2011","property=<2011","property=<=2011" - Open-ended ranges *
  • "property=>2011","property=>=2011","property=<2011","property=<=2011" - Open-ended ranges *
  • "property=2011 - 2013-06-30" - Closed ranges *
* *
See Also:
*/ @SuppressWarnings({"rawtypes"}) public final class ObjectSearcher implements ObjectTool { //----------------------------------------------------------------------------------------------------------------- // Static //----------------------------------------------------------------------------------------------------------------- /** * Default reusable searcher. */ public static final ObjectSearcher DEFAULT = new ObjectSearcher(); /** * Static creator. * * @param factories * The matcher factories to use. *
If not specified, uses the following: *
    *
  • {@link StringMatcherFactory#DEFAULT} *
  • {@link NumberMatcherFactory#DEFAULT} *
  • {@link TimeMatcherFactory#DEFAULT} *
* @return A new {@link ObjectSearcher} object. */ public static ObjectSearcher create(MatcherFactory...factories) { return new ObjectSearcher(factories); } //----------------------------------------------------------------------------------------------------------------- // Instance //----------------------------------------------------------------------------------------------------------------- final MatcherFactory[] factories; /** * Constructor. * * @param factories * The matcher factories to use. *
If not specified, uses the following: *
    *
  • {@link NumberMatcherFactory#DEFAULT} *
  • {@link TimeMatcherFactory#DEFAULT} *
  • {@link StringMatcherFactory#DEFAULT} *
*/ public ObjectSearcher(MatcherFactory...factories) { this.factories = factories.length == 0 ? new MatcherFactory[]{NumberMatcherFactory.DEFAULT, TimeMatcherFactory.DEFAULT, StringMatcherFactory.DEFAULT} : factories; } /** * Convenience method for executing the searcher. * * @param The return type. * @param input The input. * @param searchArgs The search arguments. See {@link SearchArgs} for format. * @return A list of maps/beans matching the */ @SuppressWarnings("unchecked") public List run(Object input, String searchArgs) { Object r = run(BeanContext.DEFAULT_SESSION, input, SearchArgs.create(searchArgs)); if (r instanceof List) return (List)r; if (r instanceof Collection) return new ArrayList((Collection)r); if (r.getClass().isArray()) return Arrays.asList((R[])r); return null; } @Override /* ObjectTool */ public Object run(BeanSession session, Object input, SearchArgs args) { ClassMeta type = session.getClassMetaForObject(input); Map search = args.getSearch(); if (search.isEmpty() || type == null || ! type.isCollectionOrArray()) return input; List l = null; RowMatcher rowMatcher = new RowMatcher(session, search); if (type.isCollection()) { Collection c = (Collection)input; l = list(c.size()); List l2 = l; c.forEach(x -> { if (rowMatcher.matches(x)) l2.add(x); }); } else /* isArray */ { int size = Array.getLength(input); l = list(size); for (int i = 0; i < size; i++) { Object o = Array.get(input, i); if (rowMatcher.matches(o)) l.add(o); } } return l; } //==================================================================================================== // MapMatcher //==================================================================================================== /* * Matches on a Map only if all specified entry matchers match. */ private class RowMatcher { Map entryMatchers = new HashMap<>(); BeanSession bs; @SuppressWarnings("unchecked") RowMatcher(BeanSession bs, Map query) { this.bs = bs; query.forEach((k,v) -> entryMatchers.put(stringify(k), new ColumnMatcher(bs, stringify(v)))); } boolean matches(Object o) { if (o == null) return false; ClassMeta cm = bs.getClassMetaForObject(o); if (cm.isMapOrBean()) { Map m = cm.isMap() ? (Map)o : bs.toBeanMap(o); for (Map.Entry e : entryMatchers.entrySet()) { String key = e.getKey(); Object val = null; if (m instanceof BeanMap) { val = ((BeanMap)m).getRaw(key); } else { val = m.get(key); } if (! e.getValue().matches(val)) return false; } return true; } if (cm.isCollection()) { for (Object o2 : (Collection)o) if (! matches(o2)) return false; return true; } if (cm.isArray()) { for (int i = 0; i < Array.getLength(o); i++) if (! matches(Array.get(o, i))) return false; return true; } return false; } } //==================================================================================================== // ObjectMatcher //==================================================================================================== /* * Matcher that uses the correct matcher based on object type. * Used for objects when we can't determine the object type beforehand. */ private class ColumnMatcher { String searchPattern; AbstractMatcher[] matchers; BeanSession bs; ColumnMatcher(BeanSession bs, String searchPattern) { this.bs = bs; this.searchPattern = searchPattern; this.matchers = new AbstractMatcher[factories.length]; } boolean matches(Object o) { ClassMeta cm = bs.getClassMetaForObject(o); if (cm == null) return false; if (cm.isCollection()) { for (Object o2 : (Collection)o) if (matches(o2)) return true; return false; } if (cm.isArray()) { for (int i = 0; i < Array.getLength(o); i++) if (matches(Array.get(o, i))) return true; return false; } for (int i = 0; i < factories.length; i++) { if (factories[i].canMatch(cm)) { if (matchers[i] == null) matchers[i] = factories[i].create(searchPattern); return matchers[i].matches(cm, o); } } return false; } } }