You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2016/08/01 00:07:55 UTC
[06/51] [partial] incubator-juneau git commit: Initial Juno contents
from IBM JazzHub repo
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$MapMatcher.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$MapMatcher.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$MapMatcher.class
new file mode 100755
index 0000000..e1303c6
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$MapMatcher.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberMatcher.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberMatcher.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberMatcher.class
new file mode 100755
index 0000000..d67f612
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberMatcher.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberPattern.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberPattern.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberPattern.class
new file mode 100755
index 0000000..25153ec
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberPattern.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberRange.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberRange.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberRange.class
new file mode 100755
index 0000000..127742c
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$NumberRange.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$ObjectMatcher.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$ObjectMatcher.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$ObjectMatcher.class
new file mode 100755
index 0000000..f826750
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$ObjectMatcher.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$SearchPattern.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$SearchPattern.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$SearchPattern.class
new file mode 100755
index 0000000..eead3cd
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$SearchPattern.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$StringMatcher.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$StringMatcher.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$StringMatcher.class
new file mode 100755
index 0000000..65b68c5
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$StringMatcher.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampPattern.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampPattern.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampPattern.class
new file mode 100755
index 0000000..98258f3
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampPattern.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampRange.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampRange.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampRange.class
new file mode 100755
index 0000000..4256f45
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery$TimestampRange.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.class
new file mode 100755
index 0000000..e27b8bf
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.java
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.java b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.java
new file mode 100755
index 0000000..4010e64
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoQuery.java
@@ -0,0 +1,1246 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * � Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ * The source code for this program is not published or otherwise
+ * divested of its trade secrets, irrespective of what has been
+ * deposited with the U.S. Copyright Office.
+ *******************************************************************************/
+package com.ibm.juno.core.utils;
+
+import static java.util.Calendar.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+import com.ibm.juno.core.*;
+
+/**
+ * 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>
+ * <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>
+ * <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>
+ * <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>> 100</tt> - Greater than 100
+ * <li><tt>>= 100</tt> - Greater than or equal to 100
+ * <li><tt>!123</tt> - Not 123
+ * </ul>
+ *
+ * <h6 class='topic'>Notes</h6>
+ * <ul>
+ * <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>>2001</tt> - After a specific year.
+ * <li><tt>>=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>
+ * <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 (jbognar@us.ibm.com)
+ */
+@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();
+ }
+}
+
+
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest$JsonNode.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest$JsonNode.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest$JsonNode.class
new file mode 100755
index 0000000..32b73b8
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest$JsonNode.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.class
new file mode 100755
index 0000000..3daabb9
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.java
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.java b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.java
new file mode 100755
index 0000000..6923d65
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRest.java
@@ -0,0 +1,843 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ * The source code for this program is not published or otherwise
+ * divested of its trade secrets, irrespective of what has been
+ * deposited with the U.S. Copyright Office.
+ *******************************************************************************/
+package com.ibm.juno.core.utils;
+
+import static java.net.HttpURLConnection.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import com.ibm.juno.core.*;
+import com.ibm.juno.core.json.*;
+import com.ibm.juno.core.parser.*;
+
+/**
+ * Provides the ability to perform standard REST operations (GET, PUT, POST, DELETE) against
+ * nodes in a POJO model. Nodes in the POJO model are addressed using URLs.
+ * <p>
+ * A POJO model is defined as a tree model where nodes consist of consisting of the following:
+ * <ul>
+ * <li>{@link Map Maps} and Java beans representing JSON objects.
+ * <li>{@link Collection Collections} and arrays representing JSON arrays.
+ * <li>Java beans.
+ * </ul>
+ * <p>
+ * Leaves of the tree can be any type of object.
+ * <p>
+ * Use {@link #get(String) get()} to retrieve an element from a JSON tree.<br>
+ * Use {@link #put(String,Object) put()} to create (or overwrite) an element in a JSON tree.<br>
+ * Use {@link #post(String,Object) post()} to add an element to a list in a JSON tree.<br>
+ * Use {@link #delete(String) delete()} to remove an element from a JSON tree.<br>
+ * <p>
+ * Leading slashes in URLs are ignored. So <js>"/xxx/yyy/zzz"</js> and <js>"xxx/yyy/zzz"</js> are considered identical.
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ * <jc>// Construct an unstructured POJO model</jc>
+ * ObjectMap m = <jk>new</jk> ObjectMap(<js>""</js>
+ * + <js>"{"</js>
+ * + <js>" name:'John Smith', "</js>
+ * + <js>" address:{ "</js>
+ * + <js>" streetAddress:'21 2nd Street', "</js>
+ * + <js>" city:'New York', "</js>
+ * + <js>" state:'NY', "</js>
+ * + <js>" postalCode:10021 "</js>
+ * + <js>" }, "</js>
+ * + <js>" phoneNumbers:[ "</js>
+ * + <js>" '212 555-1111', "</js>
+ * + <js>" '212 555-2222' "</js>
+ * + <js>" ], "</js>
+ * + <js>" additionalInfo:null, "</js>
+ * + <js>" remote:false, "</js>
+ * + <js>" height:62.4, "</js>
+ * + <js>" 'fico score':' > 640' "</js>
+ * + <js>"} "</js>
+ * );
+ *
+ * <jc>// Wrap Map inside a PojoRest object</jc>
+ * PojoRest johnSmith = <jk>new</jk> PojoRest(m);
+ *
+ * <jc>// Get a simple value at the top level</jc>
+ * <jc>// "John Smith"</jc>
+ * String name = johnSmith.getString(<js>"name"</js>);
+ *
+ * <jc>// Change a simple value at the top level</jc>
+ * johnSmith.put(<js>"name"</js>, <js>"The late John Smith"</js>);
+ *
+ * <jc>// Get a simple value at a deep level</jc>
+ * <jc>// "21 2nd Street"</jc>
+ * String streetAddress = johnSmith.getString(<js>"address/streetAddress"</js>);
+ *
+ * <jc>// Set a simple value at a deep level</jc>
+ * johnSmith.put(<js>"address/streetAddress"</js>, <js>"101 Cemetery Way"</js>);
+ *
+ * <jc>// Get entries in a list</jc>
+ * <jc>// "212 555-1111"</jc>
+ * String firstPhoneNumber = johnSmith.getString(<js>"phoneNumbers/0"</js>);
+ *
+ * <jc>// Add entries to a list</jc>
+ * johnSmith.post(<js>"phoneNumbers"</js>, <js>"212 555-3333"</js>);
+ *
+ * <jc>// Delete entries from a model</jc>
+ * johnSmith.delete(<js>"fico score"</js>);
+ *
+ * <jc>// Add entirely new structures to the tree</jc>
+ * ObjectMap medicalInfo = new ObjectMap(<js>""</js>
+ * + <js>"{"</js>
+ * + <js>" currentStatus: 'deceased',"</js>
+ * + <js>" health: 'non-existent',"</js>
+ * + <js>" creditWorthiness: 'not good'"</js>
+ * + <js>"}"</js>
+ * );
+ * johnSmith.put(<js>"additionalInfo/medicalInfo"</js>, medicalInfo);
+ * <p>
+ * In the special case of collections/arrays of maps/beans, a special XPath-like selector notation
+ * can be used in lieu of index numbers on GET requests to return a map/bean with a specified attribute value.<br>
+ * The syntax is {@code @attr=val}, where attr is the attribute name on the child map, and val is the matching value.
+ *
+ * <h6 class='topic'>Examples</h6>
+ * <p class='bcode'>
+ * <jc>// Get map/bean with name attribute value of 'foo' from a list of items</jc>
+ * Map m = pojoRest.getMap(<js>"/items/@name=foo"</js>);
+ * </p>
+ *
+ * @author James Bognar (jbognar@us.ibm.com)
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public final class PojoRest {
+
+ /** The list of possible request types. */
+ private static final int GET=1, PUT=2, POST=3, DELETE=4;
+
+ private ReaderParser parser = JsonParser.DEFAULT;
+ private final BeanContext bc;
+
+ /** If true, the root cannot be overwritten */
+ private boolean rootLocked = false;
+
+ /** The root of the model. */
+ private JsonNode root;
+
+ /**
+ * Create a new instance of a REST interface over the specified object.
+ * <p>
+ * Uses {@link BeanContext#DEFAULT} for working with Java beans.
+ *
+ * @param o The object to be wrapped.
+ */
+ public PojoRest(Object o) {
+ this(o, null);
+ }
+
+ /**
+ * Create a new instance of a REST interface over the specified object.
+ * <p>
+ * The parser is used as the bean context.
+ *
+ * @param o The object to be wrapped.
+ * @param parser The parser to use for parsing arguments and converting objects to the correct data type.
+ */
+ public PojoRest(Object o, ReaderParser parser) {
+ if (parser == null)
+ parser = JsonParser.DEFAULT;
+ this.parser = parser;
+ this.bc = parser.getBeanContext();
+ this.root = new JsonNode(null, null, o, bc.object());
+ }
+
+ /**
+ * Call this method to prevent the root object from being overwritten on put("", xxx); calls.
+ *
+ * @return This object (for method chaining).
+ */
+ public PojoRest setRootLocked() {
+ this.rootLocked = true;
+ return this;
+ }
+
+ /**
+ * The root object that was passed into the constructor of this method.
+ *
+ * @return The root object.
+ */
+ public Object getRootObject() {
+ return root.o;
+ }
+
+ /**
+ * Retrieves the element addressed by the URL.
+ *
+ * @param url The URL of the element to retrieve.
+ * If null or blank, returns the root.
+ * @return The addressed element, or null if that element does not exist in the tree.
+ */
+ public Object get(String url) {
+ return get(url, null);
+ }
+
+ /**
+ * Retrieves the element addressed by the URL.
+ *
+ * @param url The URL of the element to retrieve.
+ * If null or blank, returns the root.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The addressed element, or null if that element does not exist in the tree.
+ */
+ public Object get(String url, Object defVal) {
+ Object o = service(GET, url, null);
+ return o == null ? defVal : o;
+ }
+
+ /**
+ * Retrieves the element addressed by the URL as the specified object type.
+ * <p>
+ * Will convert object to the specified type per {@link BeanContext#convertToType(Object, ClassMeta)}.
+ *
+ * @param type The specified object type.
+ * @param url The URL of the element to retrieve.
+ * If null or blank, returns the root.
+ * @param <T> The specified object type.
+ *
+ * @return The addressed element, or null if that element does not exist in the tree.
+ */
+ public <T> T get(Class<T> type, String url) {
+ return get(type, url, null);
+ }
+
+ /**
+ * Retrieves the element addressed by the URL as the specified object type.
+ * <p>
+ * Will convert object to the specified type per {@link BeanContext#convertToType(Object, ClassMeta)}.
+ *
+ * @param type The specified object type.
+ * @param url The URL of the element to retrieve.
+ * If null or blank, returns the root.
+ * @param def The default value if addressed item does not exist.
+ * @param <T> The specified object type.
+ *
+ * @return The addressed element, or null if that element does not exist in the tree.
+ */
+ public <T> T get(Class<T> type, String url, T def) {
+ Object o = service(GET, url, null);
+ if (o == null)
+ return def;
+ return bc.convertToType(o, type);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link String}.
+ * <p>
+ * Shortcut for <code>get(String.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ */
+ public String getString(String url) {
+ return get(String.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link String}.
+ * <p>
+ * Shortcut for <code>get(String.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ */
+ public String getString(String url, String defVal) {
+ return get(String.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to an {@link Integer}.
+ * <p>
+ * Shortcut for <code>get(Integer.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Integer getInt(String url) {
+ return get(Integer.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to an {@link Integer}.
+ * <p>
+ * Shortcut for <code>get(Integer.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Integer getInt(String url, Integer defVal) {
+ return get(Integer.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Long}.
+ * <p>
+ * Shortcut for <code>get(Long.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Long getLong(String url) {
+ return get(Long.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Long}.
+ * <p>
+ * Shortcut for <code>get(Long.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Long getLong(String url, Long defVal) {
+ return get(Long.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Boolean}.
+ * <p>
+ * Shortcut for <code>get(Boolean.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Boolean getBoolean(String url) {
+ return get(Boolean.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Boolean}.
+ * <p>
+ * Shortcut for <code>get(Boolean.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Boolean getBoolean(String url, Boolean defVal) {
+ return get(Boolean.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Map}.
+ * <p>
+ * Shortcut for <code>get(Map.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Map<?,?> getMap(String url) {
+ return get(Map.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Map}.
+ * <p>
+ * Shortcut for <code>get(Map.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public Map<?,?> getMap(String url, Map<?,?> defVal) {
+ return get(Map.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link List}.
+ * <p>
+ * Shortcut for <code>get(List.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public List<?> getList(String url) {
+ return get(List.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link List}.
+ * <p>
+ * Shortcut for <code>get(List.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public List<?> getList(String url, List<?> defVal) {
+ return get(List.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link Map}.
+ * <p>
+ * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public ObjectMap getObjectMap(String url) {
+ return get(ObjectMap.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link ObjectMap}.
+ * <p>
+ * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public ObjectMap getObjectMap(String url, ObjectMap defVal) {
+ return get(ObjectMap.class, url, defVal);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link ObjectList}.
+ * <p>
+ * Shortcut for <code>get(ObjectList.<jk>class</jk>, key)</code>.
+ *
+ * @param url The key.
+ * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public ObjectList getObjectList(String url) {
+ return get(ObjectList.class, url);
+ }
+
+ /**
+ * Returns the specified entry value converted to a {@link ObjectList}.
+ * <p>
+ * Shortcut for <code>get(ObjectList.<jk>class</jk>, key, defVal)</code>.
+ *
+ * @param url The key.
+ * @param defVal The default value if the map doesn't contain the specified mapping.
+ * @return The converted value, or the default value if the map contains no mapping for this key.
+ * @throws InvalidDataConversionException If value cannot be converted.
+ */
+ public ObjectList getObjectList(String url, ObjectList defVal) {
+ return get(ObjectList.class, url, defVal);
+ }
+
+ /**
+ * Executes the specified method with the specified parameters on the specified object.
+ *
+ * @param url The URL of the element to retrieve.
+ * @param method The method signature.
+ * <p>
+ * Can be any of the following formats:
+ * </p>
+ * <ul>
+ * <li>Method name only. e.g. <js>"myMethod"</js>.
+ * <li>Method name with class names. e.g. <js>"myMethod(String,int)"</js>.
+ * <li>Method name with fully-qualified class names. e.g. <js>"myMethod(java.util.String,int)"</js>.
+ * </ul>
+ * <p>
+ * As a rule, use the simplest format needed to uniquely resolve a method.
+ * </p>
+ * @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>
+ * This must be an array, like a JSON array.
+ * @return The returned object from the method call.
+ * @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>
+ * <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 NoSuchMethodException
+ * @throws IOException
+ */
+ public Object invokeMethod(String url, String method, String args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, ParseException, NoSuchMethodException, IOException {
+ return new PojoIntrospector(get(url), parser).invokeMethod(method, args);
+ }
+
+ /**
+ * Returns the list of available methods that can be passed to the {@link #invokeMethod(String, String, String)} for the object
+ * addressed by the specified URL.
+ *
+ * @param url The URL.
+ * @return The list of methods.
+ */
+ public Collection<String> getPublicMethods(String url) {
+ Object o = get(url);
+ if (o == null)
+ return null;
+ return bc.getClassMeta(o.getClass()).getPublicMethods().keySet();
+ }
+
+ /**
+ * Returns the class type of the object at the specified URL.
+ *
+ * @param url The URL.
+ * @return The class type.
+ */
+ public ClassMeta getClassMeta(String url) {
+ JsonNode n = getNode(normalizeUrl(url), root);
+ if (n == null)
+ return null;
+ return n.cm;
+ }
+
+ /**
+ * Sets/replaces the element addressed by the URL.
+ * <p>
+ * This method expands the POJO model as necessary to create the new element.
+ *
+ * @param url The URL of the element to create.
+ * If <jk>null</jk> or blank, the root itself is replaced with the specified value.
+ * @param val The value being set. Value can be of any type.
+ * @return The previously addressed element, or <jk>null</jk> the element did not previously exist.
+ */
+ public Object put(String url, Object val) {
+ return service(PUT, url, val);
+ }
+
+ /**
+ * Adds a value to a list element in a POJO model.
+ * <p>
+ * The URL is the address of the list being added to.
+ * <p>
+ * If the list does not already exist, it will be created.
+ * <p>
+ * This method expands the POJO model as necessary to create the new element.
+ * <p>
+ * Note: You can only post to three types of nodes:
+ * <ul>
+ * <li>{@link List Lists}
+ * <li>{@link Map Maps} containing integers as keys (i.e sparse arrays)
+ * <li>arrays
+ * </ul>
+ *
+ * @param url The URL of the element being added to.
+ * If null or blank, the root itself (assuming it's one of the types specified above) is added to.
+ * @param val The value being added.
+ * @return The URL of the element that was added.
+ */
+ public String post(String url, Object val) {
+ return (String)service(POST, url, val);
+ }
+
+ /**
+ * Remove an element from a POJO model.
+ * <p>
+ * qIf the element does not exist, no action is taken.
+ *
+ * @param url The URL of the element being deleted.
+ * If <jk>null</jk> or blank, the root itself is deleted.
+ * @return The removed element, or null if that element does not exist.
+ */
+ public Object delete(String url) {
+ return service(DELETE, url, null);
+ }
+
+ @Override /* Object */
+ public String toString() {
+ return String.valueOf(root.o);
+ }
+
+ /** Handle nulls and strip off leading '/' char. */
+ private String normalizeUrl(String url) {
+
+ // Interpret nulls and blanks the same (i.e. as addressing the root itself)
+ if (url == null)
+ url = "";
+
+ // Strip off leading slash if present.
+ if (url.length() > 0 && url.charAt(0) == '/')
+ url = url.substring(1);
+
+ return url;
+ }
+
+
+ /*
+ * Workhorse method.
+ */
+ private Object service(int method, String url, Object val) throws PojoRestException {
+
+ url = normalizeUrl(url);
+
+ if (method == GET) {
+ JsonNode p = getNode(url, root);
+ return p == null ? null : p.o;
+ }
+
+ // Get the url of the parent and the property name of the addressed object.
+ int i = url.lastIndexOf('/');
+ String parentUrl = (i == -1 ? null : url.substring(0, i));
+ String childKey = (i == -1 ? url : url.substring(i + 1));
+
+ if (method == PUT) {
+ if (url.length() == 0) {
+ if (rootLocked)
+ throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+ Object o = root.o;
+ root = new JsonNode(null, null, val, bc.object());
+ return o;
+ }
+ JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root));
+ if (n == null)
+ throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", parentUrl);
+ ClassMeta cm = n.cm;
+ Object o = n.o;
+ if (cm.isMap())
+ return ((Map)o).put(childKey, convert(val, cm.getValueType()));
+ if (cm.isCollection() && o instanceof List)
+ return ((List)o).set(parseInt(childKey), convert(val, cm.getElementType()));
+ if (cm.isArray()) {
+ o = setArrayEntry(n.o, parseInt(childKey), val, cm.getElementType());
+ ClassMeta pct = n.parent.cm;
+ Object po = n.parent.o;
+ if (pct.isMap()) {
+ ((Map)po).put(n.keyName, o);
+ return url;
+ }
+ if (pct.isBean()) {
+ BeanMap m = bc.forBean(po);
+ m.put(n.keyName, o);
+ return url;
+ }
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct);
+ }
+ if (cm.isBean())
+ return bc.forBean(o).put(childKey, val);
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+ }
+
+ if (method == POST) {
+ // Handle POST to root special
+ if (url.length() == 0) {
+ ClassMeta cm = root.cm;
+ Object o = root.o;
+ if (cm.isCollection()) {
+ Collection c = (Collection)o;
+ c.add(convert(val, cm.getElementType()));
+ return (c instanceof List ? url + "/" + (c.size()-1) : null);
+ }
+ if (cm.isArray()) {
+ Object[] o2 = addArrayEntry(o, val, cm.getElementType());
+ root = new JsonNode(null, null, o2, null);
+ return url + "/" + (o2.length-1);
+ }
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
+ }
+ JsonNode n = getNode(url, root);
+ if (n == null)
+ throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url);
+ ClassMeta cm = n.cm;
+ Object o = n.o;
+ if (cm.isArray()) {
+ Object[] o2 = addArrayEntry(o, val, cm.getElementType());
+ ClassMeta pct = n.parent.cm;
+ Object po = n.parent.o;
+ if (pct.isMap()) {
+ ((Map)po).put(childKey, o2);
+ return url + "/" + (o2.length-1);
+ }
+ if (pct.isBean()) {
+ BeanMap m = bc.forBean(po);
+ m.put(childKey, o2);
+ return url + "/" + (o2.length-1);
+ }
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+ }
+ if (cm.isCollection()) {
+ Collection c = (Collection)o;
+ c.add(convert(val, cm.getElementType()));
+ return (c instanceof List ? url + "/" + (c.size()-1) : null);
+ }
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
+ }
+
+ if (method == DELETE) {
+ if (url.length() == 0) {
+ if (rootLocked)
+ throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+ Object o = root.o;
+ root = new JsonNode(null, null, null, bc.object());
+ return o;
+ }
+ JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root));
+ ClassMeta cm = n.cm;
+ Object o = n.o;
+ if (cm.isMap())
+ return ((Map)o).remove(childKey);
+ if (cm.isCollection() && o instanceof List)
+ return ((List)o).remove(parseInt(childKey));
+ if (cm.isArray()) {
+ int index = parseInt(childKey);
+ Object old = ((Object[])o)[index];
+ Object[] o2 = removeArrayEntry(o, index);
+ ClassMeta pct = n.parent.cm;
+ Object po = n.parent.o;
+ if (pct.isMap()) {
+ ((Map)po).put(n.keyName, o2);
+ return old;
+ }
+ if (pct.isBean()) {
+ BeanMap m = bc.forBean(po);
+ m.put(n.keyName, o2);
+ return old;
+ }
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+ }
+ if (cm.isBean())
+ return bc.forBean(o).put(childKey, null);
+ throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+ }
+
+ return null; // Never gets here.
+ }
+
+ private Object[] setArrayEntry(Object o, int index, Object val, ClassMeta componentType) {
+ Object[] a = (Object[])o;
+ if (a.length <= index) {
+ // Expand out the array.
+ Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), index+1);
+ System.arraycopy(a, 0, a2, 0, a.length);
+ a = a2;
+ }
+ a[index] = convert(val, componentType);
+ return a;
+ }
+
+ private Object[] addArrayEntry(Object o, Object val, ClassMeta componentType) {
+ Object[] a = (Object[])o;
+ // Expand out the array.
+ Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length+1);
+ System.arraycopy(a, 0, a2, 0, a.length);
+ a2[a.length] = convert(val, componentType);
+ return a2;
+ }
+
+ private Object[] removeArrayEntry(Object o, int index) {
+ Object[] a = (Object[])o;
+ // Shrink the array.
+ Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length-1);
+ System.arraycopy(a, 0, a2, 0, index);
+ System.arraycopy(a, index+1, a2, index, a.length-index-1);
+ return a2;
+ }
+
+ class JsonNode {
+ Object o;
+ ClassMeta cm;
+ JsonNode parent;
+ String keyName;
+
+ JsonNode(JsonNode parent, String keyName, Object o, ClassMeta cm) {
+ this.o = o;
+ this.keyName = keyName;
+ this.parent = parent;
+ if (cm == null || cm.isObject()) {
+ if (o == null)
+ cm = bc.object();
+ else
+ cm = bc.getClassMetaForObject(o);
+ }
+ this.cm = cm;
+ }
+ }
+
+ JsonNode getNode(String url, JsonNode n) {
+ if (url == null || url.isEmpty())
+ return n;
+ int i = url.indexOf('/');
+ String parentKey, childUrl = null;
+ if (i == -1) {
+ parentKey = url;
+ } else {
+ parentKey = url.substring(0, i);
+ childUrl = url.substring(i + 1);
+ }
+
+ Object o = n.o;
+ Object o2 = null;
+ ClassMeta cm = n.cm;
+ ClassMeta ct2 = null;
+ if (o == null)
+ return null;
+ if (cm.isMap()) {
+ o2 = ((Map)o).get(parentKey);
+ ct2 = cm.getValueType();
+ } else if (cm.isCollection() && o instanceof List) {
+ int key = parseInt(parentKey);
+ List l = ((List)o);
+ if (l.size() <= key)
+ return null;
+ o2 = l.get(key);
+ ct2 = cm.getElementType();
+ } else if (cm.isArray()) {
+ int key = parseInt(parentKey);
+ Object[] a = ((Object[])o);
+ if (a.length <= key)
+ return null;
+ o2 = a[key];
+ ct2 = cm.getElementType();
+ } else if (cm.isBean()) {
+ BeanMap m = bc.forBean(o);
+ o2 = m.get(parentKey);
+ BeanPropertyMeta pMeta = m.getPropertyMeta(parentKey);
+ if (pMeta == null)
+ throw new PojoRestException(HTTP_BAD_REQUEST,
+ "Unknown property ''{0}'' encountered while trying to parse into class ''{1}''",
+ parentKey, m.getClassMeta()
+ );
+ ct2 = pMeta.getClassMeta();
+ }
+
+ if (childUrl == null)
+ return new JsonNode(n, parentKey, o2, ct2);
+
+ return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2));
+ }
+
+ private Object convert(Object in, ClassMeta cm) {
+ if (cm == null)
+ return in;
+ if (cm.isBean() && in instanceof Map)
+ return bc.convertToType(in, cm);
+ return in;
+ }
+
+ private int parseInt(String key) {
+ try {
+ return Integer.parseInt(key);
+ } catch (NumberFormatException e) {
+ throw new PojoRestException(HTTP_BAD_REQUEST,
+ "Cannot address an item in an array with a non-integer key ''{0}''", key
+ );
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.class
new file mode 100755
index 0000000..a9c026e
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.java
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.java b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.java
new file mode 100755
index 0000000..86e3934
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/PojoRestException.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ * The source code for this program is not published or otherwise
+ * divested of its trade secrets, irrespective of what has been
+ * deposited with the U.S. Copyright Office.
+ *******************************************************************************/
+package com.ibm.juno.core.utils;
+
+import java.net.*;
+import java.text.*;
+
+/**
+ * Generic exception thrown from the {@link PojoRest} class.
+ * <p>
+ * Typically, this is a user-error, such as trying to address a non-existent node in the tree.
+ * <p>
+ * The status code is an HTTP-equivalent code. It will be one of the following:
+ * <ul>
+ * <li>{@link HttpURLConnection#HTTP_BAD_REQUEST HTTP_BAD_REQUEST} - Attempting to do something impossible.
+ * <li>{@link HttpURLConnection#HTTP_NOT_FOUND HTTP_NOT_FOUND} - Attempting to access a non-existent node in the tree.
+ * <li>{@link HttpURLConnection#HTTP_FORBIDDEN HTTP_FORBIDDEN} - Attempting to overwrite the root object.
+ * </ul>
+ *
+ * @author James Bognar (jbognar@us.ibm.com)
+ */
+public final class PojoRestException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private int status;
+
+ /**
+ * Constructor.
+ *
+ * @param status The HTTP-equivalent status code.
+ * @param message The detailed message.
+ * @param args Optional message arguments.
+ */
+ public PojoRestException(int status, String message, Object...args) {
+ super(args.length == 0 ? message : MessageFormat.format(message, args));
+ this.status = status;
+ }
+
+ /**
+ * The HTTP-equivalent status code.
+ * <p>
+ * See above for details.
+ *
+ * @return The HTTP-equivalent status code.
+ */
+ public int getStatus() {
+ return status;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$1.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$1.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$1.class
new file mode 100755
index 0000000..72f4d06
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$1.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$2.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$2.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$2.class
new file mode 100755
index 0000000..9f5f469
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$2.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$3.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$3.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$3.class
new file mode 100755
index 0000000..b13996a
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$3.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$4.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$4.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$4.class
new file mode 100755
index 0000000..c75c48a
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$4.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$5.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$5.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$5.class
new file mode 100755
index 0000000..d83e166
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$5.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$Matcher.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$Matcher.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$Matcher.class
new file mode 100755
index 0000000..9521154
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder$Matcher.class differ
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder.class
----------------------------------------------------------------------
diff --git a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder.class b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder.class
new file mode 100755
index 0000000..5083520
Binary files /dev/null and b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/utils/ProcBuilder.class differ