http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/MessageBundle.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/utils/MessageBundle.java 
b/juneau-core/src/main/java/org/apache/juneau/utils/MessageBundle.java
new file mode 100644
index 0000000..0d1ce75
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/MessageBundle.java
@@ -0,0 +1,310 @@
+/***************************************************************************************************************************
+ * 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.utils;
+
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Wraps a {@link ResourceBundle} to provide some useful additional 
functionality.
+ * <ul class='spaced-list'>
+ *     <li>Instead of throwing {@link MissingResourceException}, the {@link 
#getString(String)} method
+ *             will return <js>"{!!key}"</js> if the bundle was not found, and 
<js>"{!key}"</js> if bundle
+ *             was found but the key is not in the bundle.
+ *     <li>A client locale can be set as a {@link ThreadLocal} object using 
the static {@link #setClientLocale(Locale)} so that client localized
+ *             messages can be retrieved using the {@link 
#getClientString(String, Object...)} method on all instances of this class.
+ *     <li>Resource bundles on parent classes can be added to the search path 
for this class by using the
+ *             {@link #addSearchPath(Class, String)} method.  This allows 
messages to be retrieved from the resource bundles of parent classes.
+ *     <li>Locale-specific bundles can be retrieved by using the {@link 
#getBundle(Locale)} method.
+ *     <li>The {@link #getString(Locale, String, Object...)} method can be 
used to retrieve locale-specific messages.
+ *     <li>Messages in the resource bundle can optionally be prefixed with the 
simple class name.
+ *             For example, if the class is <code>MyClass</code> and the 
properties file contains <js>"MyClass.myMessage"</js>,
+ *             the message can be retrieved using 
<code>getString(<js>"myMessage"</js>)</code>.
+ * </ul>
+ *
+ * @author James Bognar ([email protected])
+ */
+@ThreadSafe
+public class MessageBundle extends ResourceBundle {
+
+       private static final ThreadLocal<Locale> clientLocale = new 
ThreadLocal<Locale>();
+
+       private final ResourceBundle rb;
+       private final String bundlePath, className;
+       private final Class<?> forClass;
+       private final long creationThreadId;
+
+       // A map that contains all keys [shortKeyName->keyName] and 
[keyName->keyName], where shortKeyName
+       // refers to keys prefixed and stripped of the class name (e.g. 
"foobar"->"MyClass.foobar")
+       private final Map<String,String> keyMap = new 
ConcurrentHashMap<String,String>();
+
+       // Contains all keys present in all bundles in searchBundles.
+       private final ConcurrentSkipListSet<String> allKeys = new 
ConcurrentSkipListSet<String>();
+
+       // Bundles to search through to find properties.
+       // Typically this will be a list of resource bundles for each class up 
the class hierarchy chain.
+       private final CopyOnWriteArrayList<MessageBundle> searchBundles = new 
CopyOnWriteArrayList<MessageBundle>();
+
+       // Cache of message bundles per locale.
+       private final ConcurrentHashMap<Locale,MessageBundle> localizedBundles 
= new ConcurrentHashMap<Locale,MessageBundle>();
+
+       /**
+        * Sets the locale for this thread so that calls to {@link 
#getClientString(String, Object...)} return
+        * messages in that locale.
+        *
+        * @param locale The new client locale.
+        */
+       public static void setClientLocale(Locale locale) {
+               MessageBundle.clientLocale.set(locale);
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param forClass The class using this resource bundle.
+        * @param bundlePath The path of the resource bundle to wrap.
+        *      This can be an absolute path (e.g. 
<js>"com.ibm.MyMessages"</js>) or a path
+        *      relative to the package of the <l>forClass</l> (e.g. 
<js>"MyMessages"</js> if <l>forClass</l> is <js>"com.ibm.MyClass"</js>).
+        */
+       public MessageBundle(Class<?> forClass, String bundlePath) {
+               this(forClass, bundlePath, Locale.getDefault());
+       }
+
+       private MessageBundle(Class<?> forClass, String bundlePath, Locale 
locale) {
+               this.forClass = forClass;
+               this.className = forClass.getSimpleName();
+               this.bundlePath = bundlePath;
+               this.creationThreadId = Thread.currentThread().getId();
+               ClassLoader cl = forClass.getClassLoader();
+               ResourceBundle trb = null;
+               try {
+                       trb = ResourceBundle.getBundle(bundlePath, locale, cl);
+               } catch (MissingResourceException e) {
+                       try {
+                               trb = 
ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, 
locale, cl);
+                       } catch (MissingResourceException e2) {
+                       }
+               }
+               this.rb = trb;
+               if (rb != null) {
+
+                       // Populate keyMap with original mappings.
+                       for (Enumeration<String> e = getKeys(); 
e.hasMoreElements();) {
+                               String key = e.nextElement();
+                               keyMap.put(key, key);
+                       }
+
+                       // Override/augment with shortname mappings (e.g. 
"foobar"->"MyClass.foobar")
+                       String c = className + '.';
+                       for (Enumeration<String> e = getKeys(); 
e.hasMoreElements();) {
+                               String key = e.nextElement();
+                               if (key.startsWith(c)) {
+                                       String shortKey = 
key.substring(className.length() + 1);
+                                       keyMap.put(shortKey, key);
+                               }
+                       }
+
+                       allKeys.addAll(keyMap.keySet());
+               }
+               searchBundles.add(this);
+       }
+
+
+       /**
+        * Add another bundle path to this resource bundle.
+        * Order of property lookup is first-to-last.
+        * <p>
+        * This method must be called from the same thread as the call to the 
constructor.
+        * This eliminates the need for synchronization.
+        *
+        * @param forClass The class using this resource bundle.
+        * @param bundlePath The bundle path.
+        * @return This object (for method chaining).
+        */
+       @SuppressWarnings("hiding")
+       public MessageBundle addSearchPath(Class<?> forClass, String 
bundlePath) {
+               ThrowableUtils.assertSameThread(creationThreadId, "This method 
can only be called from the same thread that created the object.");
+               MessageBundle srb = new MessageBundle(forClass, bundlePath);
+               if (srb.rb != null) {
+                       allKeys.addAll(srb.keySet());
+                       searchBundles.add(srb);
+               }
+               return this;
+       }
+
+       @Override /* ResourceBundle */
+       public boolean containsKey(String key) {
+               return allKeys.contains(key);
+       }
+
+       /**
+        * Similar to {@link ResourceBundle#getString(String)} except allows 
you to pass in {@link MessageFormat} objects.
+        *
+        * @param key The resource bundle key.
+        * @param args Optional variable replacement arguments.
+        * @return The resolved value.  Never <jk>null</jk>.  <js>"{!!key}"</j> 
if the bundle is missing.  <js>"{!key}"</j> if the key is missing.
+        */
+       public String getString(String key, Object...args) {
+               String s = getString(key);
+               if (s.length() > 0 && s.charAt(0) == '{')
+                       return s;
+               if (args.length > 0)
+                       return MessageFormat.format(s, args);
+               return s;
+       }
+
+       /**
+        * Same as {@link #getString(String, Object...)} but allows you to 
specify the locale.
+        *
+        * @param locale The locale of the resource bundle to retrieve message 
from.
+        * @param key The resource bundle key.
+        * @param args Optional variable replacement arguments.
+        * @return The resolved value.  Never <jk>null</jk>.  <js>"{!!key}"</j> 
if the bundle is missing.  <js>"{!key}"</j> if the key is missing.
+        */
+       public String getString(Locale locale, String key, Object...args) {
+               if (locale == null)
+                       return getString(key, args);
+               return getBundle(locale).getString(key, args);
+       }
+
+       /**
+        * Same as {@link #getString(String, Object...)} but uses the locale 
specified on the call to {@link #setClientLocale(Locale)}.
+        *
+        * @param key The resource bundle key.
+        * @param args Optional variable replacement arguments.
+        * @return The resolved value.  Never <jk>null</jk>.  <js>"{!!key}"</j> 
if the bundle is missing.  <js>"{!key}"</j> if the key is missing.
+        */
+       public String getClientString(String key, Object...args) {
+               return getString(clientLocale.get(), key, args);
+       }
+
+       /**
+        * Looks for all the specified keys in the resource bundle and returns 
the first value that exists.
+        *
+        * @param keys The list of possible keys.
+        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
+        */
+       public String findFirstString(String...keys) {
+               if (rb == null)
+                       return null;
+               for (String k : keys) {
+                       if (containsKey(k))
+                               return getString(k);
+               }
+               return null;
+       }
+
+       /**
+        * Same as {@link #findFirstString(String...)}, but uses the specified 
locale.
+        *
+        * @param locale The locale of the resource bundle to retrieve message 
from.
+        * @param keys The list of possible keys.
+        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
+        */
+       public String findFirstString(Locale locale, String...keys) {
+               MessageBundle srb = getBundle(locale);
+               return srb.findFirstString(keys);
+       }
+
+       @Override /* ResourceBundle */
+       public Set<String> keySet() {
+               return Collections.unmodifiableSet(allKeys);
+       }
+
+       /**
+        * Returns all keys in this resource bundle with the specified prefix.
+        *
+        * @param prefix The prefix.
+        * @return The set of all keys in the resource bundle with the prefix.
+        */
+       public Set<String> keySet(String prefix) {
+               Set<String> set = new HashSet<String>();
+               for (String s : keySet()) {
+                       if (s.equals(prefix) || (s.startsWith(prefix) && 
s.charAt(prefix.length()) == '.'))
+                               set.add(s);
+               }
+               return set;
+       }
+
+       @Override /* ResourceBundle */
+       public Enumeration<String> getKeys() {
+               if (rb == null)
+                       return new Vector<String>(0).elements();
+               return rb.getKeys();
+       }
+
+       @Override /* ResourceBundle */
+       protected Object handleGetObject(String key) {
+               for (MessageBundle srb : searchBundles) {
+                       if (srb.rb != null) {
+                               String key2 = srb.keyMap.get(key);
+                               if (key2 != null) {
+                                       try {
+                                               return srb.rb.getObject(key2);
+                                       } catch (Exception e) {
+                                               return "{!"+key+"}";
+                                       }
+                               }
+                       }
+               }
+               if (rb == null)
+                       return "{!!"+key+"}";
+               return "{!"+key+"}";
+       }
+
+       /**
+        * Returns this resource bundle as an {@link ObjectMap}.
+        * <p>
+        * Useful for debugging purposes.
+        * Note that any class that implements a <code>toObjectMap()</code> 
method will automatically be serialized by
+        *      calling this method and serializing the result.
+        * <p>
+        * This method always constructs a new {@link ObjectMap} on each call.
+        *
+        * @return A new map containing all the keys and values in this bundle.
+        */
+       public ObjectMap toObjectMap() {
+               ObjectMap om = new ObjectMap();
+               for (String k : allKeys)
+                       om.put(k, getString(k));
+               return om;
+       }
+
+       /**
+        * Returns the resource bundle for the specified locale.
+        *
+        * @param locale The client locale.
+        * @return The resource bundle for the specified locale.  Never 
<jk>null</jk>.
+        */
+       public MessageBundle getBundle(Locale locale) {
+               MessageBundle mb = localizedBundles.get(locale);
+               if (mb != null)
+                       return mb;
+               mb = new MessageBundle(forClass, bundlePath, locale);
+               List<MessageBundle> l = new 
ArrayList<MessageBundle>(searchBundles.size()-1);
+               for (int i = 1; i < searchBundles.size(); i++) {
+                       MessageBundle srb = searchBundles.get(i);
+                       srb = new MessageBundle(srb.forClass, srb.bundlePath, 
locale);
+                       l.add(srb);
+                       mb.allKeys.addAll(srb.keySet());
+               }
+               mb.searchBundles.addAll(l);
+               localizedBundles.putIfAbsent(locale, mb);
+               return localizedBundles.get(locale);
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/PojoIntrospector.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/src/main/java/org/apache/juneau/utils/PojoIntrospector.java 
b/juneau-core/src/main/java/org/apache/juneau/utils/PojoIntrospector.java
new file mode 100644
index 0000000..7e7773c
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/PojoIntrospector.java
@@ -0,0 +1,118 @@
+/***************************************************************************************************************************
+ * 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.utils;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Used to invoke methods on {@code Objects} using arguments in serialized 
form.
+ *     <p>
+ *     Example:
+ *     <p class='bcode'>
+ *             String s = <js>"foobar"</js>;
+ *             String s2 = (String)<jk>new</jk> 
PojoIntrospector(s).invoke(<js>"substring(int,int)"</js>, <js>"[3,6]"</js>);  
<jc>// "bar"</jc>
+ *     </p>
+ *
+ * @author James Bognar ([email protected])
+ */
+public final class PojoIntrospector {
+
+       private final Object o;
+       private final ReaderParser p;
+
+       /**
+        * Constructor.
+        *
+        * @param o The object on which Java methods will be invoked.
+        * @param p The parser to use to parse the method arguments.  If 
<jk>null</jk>, {@link JsonParser#DEFAULT} is used.
+        */
+       public PojoIntrospector(Object o, ReaderParser p) {
+               if (p == null)
+                       p = JsonParser.DEFAULT;
+               this.o = o;
+               this.p = p;
+       }
+
+       /**
+        * Shortcut for calling <code><jk>new</jk> PojoIntrospector(o, 
<jk>null</jk>);</code>
+        *
+        * @param o The object on which Java methods will be invoked.
+        */
+       public PojoIntrospector(Object o) {
+               this(o, null);
+       }
+
+       /**
+        * Primary method.  Invokes the specified method on this bean.
+        *
+        * @param method The method being invoked.
+        * @param args The arguments to pass as parameters to the method.<br>
+        *      These will automatically be converted to the appropriate object 
type if possible.<br>
+        *      Can be <jk>null</jk> if method has no arguments.
+        * @return The object returned by the call to the method, or 
<jk>null</jk> if target object is <jk>null</jk>.
+        * @throws IllegalAccessException If the <code>Constructor</code> 
object enforces Java language access control and the underlying constructor is 
inaccessible.
+        * @throws IllegalArgumentException If one of the following occurs:
+        *      <ul class='spaced-list'>
+        *              <li>The number of actual and formal parameters differ.
+        *              <li>An unwrapping conversion for primitive arguments 
fails.
+        *              <li>A parameter value cannot be converted to the 
corresponding formal parameter type by a method invocation conversion.
+        *              <li>The constructor pertains to an enum type.
+        *      </ul>
+        * @throws InvocationTargetException If the underlying constructor 
throws an exception.
+        * @throws ParseException If the input contains a syntax error or is 
malformed.
+        * @throws IOException
+        */
+       public Object invokeMethod(Method method, Reader args) throws 
InvocationTargetException, IllegalArgumentException, IllegalAccessException, 
ParseException, IOException {
+               if (o == null)
+                       return null;
+               ClassMeta<?>[] argTypes = 
p.getBeanContext().getClassMetas(method.getParameterTypes());
+               Object[] params = args == null ? null : p.parseArgs(args, 
argTypes);
+               return method.invoke(o, params);
+       }
+
+       /**
+        * Convenience method for invoking argument from method signature (@see 
{@link ClassUtils#getMethodSignature(Method)}.
+        *
+        * @param method The method being invoked.
+        * @param args The arguments to pass as parameters to the method.<br>
+        *      These will automatically be converted to the appropriate object 
type if possible.<br>
+        *      Can be <jk>null</jk> if method has no arguments.
+        * @return The object returned by the call to the method, or 
<jk>null</jk> if target object is <jk>null</jk>.
+        * @throws NoSuchMethodException If method does not exist.
+        * @throws IllegalAccessException If the <code>Constructor</code> 
object enforces Java language access control and the underlying constructor is 
inaccessible.
+        * @throws IllegalArgumentException If one of the following occurs:
+        *      <ul class='spaced-list'>
+        *              <li>The number of actual and formal parameters differ.
+        *              <li>An unwrapping conversion for primitive arguments 
fails.
+        *              <li>A parameter value cannot be converted to the 
corresponding formal parameter type by a method invocation conversion.
+        *              <li>The constructor pertains to an enum type.
+        *      </ul>
+        * @throws InvocationTargetException If the underlying constructor 
throws an exception.
+        * @throws ParseException If the input contains a syntax error or is 
malformed.
+        * @throws IOException
+        */
+       public Object invokeMethod(String method, String args) throws 
NoSuchMethodException, IllegalArgumentException, InvocationTargetException, 
IllegalAccessException, ParseException, IOException {
+               if (o == null)
+                       return null;
+               Method m = 
p.getBeanContext().getClassMeta(o.getClass()).getPublicMethods().get(method);
+               if (m == null)
+                       throw new NoSuchMethodException(method);
+               return invokeMethod(m, args == null ? null : new 
StringReader(args));
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/utils/PojoQuery.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/utils/PojoQuery.java 
b/juneau-core/src/main/java/org/apache/juneau/utils/PojoQuery.java
new file mode 100644
index 0000000..cb92897
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/utils/PojoQuery.java
@@ -0,0 +1,1251 @@
+/***************************************************************************************************************************
+ * 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.utils;
+
+import static java.util.Calendar.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Designed to provide query/view/sort/paging filtering on tabular in-memory 
POJO models.
+ * <p>
+ *     It can also perform just view filtering on beans/maps.
+ * <p>
+ *     Examples of tabular POJO models:
+ *     <ul>
+ *             <li><tt>Collection{@code <Map>}</tt>
+ *             <li><tt>Collection{@code <Bean>}</tt>
+ *             <li><tt>Map[]</tt>
+ *             <li><tt>Bean[]</tt>
+ *     </ul>
+ * <p>
+ *     Tabular POJO models can be thought of as tables of data.  For example, 
a list of the following beans...
+ * <p class='bcode'>
+ *     <jk>public</jk> MyBean {
+ *             <jk>public int</jk> fi;
+ *             <jk>public</jk> String fs;
+ *             <jk>public</jk> Date fd;
+ *     }
+ * <p>
+ *     ... can be thought of a table containing the following columns...
+ * <p>
+ *     <table class='styled code'>
+ *             <tr><th>fi</th><th>fs</th><th>fd</th></tr>
+ *             <tr><td>123</td><td>'foobar'</td><td>yyyy/MM/dd 
HH:mm:ss</td></tr>
+ *             <tr><td colspan=3>...</td></tr>
+ *     </table>
+ * <p>
+ *     From this table, you can perform the following functions:
+ *     <ul class='spaced-list'>
+ *             <li>Query - Return only rows where a search pattern matches.
+ *             <li>View - Return only the specified subset of columns in the 
specified order.
+ *             <li>Sort - Sort the table by one or more columns.
+ *             <li>Page - Only return a subset of rows.
+ *     </ul>
+ *
+ * <h5 class='topic'>Query</h5>
+ * <p>
+ *     The query capabilites allow you to filter based on query patterns 
against
+ *     strings, dates, and numbers.  Queries take the form of a Map with 
column names
+ *     as keys, and search patterns as values.  <br>
+ *     Search patterns can be either {@code Strings} or {@code Maps}.<br>
+ *     Multiple search patterns are ANDed (i.e. all patterns must match for 
the row to be returned).
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <ul class='spaced-list'>
+ *     <li><tt>{fi:'123'}</tt> - Return only rows where the <tt>fi</tt> column 
is 123.
+ *     <li><tt>{fs:'foobar'}</tt> - Return only rows where the <tt>fs</tt> 
column is 'foobar'.
+ *     <li><tt>{fd:'2001'}</tt> - Return only rows where the <tt>fd</tt> 
column have dates in the year 2001.
+ *     <li><tt>{fs:'foobar'}</tt> - Return only rows where the <tt>fs</tt> 
column is 'foobar'.
+ *             and the <tt>fs</tt> column starts with <tt>"foo"</tt>.
+ * </ul>
+ * <p>
+ *     Search patterns can also be applied to lower level fields.  For 
example, the search term
+ *     <tt>{f1:{f2:{f3{'foobar'}}}</tt> means only return top level rows where 
the <tt>f1.getF2().getF3()</tt>
+ *     property is <tt>'foobar'</tt>.
+ *
+ * <h5 class='topic'>String Patterns</h5>
+ * <p>
+ *     Any objects can be queried against using string patterns.  If the 
objects being
+ *     searched are not strings, then the patterns are matched against 
whatever is
+ *     return by the {@code Object#toString()} method.
+ *
+ * <h6 class='topic'>Example string query patterns</h6>
+ * <ul>
+ *     <li><tt>foo</tt> - The string 'foo'
+ *     <li><tt>foo bar</tt> - The string 'foo' or the string 'bar'
+ *     <li><tt>'foo bar'</tt> - The phrase 'foo bar'
+ *     <li><tt>"foo bar"</tt> - The phrase 'foo bar'
+ *     <li><tt>foo*</tt> - <tt>*</tt> matches zero-or-more characters.
+ *     <li><tt>foo?</tt> - <tt>?</tt> matches exactly one character
+ * </ul>
+ *
+ * <h6 class='topic'>Notes</h6>
+ * <ul class='spaced-list'>
+ *     <li>Whitespace is ignored around search patterns.
+ *     <li>Prepend <tt>+</tt> to tokens that must match.  (e.g. <tt>+foo* 
+*bar</tt>)
+ *     <li>Prepend <tt>-</tt> to tokens that must not match.  (e.g. <tt>+foo* 
-*bar</tt>)
+ * </ul>
+ *
+ * <h5 class='topic'>Numeric Patterns</h5>
+ * <p>
+ *     Any object of type {@link Number} (or numeric primitives) can be 
searched using numeric patterns.
+ *
+ * <h6 class='topic'>Example numeric query patterns</h6>
+ * <ul>
+ *     <li><tt>123</tt> - The single number 123
+ *     <li><tt>1 2 3</tt>      - 1, 2, or 3
+ *     <li><tt>1-100</tt> - Between 1 and 100
+ *     <li><tt>1 - 100</tt> - Between 1 and 100
+ *     <li><tt>1 - 100 200-300</tt> - Between 1 and 100 or between 200 and 300
+ *     <li><tt>&gt; 100</tt> - Greater than 100
+ *     <li><tt>&gt;= 100</tt> - Greater than or equal to 100
+ *     <li><tt>!123</tt> - Not 123
+ * </ul>
+ *
+ * <h6 class='topic'>Notes</h6>
+ * <ul class='spaced-list'>
+ *     <li>Whitespace is ignored in search patterns.
+ *     <li>Negative numbers are supported.
+ * </ul>
+ *
+ * <h5 class='topic'>Date Patterns</h5>
+ * <p>
+ *     Any object of type {@link Date} or {@link Calendar} can be searched 
using date patterns.
+ * <p>
+ *     The default valid input timestamp formats (which can be overridden via 
the {@link #setValidTimestampFormats(String...)} method are...
+ *
+ * <ul>
+ *     <li><tt>yyyy.MM.dd.HH.mm.ss</tt>
+ *     <li><tt>yyyy.MM.dd.HH.mm</tt>
+ *     <li><tt>yyyy.MM.dd.HH</tt>
+ *     <li><tt>yyyy.MM.dd</tt>
+ *     <li><tt>yyyy.MM</tt>
+ *     <li><tt>yyyy</tt>
+ * </ul>
+ *
+ * <h6 class='topic'>Example date query patterns</h6>
+ * <ul>
+ *     <li><tt>2001</tt> - A specific year.
+ *     <li><tt>2001.01.01.10.50</tt> - A specific time.
+ *     <li><tt>&gt;2001</tt>   - After a specific year.
+ *     <li><tt>&gt;=2001</tt> - During or after a specific year.
+ *     <li><tt>2001 - 2003.06.30</tt>  - A date range.
+ *     <li><tt>2001 2003 2005</tt>     - Multiple date patterns are ORed.
+ * </ul>
+ *
+ * <h6 class='topic'>Notes</h6>
+ * <ul>
+ *     <li>Whitespace is ignored in search patterns.
+ * </ul>
+ *
+ * <h5 class='topic'>View</h5>
+ * <p>
+ *     The view capability allows you to return only the specified subset of 
columns in the
+ *     specified order.<br>
+ *     The view parameter is a list of either <tt>Strings</tt> or 
<tt>Maps</tt>.
+ *
+ * <h6 class='topic'>Example view parameters</h6>
+ * <ul>
+ *     <li><tt>['f1']</tt> - Return only column 'f1'.
+ *     <li><tt>['f2','f1']</tt> - Return only columns 'f2' and 'f1'.
+ *     <li><tt>['f1',{f2:'f3'}]</tt> - Return only columns 'f1' and 'f2', but 
for 'f2' objects,
+ *             only show the 'f3' property.
+ * </ul>
+ *
+ * <h5 class='topic'>Sort</h5>
+ * <p>
+ *     The sort capability allows you to sort values by the specified rows.<br>
+ *     The sort parameter is a list of either <tt>Strings</tt> or 
<tt>Maps</tt>.<br>
+ *     <tt>Strings</tt> represent column names to sort ascending.  If you want
+ *     to sort descending, you need to specify a <tt>Map</tt> of the form 
<tt>{colname:'d'}</tt>
+ *
+ * <h6 class='topic'>Example sort parameters</h6>
+ * <ul>
+ *     <li><tt>['f1']</tt> - Sort rows by column 'f1' ascending.
+ *     <li><tt>[{f1:'a'}]</tt> - Sort rows by column 'f1' ascending.
+ *     <li><tt>[{f1:'d'}]</tt> - Sort rows by column 'f1' descending.
+ *     <li><tt>[{f1:'a'},{f2:'d'}]</tt> - Sort rows by column 'f1' ascending, 
then 'f2' descending.
+ * </ul>
+ *
+ * <h5 class='topic'>Paging</h5>
+ * <p>
+ *     Use the <tt>pos</tt> and <tt>limit</tt> parameters to specify a subset 
of rows to
+ *     return.
+ *
+ * <h5 class='topic'>Other Notes</h5>
+ * <ul class='spaced-list'>
+ *     <li>Calling <tt>filterMap()</tt> or <tt>filterCollection()</tt> always 
returns a new data
+ *             structure, so the methods can be called multiple times against 
the same input.
+ * </ul>
+ *
+ * @author James Bognar ([email protected])
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public final class PojoQuery {
+
+       private Object input;
+       private ClassMeta type;
+       private BeanContext beanContext;
+
+       /**
+        * Constructor.
+        *
+        * @param input The POJO we're going to be filtering.
+        * @param beanContext The bean context to use to create bean maps for 
beans.
+        */
+       public PojoQuery(Object input, BeanContext beanContext) {
+               this.input = input;
+               this.type = beanContext.getClassMetaForObject(input);
+               this.beanContext = beanContext;
+       }
+
+       /**
+        * Filters the input object as a map.
+        *
+        * @param view The list and order of properties to return from the map. 
 Values must be of type {@code String} or {@code Map}.
+        * @return The filtered map
+        */
+       public Map filterMap(List view) {
+
+               if (input == null)
+                       return null;
+
+               if (! (type.isBean() || type.isMap()))
+                       throw new RuntimeException("Cannot call filterMap() on 
class type " + type);
+
+               Map m = (Map)replaceWithMutables(input);
+               doView(m, view);
+
+               return m;
+       }
+
+       /**
+        * Filters the input object as a collection of maps.
+        *
+        * @param query The query attributes.  Keys must be column names and 
values must be of type {@code String} or {@code Map}.
+        * @param view The view attributes.  Values must be of type {@code 
String} or {@code Map}.
+        * @param sort The sort attributes.  Values must be of type {@code 
String} or {@code Map}.
+        * @param pos The index into the list to start returning results from.  
Default is {@code 0}.
+        * @param limit The number of rows to return.  Default is all rows.
+        * @param ignoreCase If <jk>true</jk>, then querying is case 
insensitive.  Default is <jk>false</jk>.
+        * @return The filtered collection.
+        */
+       public List filterCollection(Map query, List view, List sort, int pos, 
int limit, boolean ignoreCase) {
+
+               if (input == null)
+                       return null;
+
+               if (! (type.isArray() || type.isCollection()))
+                       throw new RuntimeException("Cannot call 
filterCollection() on class type " + type);
+
+               if (view == null)
+                       view = Collections.EMPTY_LIST;
+
+               if (sort == null)
+                       sort = Collections.EMPTY_LIST;
+
+               // Create a new ObjectList
+               ObjectList l = (ObjectList)replaceWithMutables(input);
+
+               // Do the search
+               CollectionFilter filter = new CollectionFilter(query, 
ignoreCase);
+               filter.doQuery(l);
+
+               // If sort or view isn't empty, then we need to make sure that 
all entries in the
+               // list are maps.
+               if ((! sort.isEmpty()) || (! view.isEmpty())) {
+
+                       if (! sort.isEmpty())
+                               doSort(l, sort);
+
+                       if (! view.isEmpty())
+                               doView(l, view);
+               }
+
+               // Do the paging.
+               if (pos != 0 || limit != 0) {
+                       int end = (limit == 0 || limit+pos >= l.size()) ? 
l.size() : limit + pos;
+                       ObjectList l2 = new 
DelegateList(((DelegateList)l).getClassMeta());
+                       l2.addAll(l.subList(pos, end));
+                       l = l2;
+               }
+
+               return l;
+       }
+
+       /*
+        * If there are any non-Maps in the specified list, replaces them with 
BeanMaps.
+        */
+       private Object replaceWithMutables(Object o) {
+               if (o == null)
+                       return null;
+               ClassMeta cm = beanContext.getClassMetaForObject(o);
+               if (cm.isCollection()) {
+                       ObjectList l = new 
DelegateList(beanContext.getClassMetaForObject(o));
+                       for (Object o2 : (Collection)o)
+                               l.add(replaceWithMutables(o2));
+                       return l;
+               }
+               if (cm.isMap() && o instanceof BeanMap) {
+                       BeanMap bm = (BeanMap)o;
+                       DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), 
beanContext);
+                       for (BeanMapEntry e : (Set<BeanMapEntry>)bm.entrySet()) 
{
+                               ClassMeta ct1 = e.getMeta().getClassMeta();
+                               if (ct1.isArray() || ct1.isBean() || 
ct1.isCollection() || ct1.isMap() || ct1.isObject())
+                                       dbm.put(e.getKey(), 
replaceWithMutables(e.getValue()));
+                               else
+                                       dbm.addKey(e.getKey());
+                       }
+                       return dbm;
+               }
+               if (cm.isBean()) {
+                       BeanMap bm = beanContext.forBean(o);
+                       DelegateBeanMap dbm = new DelegateBeanMap(bm.getBean(), 
beanContext);
+                       for (BeanMapEntry e : (Set<BeanMapEntry>)bm.entrySet()) 
{
+                               ClassMeta ct1 = e.getMeta().getClassMeta();
+                               if (ct1.isArray() || ct1.isBean() || 
ct1.isCollection() || ct1.isMap() || ct1.isObject()) {
+                                       Object val = null;
+                                       try {
+                                               val = e.getValue();
+                                       } catch (BeanRuntimeException ex) {
+                                               // Ignore.
+                                       }
+                                       dbm.put(e.getKey(), 
replaceWithMutables(val));
+                               }
+                               else
+                                       dbm.addKey(e.getKey());
+                       }
+                       return dbm;
+               }
+               if (cm.isMap()) {
+                       Map m = (Map)o;
+                       DelegateMap dm = new 
DelegateMap(beanContext.getClassMetaForObject(m));
+                       for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
+                               dm.put(e.getKey().toString(), 
replaceWithMutables(e.getValue()));
+                       return dm;
+               }
+               if (cm.isArray()) {
+                       return replaceWithMutables(Arrays.asList((Object[])o));
+               }
+               return o;
+       }
+
+       /*
+        * Sorts the specified list by the sort list.
+        */
+       private void doSort(List list, List sortList) {
+
+               Map sort = new LinkedHashMap();
+               for (Object s : sortList) {
+                       if (s instanceof String)
+                               sort.put(s, "a");
+                       else if (s instanceof Map) {
+                               Map sm = (Map)s;
+                               for (Map.Entry e : 
(Set<Map.Entry>)sm.entrySet())
+                                       sort.put(e.getKey(), 
e.getValue().toString().toLowerCase(Locale.ENGLISH));
+                       }
+               }
+
+               // Do the sort.
+               List<String> columns = new ArrayList<String>(sort.keySet());
+               Collections.reverse(columns);
+               for (final String c : columns) {
+                       final boolean isDesc = 
StringUtils.startsWith(sort.get(c).toString(), 'd');
+                       Comparator comp = new Comparator<Map>() {
+                               @Override /* Comparator */
+                               public int compare(Map m1, Map m2) {
+                                       Comparable v1 = (Comparable)m1.get(c), 
v2 = (Comparable)m2.get(c);
+                                       if (v1 == null && v2 == null)
+                                               return 0;
+                                       if (v1 == null)
+                                               return (isDesc ? -1 : 1);
+                                       if (v2 == null)
+                                               return (isDesc ? 1 : -1);
+                                       return (isDesc ? v2.compareTo(v1) : 
v1.compareTo(v2));
+                               }
+                       };
+                       Collections.sort(list, comp);
+               }
+       }
+
+       /*
+        * Filters all but the specified view columns on all entries in the 
specified list.
+        */
+       private void doView(List list, List view) {
+
+               for (ListIterator i = list.listIterator(); i.hasNext();) {
+                       Object o = i.next();
+                       Map m = (Map)o;
+                       doView(m, view);
+               }
+       }
+
+       /*
+        * Creates a new Map with only the entries specified in the view list.
+        */
+       private void doView(Map m, List view) {
+               List<String> filterKeys = new LinkedList<String>();
+               for (Object v : view) {
+                       if (v instanceof String) {
+                               filterKeys.add(v.toString());
+                       } else if (v instanceof ObjectMap) {
+                               ObjectMap vm = (ObjectMap)v;
+                               for (Map.Entry<String,Object> e : 
vm.entrySet()) {
+                                       String vmKey = e.getKey();
+                                       Object vmVal = e.getValue();
+                                       Object mv = m.get(vmKey);
+                                       filterKeys.add(vmKey);
+                                       if (vmVal instanceof List) {
+                                               List l = (List)vmVal;
+                                               if (mv instanceof List)
+                                                       doView((List)mv, l);
+                                               else if (mv instanceof Map)
+                                                       doView((Map)mv, l);
+                                       }
+                               }
+                       }
+               }
+               if (m instanceof DelegateMap)
+                       ((DelegateMap)m).filterKeys(filterKeys);
+               else
+                       ((DelegateBeanMap)m).filterKeys(filterKeys);
+       }
+
+
+       /*
+        * Returns the appropriate IMatcher for the specified class type.
+        */
+       private IMatcher getObjectMatcherForType(String queryString, boolean 
ignoreCase, ClassMeta cm) {
+               if (cm.isDate())
+                       return new DateMatcher(queryString);
+               if (cm.isNumber())
+                       return new NumberMatcher(queryString);
+               if (cm.isObject())
+                       return new ObjectMatcher(queryString, ignoreCase);
+               return new StringMatcher(queryString, ignoreCase);
+       }
+
+       
//====================================================================================================
+       // CollectionFilter
+       
//====================================================================================================
+       private class CollectionFilter {
+               IMatcher entryMatcher;
+
+               public CollectionFilter(Map query, boolean ignoreCase) {
+                       if (query != null && ! query.isEmpty())
+                               entryMatcher = new MapMatcher(query, 
ignoreCase);
+               }
+
+               public void doQuery(List in) {
+                       if (in == null || entryMatcher == null)
+                               return;
+                       for (Iterator i = in.iterator(); i.hasNext();) {
+                               Object o = i.next();
+                               if (! entryMatcher.matches(o))
+                                       i.remove();
+                       }
+               }
+       }
+
+       
//====================================================================================================
+       // IMatcher
+       
//====================================================================================================
+       private interface IMatcher<E> {
+               public boolean matches(E o);
+       }
+
+       
//====================================================================================================
+       // MapMatcher
+       
//====================================================================================================
+       /*
+        * Matches on a Map only if all specified entry matchers match.
+        */
+       private class MapMatcher implements IMatcher<Map> {
+
+               Map<String,IMatcher> entryMatchers = new 
HashMap<String,IMatcher>();
+
+               public MapMatcher(Map query, boolean ignoreCase) {
+                       for (Map.Entry e : (Set<Map.Entry>)query.entrySet()) {
+                               String key = e.getKey().toString();
+                               Object value = e.getValue();
+                               IMatcher matcher = null;
+                               if (value instanceof String)
+                                       matcher = 
getObjectMatcherForType((String)value, ignoreCase, beanContext.object());
+                               else if (value instanceof ObjectMap)
+                                       matcher = new 
MapMatcher((ObjectMap)value, ignoreCase);
+                               else
+                                       throw new RuntimeException("Invalid 
value type: " + value);
+                               entryMatchers.put(key, matcher);
+                       }
+               }
+
+               @Override /* IMatcher */
+               public boolean matches(Map m) {
+                       if (m == null)
+                               return false;
+                       for (Map.Entry<String,IMatcher> e : 
entryMatchers.entrySet()) {
+                               String key = e.getKey();
+                               Object val = m.get(key);
+                               if (! e.getValue().matches(val))
+                                       return false;
+                       }
+                       return true;
+               }
+       }
+
+       
//====================================================================================================
+       // 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 ObjectMatcher implements IMatcher<Object> {
+
+               String searchPattern;
+               boolean ignoreCase;
+               DateMatcher dateMatcher;
+               NumberMatcher numberMatcher;
+               StringMatcher stringMatcher;
+
+               ObjectMatcher(String searchPattern, boolean ignoreCase) {
+                       this.searchPattern = searchPattern;
+                       this.ignoreCase = ignoreCase;
+               }
+
+               @Override /* IMatcher */
+               public boolean matches(Object o) {
+                       if (o instanceof Number)
+                               return getNumberMatcher().matches(o);
+                       if (o instanceof Date || o instanceof Calendar)
+                               return getDateMatcher().matches(o);
+                       return getStringMatcher().matches(o);
+               }
+
+               private IMatcher getNumberMatcher() {
+                       if (numberMatcher == null)
+                               numberMatcher = new 
NumberMatcher(searchPattern);
+                       return numberMatcher;
+               }
+
+               private IMatcher getStringMatcher() {
+                       if (stringMatcher == null)
+                               stringMatcher = new 
StringMatcher(searchPattern, ignoreCase);
+                       return stringMatcher;
+               }
+
+               private IMatcher getDateMatcher() {
+                       if (dateMatcher == null)
+                               dateMatcher = new DateMatcher(searchPattern);
+                       return dateMatcher;
+               }
+       }
+
+       
//====================================================================================================
+       // NumberMatcher
+       
//====================================================================================================
+       private static class NumberMatcher implements IMatcher<Number> {
+
+               private NumberPattern[] numberPatterns;
+
+               /**
+                * Construct a number matcher for the given search pattern.
+                * @param searchPattern A date search paattern.  See class 
usage for a description.
+                */
+               public NumberMatcher(String searchPattern) {
+                       numberPatterns = new NumberPattern[1];
+                       numberPatterns[0] = new NumberPattern(searchPattern);
+
+               }
+
+               /**
+                * Returns 'true' if this integer matches the pattern(s).
+                */
+               @Override /* IMatcher */
+               public boolean matches(Number in) {
+                       for (int i = 0; i < numberPatterns.length; i++) {
+                               if (! numberPatterns[i].matches(in))
+                                       return false;
+                       }
+                       return true;
+               }
+
+       }
+
+       /**
+        * A construct representing a single search pattern.
+        */
+       private static class NumberPattern {
+               NumberRange[] numberRanges;
+
+               public NumberPattern(String searchPattern) {
+
+                       List<NumberRange> l = new LinkedList<NumberRange>();
+
+                       for (String s : breakUpTokens(searchPattern)) {
+                               boolean isNot = (s.charAt(0) == '!');
+                               String token = s.substring(1);
+                               Pattern p = 
Pattern.compile("(([<>]=?)?)(-?\\d+)(-?(-?\\d+)?)");
+
+                               // Possible patterns:
+                               // 123, >123, <123, >=123, <=123, >-123, 
>=-123, 123-456, -123--456
+                               // Regular expression used:  
(([<>]=?)?)(-?\d+)(-??(-?\d+))
+                               Matcher m = p.matcher(token);
+
+                               // If a non-numeric value was passed in for a 
numeric value, just set the value to '0'.
+                               // (I think this might resolve a workaround in 
custom queries).
+                               if (! m.matches())
+                                       throw new RuntimeException("Numeric 
value didn't match pattern:  ["+token+"]");
+                                       //m = numericPattern.matcher("0");
+
+                               String arg1 = m.group(1);
+                               String start = m.group(3);
+                               String end = m.group(5);
+
+                               l.add(new NumberRange(arg1, start, end, isNot));
+                       }
+
+                       numberRanges = l.toArray(new NumberRange[l.size()]);
+               }
+
+               private List<String> breakUpTokens(String s) {
+                       // Get rid of whitespace in "123 - 456"
+                       s = s.replaceAll("(-?\\d+)\\s*-\\s*(-?\\d+)", "$1-$2");
+                       // Get rid of whitespace in ">= 123"
+                       s = s.replaceAll("([<>]=?)\\s+(-?\\d+)", "$1$2");
+                       // Get rid of whitespace in "! 123"
+                       s = s.replaceAll("(!)\\s+(-?\\d+)", "$1$2");
+
+                       // Replace all commas with whitespace
+                       // Allows for alternate notation of: 123,456...
+                       s = s.replaceAll(",", " ");
+
+                       String[] s2 = s.split("\\s+");
+
+                       // Make all tokens 'ORed'.  There is no way to AND 
numeric tokens.
+                       for (int i = 0; i < s2.length; i++)
+                               if (! StringUtils.startsWith(s2[i], '!'))
+                                       s2[i] = "^"+s2[i];
+
+                       List<String> l = new LinkedList<String>();
+                       l.addAll(Arrays.asList(s2));
+                       return l;
+               }
+
+               public boolean matches(Number number) {
+                       if (numberRanges.length == 0) return true;
+                       for (int i = 0; i < numberRanges.length; i++)
+                               if (numberRanges[i].matches(number))
+                                       return true;
+                       return false;
+               }
+       }
+
+       /**
+        * A construct representing a single search range in a single search 
pattern.
+        * All possible forms of search patterns are boiled down to these 
number ranges.
+        */
+       private static class NumberRange {
+               int start;
+               int end;
+               boolean isNot;
+
+               public NumberRange(String arg, String start, String end, 
boolean isNot) {
+
+                       this.isNot = isNot;
+
+                       // 123, >123, <123, >=123, <=123, >-123, >=-123, 
123-456, -123--456
+                       if (arg.equals("") && end == null) { // 123
+                               this.start = Integer.parseInt(start);
+                               this.end = this.start;
+                       } else if (arg.equals(">")) {
+                               this.start = Integer.parseInt(start)+1;
+                               this.end = Integer.MAX_VALUE;
+                       } else if (arg.equals(">=")) {
+                               this.start = Integer.parseInt(start);
+                               this.end = Integer.MAX_VALUE;
+                       } else if (arg.equals("<")) {
+                               this.start = Integer.MIN_VALUE;
+                               this.end = Integer.parseInt(start)-1;
+                       } else if (arg.equals("<=")) {
+                               this.start = Integer.MIN_VALUE;
+                               this.end = Integer.parseInt(start);
+                       } else {
+                               this.start = Integer.parseInt(start);
+                               this.end = Integer.parseInt(end);
+                       }
+               }
+
+               public boolean matches(Number n) {
+                       long i = n.longValue();
+                       boolean b = (i>=start && i<=end);
+                       if (isNot) b = !b;
+                       return b;
+               }
+       }
+
+       
//====================================================================================================
+       // DateMatcher
+       
//====================================================================================================
+       /** The list of all valid timestamp formats */
+       private SimpleDateFormat[] validTimestampFormats = new 
SimpleDateFormat[0];
+       {
+               
setValidTimestampFormats("yyyy.MM.dd.HH.mm.ss","yyyy.MM.dd.HH.mm","yyyy.MM.dd.HH","yyyy.MM.dd","yyyy.MM","yyyy");
+       }
+
+       /**
+        * Use this method to override the allowed search patterns when used in 
locales where time formats are
+        * different.
+        * @param s A comma-delimited list of valid time formats.
+        */
+       public void setValidTimestampFormats(String...s) {
+               validTimestampFormats = new SimpleDateFormat[s.length];
+               for (int i = 0; i < s.length; i++)
+                       validTimestampFormats[i] = new SimpleDateFormat(s[i]);
+       }
+
+       private class DateMatcher implements IMatcher<Object> {
+
+               private TimestampPattern[] patterns;
+
+               /**
+                * Construct a timestamp matcher for the given search pattern.
+                * @param searchPattern The search pattern.
+                */
+               DateMatcher(String searchPattern) {
+                       patterns = new TimestampPattern[1];
+                       patterns[0] = new TimestampPattern(searchPattern);
+
+               }
+
+               /**
+                * Returns <jk>true</jk> if the specified date matches the 
pattern passed in through
+                *      the contstructor.<br>
+                * The Object can be of type {@link Date} or {@link 
Calendar}.<br>
+                * Always returns <jk>false</jk> on <jk>null</jk> input.
+                */
+               @Override /* IMatcher */
+               public boolean matches(Object in) {
+                       if (in == null) return false;
+
+                       Calendar c = null;
+                       if (in instanceof Calendar)
+                               c = (Calendar)in;
+                       else if (in instanceof Date) {
+                               c = Calendar.getInstance();
+                               c.setTime((Date)in);
+                       } else {
+                               return false;
+                       }
+                       for (int i = 0; i < patterns.length; i++) {
+                               if (! patterns[i].matches(c))
+                                       return false;
+                       }
+                       return true;
+               }
+       }
+
+       /**
+        * A construct representing a single search pattern.
+        */
+       private class TimestampPattern {
+               TimestampRange[] ranges;
+               List<TimestampRange> l = new LinkedList<TimestampRange>();
+
+               public TimestampPattern(String s) {
+
+                       // Handle special case where timestamp is enclosed in 
quotes.
+                       // This can occur on hyperlinks created by group-by 
queries.
+                       // e.g. '2007/01/29 04:17:43 PM'
+                       if (s.charAt(0) == '\'' && s.charAt(s.length()-1) == 
'\'')
+                               s = s.substring(1, s.length()-1);
+
+                       // Pattern for finding <,>,<=,>=
+                       Pattern p1 = 
Pattern.compile("^\\s*([<>](?:=)?)\\s*(\\S+.*)$");
+                       // Pattern for finding range dash (e.g. xxx - yyy)
+                       Pattern p2 = Pattern.compile("^(\\s*-\\s*)(\\S+.*)$");
+
+                       // States are...
+                       // 1 - Looking for <,>,<=,>=
+                       // 2 - Looking for single date.
+                       // 3 - Looking for start date.
+                       // 4 - Looking for -
+                       // 5 - Looking for end date.
+                       int state = 1;
+
+                       String op = null;
+                       CalendarP startDate = null;
+
+                       ParsePosition pp = new ParsePosition(0);
+                       Matcher m = null;
+                       String seg = s;
+
+                       while (! seg.equals("") || state != 1) {
+                               if (state == 1) {
+                                       m = p1.matcher(seg);
+                                       if (m.matches()) {
+                                               op = m.group(1);
+                                               seg = m.group(2);
+                                               state = 2;
+                                       } else {
+                                               state = 3;
+                                       }
+                               } else if (state == 2) {
+                                       l.add(new TimestampRange(op, 
parseDate(seg, pp)));
+                                       //tokens.add("^"+op + 
parseTimestamp(seg, pp));
+                                       seg = 
seg.substring(pp.getIndex()).trim();
+                                       pp.setIndex(0);
+                                       state = 1;
+                               } else if (state == 3) {
+                                       startDate = parseDate(seg, pp);
+                                       seg = 
seg.substring(pp.getIndex()).trim();
+                                       pp.setIndex(0);
+                                       state = 4;
+                               } else if (state == 4) {
+                                       // Look for '-'
+                                       m = p2.matcher(seg);
+                                       if (m.matches()) {
+                                               state = 5;
+                                               seg = m.group(2);
+                                       } else {
+                                               // This is a single date (e.g. 
2002/01/01)
+                                               l.add(new 
TimestampRange(startDate));
+                                               state = 1;
+                                       }
+                               } else if (state == 5) {
+                                       l.add(new TimestampRange(startDate, 
parseDate(seg, pp)));
+                                       seg = 
seg.substring(pp.getIndex()).trim();
+                                       pp.setIndex(0);
+                                       state = 1;
+                               }
+                       }
+
+                       ranges = l.toArray(new TimestampRange[l.size()]);
+               }
+
+               public boolean matches(Calendar c) {
+                       if (ranges.length == 0) return true;
+                       for (int i = 0; i < ranges.length; i++)
+                               if (ranges[i].matches(c))
+                                       return true;
+                       return false;
+               }
+       }
+
+       /**
+        * A construct representing a single search range in a single search 
pattern.
+        * All possible forms of search patterns are boiled down to these 
timestamp ranges.
+        */
+       private static class TimestampRange {
+               Calendar start;
+               Calendar end;
+
+               public TimestampRange(CalendarP start, CalendarP end) {
+                       this.start = start.copy().roll(MILLISECOND, 
-1).getCalendar();
+                       this.end = end.roll(1).getCalendar();
+               }
+
+               public TimestampRange(CalendarP singleDate) {
+                       this.start = singleDate.copy().roll(MILLISECOND, 
-1).getCalendar();
+                       this.end = singleDate.roll(1).getCalendar();
+               }
+
+               public TimestampRange(String op, CalendarP singleDate) {
+                       if (op.equals(">")) {
+                               this.start = 
singleDate.roll(1).roll(MILLISECOND, -1).getCalendar();
+                               this.end = new CalendarP(new 
Date(Long.MAX_VALUE), 0).getCalendar();
+                       } else if (op.equals("<")) {
+                               this.start = new CalendarP(new Date(0), 
0).getCalendar();
+                               this.end = singleDate.getCalendar();
+                       } else if (op.equals(">=")) {
+                               this.start = singleDate.roll(MILLISECOND, 
-1).getCalendar();
+                               this.end = new CalendarP(new 
Date(Long.MAX_VALUE), 0).getCalendar();
+                       } else if (op.equals("<=")) {
+                               this.start = new CalendarP(new Date(0), 
0).getCalendar();
+                               this.end = singleDate.roll(1).getCalendar();
+                       }
+               }
+
+               public boolean matches(Calendar c) {
+                       boolean b = (c.after(start) && c.before(end));
+                       return b;
+               }
+       }
+
+       private static int getPrecisionField(String pattern) {
+               if (pattern.indexOf('s') != -1)
+                       return SECOND;
+               if (pattern.indexOf('m') != -1)
+                       return MINUTE;
+               if (pattern.indexOf('H') != -1)
+                       return HOUR_OF_DAY;
+               if (pattern.indexOf('d') != -1)
+                       return DAY_OF_MONTH;
+               if (pattern.indexOf('M') != -1)
+                       return MONTH;
+               if (pattern.indexOf('y') != -1)
+                       return YEAR;
+               return Calendar.MILLISECOND;
+       }
+
+
+       /**
+        * Parses a timestamp string off the beginning of the string segment 
'seg'.
+        * Goes through each possible valid timestamp format until it finds a 
match.
+        * The position where the parsing left off is stored in pp.
+        * @param seg The string segment being parsed.
+        * @param pp Where parsing last left off.
+        * @return An object represening a timestamp.
+        */
+       protected CalendarP parseDate(String seg, ParsePosition pp) {
+
+               CalendarP cal = null;
+
+               for (int i = 0; i < validTimestampFormats.length && cal == 
null; i++) {
+                       pp.setIndex(0);
+                       SimpleDateFormat f = validTimestampFormats[i];
+                       Date d = f.parse(seg, pp);
+                       int idx = pp.getIndex();
+                       if (idx != 0) {
+                               // it only counts if the next character is '-', 
'space', or end-of-string.
+                               char c = (seg.length() == idx ? 0 : 
seg.charAt(idx));
+                               if (c == 0 || c == '-' || 
Character.isWhitespace(c))
+                                       cal = new CalendarP(d, 
getPrecisionField(f.toPattern()));
+                       }
+               }
+
+               if (cal == null) throw new RuntimeException("Invalid date 
encountered:  ["+seg+"]");
+
+               return cal;
+       }
+
+       /**
+        * Combines a Calendar with a precision identifier.
+        */
+       private static class CalendarP {
+               public Calendar c;
+               public int precision;
+
+               public CalendarP(Date date, int precision) {
+                       c = Calendar.getInstance();
+                       c.setTime(date);
+                       this.precision = precision;
+               }
+
+               public CalendarP copy() {
+                       return new CalendarP(c.getTime(), precision);
+               }
+
+               public CalendarP roll(int field, int amount) {
+                       c.add(field, amount);
+                       return this;
+               }
+
+               public CalendarP roll(int amount) {
+                       return roll(precision, amount);
+               }
+
+               public Calendar getCalendar() {
+                       return c;
+               }
+       }
+
+       
//====================================================================================================
+       // StringMatcher
+       
//====================================================================================================
+       private static class StringMatcher implements IMatcher<Object> {
+
+               private SearchPattern[] searchPatterns;
+
+               /**
+                * Construct a string matcher for the given search pattern.
+                * @param searchPattern The search pattern.  See class usage 
for details.
+                * @param ignoreCase If <jk>true</jk>, use case-insensitive 
matching.
+                */
+               public StringMatcher(String searchPattern, boolean ignoreCase) {
+                       this.searchPatterns = new SearchPattern[1];
+                       this.searchPatterns[0] = new 
SearchPattern(searchPattern, ignoreCase);
+               }
+
+               /**
+                * Returns 'true' if this string matches the pattern(s).
+                * Always returns false on null input.
+                */
+               @Override /* IMatcher */
+               public boolean matches(Object in) {
+                       if (in == null) return false;
+                       for (int i = 0; i < searchPatterns.length; i++) {
+                               if (! searchPatterns[i].matches(in.toString()))
+                                       return false;
+                       }
+                       return true;
+               }
+
+       }
+       /**
+        * A construct representing a single search pattern.
+        */
+       private static class SearchPattern {
+               Pattern[] orPatterns, andPatterns, notPatterns;
+
+               public SearchPattern(String searchPattern, boolean ignoreCase) {
+
+                       List<Pattern> ors = new LinkedList<Pattern>();
+                       List<Pattern> ands = new LinkedList<Pattern>();
+                       List<Pattern> nots = new LinkedList<Pattern>();
+
+                       for (String arg : breakUpTokens(searchPattern)) {
+                               char prefix = arg.charAt(0);
+                               String token = arg.substring(1);
+
+                               token = 
token.replaceAll("([\\?\\*\\+\\\\\\[\\]\\{\\}\\(\\)\\^\\$\\.])", "\\\\$1");
+                               token = token.replace("\u9997", ".*");
+                               token = token.replace("\u9996", ".?");
+
+                               if (! token.startsWith(".*"))
+                                       token = "^" + token;
+                               if (! token.endsWith(".*"))
+                                       token = token + "$";
+
+                               int flags = Pattern.DOTALL;
+                               if (ignoreCase)
+                                       flags |= Pattern.CASE_INSENSITIVE;
+
+                               Pattern p = Pattern.compile(token, flags);
+
+                               if (prefix == '^')
+                                       ors.add(p);
+                               else if (prefix == '+')
+                                       ands.add(p);
+                               else if (prefix == '-')
+                                       nots.add(p);
+                       }
+                       orPatterns = ors.toArray(new Pattern[ors.size()]);
+                       andPatterns = ands.toArray(new Pattern[ands.size()]);
+                       notPatterns = nots.toArray(new Pattern[nots.size()]);
+               }
+
+               /**
+                * Break up search pattern into separate tokens.
+                */
+               private List<String> breakUpTokens(String s) {
+
+                       // If the string is null or all whitespace, return an 
empty vector.
+                       if (s == null || s.trim().length() == 0)
+                               return Collections.emptyList();
+
+                       // Pad with spaces.
+                       s = " " + s + " ";
+
+                       // Replace instances of [+] and [-] inside single and 
double quotes with
+                       // \u2001 and \u2002 for later replacement.
+                       int escapeCount = 0;
+                       boolean inSingleQuote = false;
+                       boolean inDoubleQuote = false;
+                       char[] ca = s.toCharArray();
+                       for (int i = 0; i < ca.length; i++) {
+                               if (ca[i] == '\\') escapeCount++;
+                               else if (escapeCount % 2 == 0) {
+                                       if (ca[i] == '\'') inSingleQuote = ! 
inSingleQuote;
+                                       else if (ca[i] == '"') inDoubleQuote = 
! inDoubleQuote;
+                                       else if (ca[i] == '+' && (inSingleQuote 
|| inDoubleQuote)) ca[i] = '\u9999';
+                                       else if (ca[i] == '-' && (inSingleQuote 
|| inDoubleQuote)) ca[i] = '\u9998';
+                               }
+                               if (ca[i] != '\\') escapeCount = 0;
+                       }
+                       s = new String(ca);
+
+                       // Remove spaces between '+' or '-' and the keyword.
+                       //s = perl5Util.substitute("s/([\\+\\-])\\s+/$1/g", s);
+                       s = s.replaceAll("([\\+\\-])\\s+", "$1");
+
+                       // Replace:  [*]->[\u3001] as placeholder for '%', 
ignore escaped.
+                       s = replace(s, '*', '\u9997', true);
+                       // Replace:  [?]->[\u3002] as placeholder for '_', 
ignore escaped.
+                       s = replace(s, '?', '\u9996', true);
+                       // Replace:  [\*]->[*], [\?]->[?]
+                       s = unEscapeChars(s, new char[]{'*','?'});
+
+                       // Remove spaces
+                       s = s.trim();
+
+                       // Re-replace the [+] and [-] characters inside quotes.
+                       s = s.replace('\u9999', '+');
+                       s = s.replace('\u9998', '-');
+
+                       String[] sa = splitQuoted(s, ' ');
+                       List<String> l = new ArrayList<String>(sa.length);
+                       int numOrs = 0;
+                       for (int i = 0; i < sa.length; i++) {
+                               String token = sa[i];
+                               int len = token.length();
+                               if (len > 0) {
+                                       char c = token.charAt(0);
+                                       String s2 = null;
+                                       if ((c == '+' || c == '-') && len > 1)
+                                               s2 = token.substring(1);
+                                       else {
+                                               s2 = token;
+                                               c = '^';
+                                               numOrs++;
+                                       }
+                                       // Trim off leading and trailing single 
and double quotes.
+                                       if (s2.matches("\".*\"") || 
s2.matches("'.*'"))
+                                               s2 = s2.substring(1, 
s2.length()-1);
+
+                                       // Replace:  [\"]->["]
+                                       s2 = unEscapeChars(s2, new 
char[]{'"','\''});
+
+                                       // Un-escape remaining escaped 
backslashes.
+                                       s2 = unEscapeChars(s2, new 
char[]{'\\'});
+
+                                       l.add(c + s2);
+                               }
+                       }
+
+                       // If there's a single OR clause, turn it into an AND 
clause (makes the SQL cleaner).
+                       if (numOrs == 1) {
+                               int ii = l.size();
+                               for (int i = 0; i < ii; i++) {
+                                       String x = l.get(i);
+                                       if (x.charAt(0) == '^')
+                                               l.set(i, '+'+x.substring(1));
+                               }
+                       }
+                       return l;
+               }
+
+               public boolean matches(String input) {
+                       if (input == null) return false;
+                       for (int i = 0; i < andPatterns.length; i++)
+                               if (! andPatterns[i].matcher(input).matches())
+                                       return false;
+                       for (int i = 0; i < notPatterns.length; i++)
+                               if (notPatterns[i].matcher(input).matches())
+                                       return false;
+                       for (int i = 0; i < orPatterns.length; i++)
+                               if (orPatterns[i].matcher(input).matches())
+                                       return true;
+                       return orPatterns.length == 0;
+               }
+
+       }
+
+       /**
+        * Same as split(String, char), but does not split on characters inside
+        * single quotes.
+        * Does not split on escaped delimiters, and escaped quotes are also 
ignored.
+        * Example:
+        * split("a,b,c",',') -> {"a","b","c"}
+        * split("a,'b,b,b',c",',') -> {"a","'b,b,b'","c"}
+        */
+       private static String[] splitQuoted(String s, char c) {
+
+               if (s == null || s.matches("\\s*"))
+                       return new String[0];
+
+               List<String> l = new LinkedList<String>();
+               char[] sArray = s.toCharArray();
+               int x1 = 0;
+               int escapeCount = 0;
+               boolean inSingleQuote = false;
+               boolean inDoubleQuote = false;
+               for (int i = 0; i < sArray.length; i++) {
+                       if (sArray[i] == '\\') escapeCount++;
+                       else if (escapeCount % 2 == 0) {
+                               if (sArray[i] == '\'' && ! inDoubleQuote) 
inSingleQuote = ! inSingleQuote;
+                               else if (sArray[i] == '"' && ! inSingleQuote) 
inDoubleQuote = ! inDoubleQuote;
+                               else if (sArray[i] == c && ! inSingleQuote && ! 
inDoubleQuote) {
+                                       String s2 = new String(sArray, x1, 
i-x1).trim();
+                                       l.add(s2);
+                                       x1 = i+1;
+                               }
+                       }
+                       if (sArray[i] != '\\') escapeCount = 0;
+               }
+               String s2 = new String(sArray, x1, sArray.length-x1).trim();
+               l.add(s2);
+
+               return l.toArray(new String[l.size()]);
+       }
+
+       /**
+        * Replaces tokens in a string with a different token.
+        * replace("A and B and C", "and", "or") -> "A or B or C"
+        * replace("andandand", "and", "or") -> "ororor"
+        * replace(null, "and", "or") -> null
+        * replace("andandand", null, "or") -> "andandand"
+        * replace("andandand", "", "or") -> "andandand"
+        * replace("A and B and C", "and", null) -> "A  B  C"
+        * @param ignoreEscapedChars Specify 'true' if escaped 'from' 
characters should be ignored.
+        */
+       static String replace(String s, char from, char to, boolean 
ignoreEscapedChars) {
+               if (s == null) return null;
+
+               char[] sArray = s.toCharArray();
+
+               int escapeCount = 0;
+               int singleQuoteCount = 0;
+               int doubleQuoteCount = 0;
+               for (int i = 0; i < sArray.length; i++) {
+                       char c = sArray[i];
+                       if (c == '\\' && ignoreEscapedChars)
+                               escapeCount++;
+                       else if (escapeCount % 2 == 0) {
+                               if (c == from && singleQuoteCount % 2 == 0 && 
doubleQuoteCount % 2 == 0)
+                               sArray[i] = to;
+                       }
+                       if (sArray[i] != '\\') escapeCount = 0;
+               }
+               return new String(sArray);
+       }
+
+       /**
+        * Removes escape characters (specified by escapeChar) from the 
specified characters.
+        */
+       static String unEscapeChars(String s, char[] toEscape) {
+               char escapeChar = '\\';
+               if (s == null) return null;
+               if (s.length() == 0) return s;
+               StringBuffer sb = new StringBuffer(s.length());
+               char[] sArray = s.toCharArray();
+               for (int i = 0; i < sArray.length; i++) {
+                       char c = sArray[i];
+
+                       if (c == escapeChar) {
+                               if (i+1 != sArray.length) {
+                                       char c2 = sArray[i+1];
+                                       boolean isOneOf = false;
+                                       for (int j = 0; j < toEscape.length && 
! isOneOf; j++)
+                                               isOneOf = (c2 == toEscape[j]);
+                                       if (isOneOf) {
+                                               i++;
+                                       } else if (c2 == escapeChar) {
+                                               sb.append(escapeChar);
+                                               i++;
+                                       }
+                               }
+                       }
+                       sb.append(sArray[i]);
+               }
+               return sb.toString();
+       }
+}
+
+

Reply via email to