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 2019/03/10 15:52:30 UTC

[juneau] 05/09: Add RestResponse access to HTML widgets.

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit f6e2dc415d8ef486d451e5007b8214cd0637a891
Author: JamesBognar <ja...@apache.org>
AuthorDate: Mon Feb 18 10:39:20 2019 -0500

    Add RestResponse access to HTML widgets.
---
 .../java/org/apache/juneau/config/ConfigTest.java  |    2 +-
 .../apache/juneau/pojotools/PojoSearcherTest.java  | 1056 ++++++++++++++++++++
 .../apache/juneau/pojotools/PojoSorterTest.java    |  265 +++++
 .../apache/juneau/pojotools/PojoViewerTest.java    |  349 +++++++
 .../apache/juneau/internal/DelegateBeanMap.java    |    8 +-
 .../org/apache/juneau/internal/DelegateMap.java    |   19 +-
 .../org/apache/juneau/internal/ObjectUtils.java    |   28 +
 .../apache/juneau/internal/StateMachineState.java  |   31 +-
 .../org/apache/juneau/internal/StringUtils.java    |   76 +-
 .../java/org/apache/juneau/pojotools/Equality.java |   33 +-
 .../java/org/apache/juneau/pojotools/Matcher.java  |   30 +-
 .../apache/juneau/pojotools/MatcherFactory.java    |   35 +-
 .../juneau/pojotools/NumberMatcherFactory.java     |  291 ++++++
 .../apache/juneau/pojotools/PatternException.java  |   29 +-
 .../org/apache/juneau/pojotools/PojoPaginator.java |   71 ++
 .../org/apache/juneau/pojotools/PojoSearcher.java  |  185 ++++
 .../org/apache/juneau/pojotools/PojoSorter.java    |  110 ++
 .../java/org/apache/juneau/pojotools/PojoTool.java |   31 +-
 .../org/apache/juneau/pojotools/PojoViewer.java    |   80 ++
 .../{utils/ASet.java => pojotools/SearchArgs.java} |   73 +-
 .../DelegateMap.java => pojotools/SortArgs.java}   |  138 +--
 .../juneau/pojotools/StringMatcherFactory.java     |  145 +++
 .../juneau/pojotools/TimeMatcherFactory.java       |  463 +++++++++
 .../java/org/apache/juneau/pojotools/ViewArgs.java |   59 +-
 .../main/java/org/apache/juneau/utils/ASet.java    |    1 +
 .../java/org/apache/juneau/utils/PojoQuery.java    |    2 +-
 juneau-doc/docs/ReleaseNotes/8.0.1.html            |    2 +
 .../rest/petstore/rest/AddOrderMenuItem.java       |    6 +-
 .../rest/petstore/rest/AddPetMenuItem.java         |    4 +-
 .../rest/petstore/rest/UploadPhotoMenuItem.java    |    4 +-
 .../apache/juneau/rest/BasicRestCallHandler.java   |    3 +-
 .../org/apache/juneau/rest/RestCallHandler.java    |    3 +-
 .../java/org/apache/juneau/rest/RestContext.java   |    6 +-
 .../java/org/apache/juneau/rest/RestRequest.java   |    3 +-
 .../org/apache/juneau/rest/annotation/HtmlDoc.java |    6 +-
 .../org/apache/juneau/rest/vars/RequestVar.java    |    5 +
 .../org/apache/juneau/rest/vars/WidgetVar.java     |    8 +-
 .../juneau/rest/widget/ContentTypeMenuItem.java    |    4 +-
 .../apache/juneau/rest/widget/MenuItemWidget.java  |   32 +-
 .../apache/juneau/rest/widget/PoweredByApache.java |    2 +-
 .../apache/juneau/rest/widget/PoweredByJuneau.java |    2 +-
 .../apache/juneau/rest/widget/QueryMenuItem.java   |    8 +-
 .../apache/juneau/rest/widget/ThemeMenuItem.java   |    4 +-
 .../java/org/apache/juneau/rest/widget/Widget.java |   18 +-
 44 files changed, 3414 insertions(+), 316 deletions(-)

diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigTest.java
index 9555f64..a0d0ed8 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigTest.java
@@ -1742,7 +1742,7 @@ public class ConfigTest {
 		assertObjectEquals("['foo.txt']", Config.getCandidateSystemDefaultConfigNames());
 
 		System.clearProperty("juneau.configFile");
-		assertObjectEquals("['test.cfg','juneau.cfg','default.cfg']", Config.getCandidateSystemDefaultConfigNames());
+		assertObjectEquals("['test.cfg','juneau.cfg','default.cfg','application.cfg','app.cfg','settings.cfg']", Config.getCandidateSystemDefaultConfigNames());
 	}
 
 }
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSearcherTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSearcherTest.java
new file mode 100755
index 0000000..8374d58
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSearcherTest.java
@@ -0,0 +1,1056 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.junit.Assert.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.transforms.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+/**
+ * Tests the PojoSearcher class.
+ */
+public class PojoSearcherTest {
+
+	private static BeanSession bs = BeanContext.DEFAULT.createSession();
+	private static PojoSearcher ps = PojoSearcher.DEFAULT;
+	private static WriterSerializer ws = JsonSerializer.create().ssq().pojoSwaps(CalendarSwap.DateTimeSimple.class).build();
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Utility
+	//-----------------------------------------------------------------------------------------------------------------
+
+	static SearchArgs[] create(String...search) {
+		SearchArgs[] sa = new SearchArgs[search.length];
+		for (int i = 0; i < search.length; i++)
+			sa[i] = new SearchArgs(search[i]);
+		return sa;
+	}
+
+	static SearchArgs create(String search) {
+		return new SearchArgs(search);
+	}
+
+	static Object run(Object in, String search) {
+		return ps.run(bs, in, create(search));
+	}
+
+	static Object run(Object in, SearchArgs sa) {
+		return ps.run(bs, in, sa);
+	}
+
+	static String[] a(String...s) {
+		return s;
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// String search
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class A {
+		public String f;
+
+		public static A create(String f) {
+			A a = new A();
+			a.f = f;
+			return a;
+		}
+	}
+
+	public static List<A> A_LIST = AList.create(A.create("foo"), A.create("bar"), A.create("baz"), A.create("q ux"), A.create("qu'ux"), null, A.create(null));
+	public static Set<A> A_SET = ASet.create(A.create("foo"), A.create("bar"), A.create("baz"), A.create("q ux"), A.create("qu'ux"), null, A.create(null));
+	public static A[] A_ARRAY = new A[]{A.create("foo"), A.create("bar"), A.create("baz"), A.create("q ux"), A.create("qu'ux"), null, A.create(null)};
+
+	@Test
+	public void stringSearch_singleWord() throws Exception {
+		assertObjectEquals("[{f:'foo'}]", run(A_LIST, "f=foo"));
+		assertObjectEquals("[{f:'foo'}]", run(A_SET, "f=foo"));
+		assertObjectEquals("[{f:'foo'}]", run(A_ARRAY, "f=foo"));
+	}
+
+	@Test
+	public void stringSearch_pattern1() throws Exception {
+		assertObjectEquals("[{f:'foo'}]", run(A_LIST, "f=fo*"));
+		assertObjectEquals("[{f:'foo'}]", run(A_SET, "f=fo*"));
+		assertObjectEquals("[{f:'foo'}]", run(A_ARRAY, "f=fo*"));
+	}
+
+	@Test
+	public void stringSearch_pattern2() throws Exception {
+		assertObjectEquals("[{f:'bar'}]", run(A_LIST, "f=*ar"));
+		assertObjectEquals("[{f:'bar'}]", run(A_SET, "f=*ar"));
+		assertObjectEquals("[{f:'bar'}]", run(A_ARRAY, "f=*ar"));
+	}
+
+	@Test
+	public void stringSearch_pattern3() throws Exception {
+		assertObjectEquals("[{f:'bar'}]", run(A_LIST, "f=?ar"));
+		assertObjectEquals("[{f:'bar'}]", run(A_SET, "f=?ar"));
+		assertObjectEquals("[{f:'bar'}]", run(A_ARRAY, "f=?ar"));
+	}
+
+	@Test
+	public void stringSearch_multiple() throws Exception {
+		assertObjectEquals("[{f:'foo'},{f:'bar'}]", run(A_LIST, "f=foo bar q ux"));
+		assertObjectEquals("[{f:'foo'},{f:'bar'}]", run(A_SET, "f=foo bar q ux"));
+		assertObjectEquals("[{f:'foo'},{f:'bar'}]", run(A_ARRAY, "f=foo bar q ux"));
+	}
+
+	@Test
+	public void stringSearch_quoted() throws Exception {
+		assertObjectEquals("[{f:'q ux'}]", run(A_LIST, "f='q ux'"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_SET, "f='q ux'"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_ARRAY, "f='q ux'"));
+	}
+
+	@Test
+	public void stringSearch_quotedWithPattern() throws Exception {
+		assertObjectEquals("[{f:'q ux'}]", run(A_LIST, "f='q *x'"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_SET, "f='q *x'"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_ARRAY, "f='q *x'"));
+	}
+
+	@Test
+	public void stringSearch_unquotedContainingQuote() throws Exception {
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_LIST, "f=qu'ux"));
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_SET, "f=qu'ux"));
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_ARRAY, "f=qu'ux"));
+	}
+
+	@Test
+	public void stringSearch_quotedContainingQuote() throws Exception {
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_LIST, "f='qu\\'ux'"));
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_SET, "f='qu\\'ux'"));
+		assertObjectEquals("[{f:'qu\\'ux'}]", run(A_ARRAY, "f='qu\\'ux'"));
+	}
+
+	@Test
+	public void stringSearch_regExp() throws Exception {
+		assertObjectEquals("[{f:'q ux'}]", run(A_LIST, "f=/q\\sux/"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_SET, "f=/q\\sux/"));
+		assertObjectEquals("[{f:'q ux'}]", run(A_ARRAY, "f=/q\\sux/"));
+	}
+
+	@Test
+	public void stringSearch_regExp_noEndSlash() throws Exception {
+		Object in = AList.create(A.create("/foo"), A.create("bar"));
+		for (String s : a("f=/foo","f='/foo'")) // Not a regex.
+			assertObjectEquals("[{f:'/foo'}]", run(in, s));
+	}
+
+	@Test
+	public void stringSearch_regExp_onlySlash() throws Exception {
+		Object in = AList.create(A.create("/"), A.create("bar"));
+		for (String s : a("f=/", "f='/'")) // Not a regex.
+			assertObjectEquals("[{f:'/'}]", run(in, s));
+	}
+
+	@Test
+	public void stringSearch_or_pattern() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'foo'},{f:'bar'}]", run(in, "f=f* *r"));
+		assertObjectEquals("[]", run(in, "f='f* *r'"));
+		assertObjectEquals("[{f:'foo'}]", run(in, "f='f*oo'"));
+	}
+
+	@Test
+	public void stringSearch_explicit_or_pattern() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'foo'},{f:'bar'}]", run(in, "f=^f* ^*r"));
+		assertObjectEquals("[]", run(in, "f=^'f* *r'"));
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=^'f*oo'"));
+	}
+
+	@Test
+	public void stringSearch_and_pattern() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'bar'}]", run(in, "f=+b* +*r"));
+		assertObjectEquals("[{f:'bar'}]", run(in, "f=+'b*' +'*r'"));
+	}
+
+	@Test
+	public void stringSearch_not_pattern() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'baz'}]", run(in, "f=b* -*r"));
+		assertObjectEquals("[{f:'baz'}]", run(in, "f=+'b*' -'*r'"));
+	}
+
+	@Test
+	public void stringSearch_caseSensitive() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[]", run(in, "f=F*"));
+		assertObjectEquals("[]", run(in, "f=\"F*\""));
+		assertObjectEquals("[{f:'foo'}]", run(in, "f='F*'"));
+	}
+
+	@Test
+	public void stringSearch_malformedQuotes() throws Exception {
+		Object in = AList.create(A.create("'foo"), A.create("\"bar"), A.create("baz"));
+
+		try {
+			run(in, "f='*");
+			fail();
+		} catch (Exception e) {
+			assertTrue(e.getLocalizedMessage().contains("Unmatched string quotes"));
+		}
+
+		try {
+			run(in, "f=\"*");
+			fail();
+		} catch (Exception e) {
+			assertTrue(e.getLocalizedMessage().contains("Unmatched string quotes"));
+		}
+
+		assertObjectEquals("[{f:'\\'foo'}]", run(in, "f='\\'*'"));
+		assertObjectEquals("[{f:'\"bar'}]", run(in, "f='\"*'"));
+		assertObjectEquals("[{f:'\"bar'}]", run(in, "f=\"\\\"*\""));
+	}
+
+	@Test
+	public void stringSearch_regexChars() throws Exception {
+		Object in = AList.create(A.create("+\\[]{}()^$."), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'+\\\\[]{}()^$.'}]", run(in, "f=*+*"));
+		assertObjectEquals("[{f:'+\\\\[]{}()^$.'}]", run(in, "f='+\\\\[]{}()^$.'"));
+		assertObjectEquals("[{f:'+\\\\[]{}()^$.'}]", run(in, "f=++\\\\[]{}()^$."));
+	}
+
+	@Test
+	public void stringSearch_metaChars() throws Exception {
+		Object in = AList.create(A.create("*?\\'\""), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'*?\\\\\\'\"'}]", run(in, "f='\\*\\?\\\\\\'\"'"));
+	}
+
+	@Test
+	public void stringSearch_metaChars_escapedQuotes() throws Exception {
+		Object in = AList.create(A.create("'"), A.create("\""), A.create("baz"));
+		assertObjectEquals("[{f:'\\''}]", run(in, "f=\\'"));
+		assertObjectEquals("[{f:'\"'}]", run(in, "f=\\\""));
+	}
+
+	@Test
+	public void stringSearch_metaChars_falseEscape() throws Exception {
+		Object in = AList.create(A.create("foo"), A.create("bar"), A.create("baz"));
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=\\f\\o\\o"));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Number search
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class C {
+		public int f;
+
+		static C create(int f) {
+			C c = new C();
+			c.f = f;
+			return c;
+		}
+	}
+
+	C[] INT_BEAN_ARRAY = new C[]{C.create(-2), C.create(-1), C.create(0), C.create(1), C.create(2), C.create(3)};
+
+	@Test
+	public void intSearch_oneNumber() throws Exception {
+		for (String s : a("f=1", "f = 1"))
+			assertObjectEquals("[{f:1}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_twoNumbers() throws Exception {
+		for (String s : a("f=1 2", "f = 1  2 "))
+			assertObjectEquals("[{f:1},{f:2}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_oneNegativeNumber() throws Exception {
+		for (String s : a("f=-1", "f = -1 "))
+			assertObjectEquals("[{f:-1}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_twoNegativeNumbers() throws Exception {
+		assertObjectEquals("[{f:-2},{f:-1}]", run(INT_BEAN_ARRAY, "f=-1 -2"));
+	}
+
+	@Test
+	public void intSearch_simpleRange() throws Exception {
+		for (String s : a("f=1-2", "f = 1 - 2 ", "f = 1- 2 "))
+			assertObjectEquals("[{f:1},{f:2}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_simpleRange_invalid() throws Exception {
+		assertObjectEquals("[]", run(INT_BEAN_ARRAY, "f=2-1"));
+	}
+
+	@Test
+	public void intSearch_twoNumbersThatLookLikeRange() throws Exception {
+		assertObjectEquals("[{f:-2},{f:1}]", run(INT_BEAN_ARRAY, "f = 1 -2 "));
+	}
+
+	@Test
+	public void intSearch_rangeWithNegativeNumbers() throws Exception {
+		assertObjectEquals("[{f:-2},{f:-1}]", run(INT_BEAN_ARRAY, "f = -2--1 "));
+	}
+
+	@Test
+	public void intSearch_rangeWithNegativeNumbers_invalidRange() throws Exception {
+		assertObjectEquals("[]", run(INT_BEAN_ARRAY, "f = -1--2 "));
+	}
+
+	@Test
+	public void intSearch_multipleRanges() throws Exception {
+		assertObjectEquals("[{f:0},{f:1},{f:3}]", run(INT_BEAN_ARRAY, "f = 0-1 3-4"));
+	}
+
+	@Test
+	public void intSearch_overlappingRanges() throws Exception {
+		assertObjectEquals("[{f:0},{f:2}]", run(INT_BEAN_ARRAY, "f = 0-0 2-2"));
+	}
+
+	@Test
+	public void intSearch_LT() throws Exception {
+		for (String s : a("f = <0", "f<0", "f = < 0 ", "f < 0 "))
+			assertObjectEquals("[{f:-2},{f:-1}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_LT_negativeNumber() throws Exception {
+		for (String s : a("f = <-1", "f<-1", "f = < -1 ", "f < -1 "))
+			assertObjectEquals("[{f:-2}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_GT() throws Exception {
+		for (String s : a("f = >1", "f>1", "f = > 1 ", "f > 1 "))
+			assertObjectEquals("[{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_GT_negativeNumber() throws Exception {
+		for (String s : a("f = >-1", "f>-1", "f = > -1 ", "f > -1 ", "f =  >  -1  ", "f >  -1  "))
+			assertObjectEquals("[{f:0},{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_LTE() throws Exception {
+		for (String s : a("f = <=0", "f<=0", "f = <= 0 ", "f <= 0 ", "f =  <=  0  "))
+			assertObjectEquals("[{f:-2},{f:-1},{f:0}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_LTE_negativeNumber() throws Exception {
+		for (String s : a("f = <=-1", "f <=-1", "f = <= -1 ", "f =  <=  -1  ", "f <=  -1  "))
+			assertObjectEquals("[{f:-2},{f:-1}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_GTE() throws Exception {
+		for (String s : a("f = >=1", "f >=1", "f = >= 1 ", "f >= 1 ", "f =  >=  1  "))
+			assertObjectEquals("[{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_GTE_negativeNumber() throws Exception {
+		for (String s : a("f = >=-1", "f >=-1", "f = >= -1 ", "f >= -1 ", "f =  >=  -1  "))
+			assertObjectEquals("[{f:-1},{f:0},{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_not_singleNumber() throws Exception {
+		for (String s : a("f = !1", "f = ! 1 ", "f =  !  1  "))
+			assertObjectEquals("[{f:-2},{f:-1},{f:0},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_not_range() throws Exception {
+		assertObjectEquals("[{f:-2},{f:-1},{f:0},{f:3}]", run(INT_BEAN_ARRAY, "f = !1-2"));
+	}
+
+	@Test
+	public void intSearch_not_range_negativeNumbers() throws Exception {
+		for (String s : a("f = !-2--1", "f = ! -2 - -1", "f =  !  -2  -  -1 "))
+			assertObjectEquals("[{f:0},{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_not_looksLikeRange() throws Exception {
+		assertObjectEquals("[{f:-2},{f:-1},{f:0},{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, "f = ! -2 -2"));
+	}
+
+	@Test
+	public void intSearch_empty() throws Exception {
+		for (String s : a("f=", "f = ", "f =  "))
+			assertObjectEquals("[{f:-2},{f:-1},{f:0},{f:1},{f:2},{f:3}]", run(INT_BEAN_ARRAY, s));
+	}
+
+	@Test
+	public void intSearch_badSearches() throws Exception {
+		String[] ss = new String[] {
+			"f=x","(S01)",
+			"f=>x","(S02)",
+			"f=<x","(S03)",
+			"f=>=x","(S04)",
+			"f=>= x","(S05)",
+			"f=1x","(S06)",
+			"f=1 x","(S07)",
+			"f=1-x","(S08)",
+			"f=1 -x","(S09)",
+			"f=1 - x","(S10)",
+			"f=1 - 1x","(S11)",
+			"f=>","(ES02)",
+			"f=<","(ES03)",
+			"f=>=","(ES04)",
+			"f=123-","(ES08)",
+			"f=123 -","(ES09)",
+		};
+
+		for (int i = 0; i < ss.length; i+=2) {
+			try {
+				run(INT_BEAN_ARRAY, ss[i]);
+				fail("i=" + i);
+			} catch (PatternException e) {
+				assertTrue(e.getLocalizedMessage().contains(ss[i+1]));
+			}
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Date search
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class B {
+		public Calendar f;
+
+		static B[] create(String...dates) {
+			B[] bb = new B[dates.length];
+			for (int i = 0; i < dates.length; i++) {
+				bb[i] = new B();
+				bb[i].f = DateUtils.parseISO8601Calendar(dates[i]);
+			}
+			return bb;
+		}
+	}
+
+	@Test
+	public void dateSearch_singleDate_y() throws Exception {
+		B[] in = B.create("2010-01-01", "2011-01-01", "2011-01-31", "2012-01-01");
+		for (String s : a(
+				"f=2011",
+				"f = 2011 ",
+				"f = '2011' ",
+				"f = \"2011\" "
+			))
+			assertObjectEquals("[{f:'2011/01/01 00:00:00'},{f:'2011/01/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_singleDate_ym() throws Exception {
+		B[] in = B.create("2010-01-01", "2011-01-01", "2011-01-31", "2012-01-01");
+		for (String s : a(
+				"f=2011-01",
+				"f = 2011-01 ",
+				"f='2011-01'",
+				"f=\"2011-01\""
+			))
+			assertObjectEquals("[{f:'2011/01/01 00:00:00'},{f:'2011/01/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_singleDate_ymd() throws Exception {
+		B[] in = B.create("2010-01-01", "2011-01-01", "2011-01-31", "2012-01-01");
+		assertObjectEquals("[{f:'2011/01/01 00:00:00'}]", run(in, "f=2011-01-01"), ws);
+	}
+
+
+	@Test
+	public void dateSearch_singleDate_ymdh() throws Exception {
+		B[] in = B.create("2011-01-01T11:15:59", "2011-01-01T12:00:00", "2011-01-01T12:59:59", "2011-01-01T13:00:00");
+		assertObjectEquals("[{f:'2011/01/01 12:00:00'},{f:'2011/01/01 12:59:59'}]", run(in, "f=2011-01-01T12"), ws);
+	}
+
+	@Test
+	public void dateSearch_singleDate_ymdhm() throws Exception {
+		B[] in = B.create("2011-01-01T12:29:59", "2011-01-01T12:30:00", "2011-01-01T12:30:59", "2011-01-01T12:31:00");
+		assertObjectEquals("[{f:'2011/01/01 12:30:00'},{f:'2011/01/01 12:30:59'}]", run(in, "f=2011-01-01T12:30"), ws);
+	}
+
+	@Test
+	public void dateSearch_singleDate_ymdhms() throws Exception {
+		B[] in = B.create("2011-01-01T12:30:29", "2011-01-01T12:30:30", "2011-01-01T12:30:31");
+		assertObjectEquals("[{f:'2011/01/01 12:30:30'}]", run(in, "f=2011-01-01T12:30:30"), ws);
+	}
+
+	@Test
+	public void dateSearch_openEndedRanges_y() throws Exception {
+		B[] in = B.create("2000-12-31", "2001-01-01");
+		for (String s : a(
+				"f>2000",
+				"f > 2000 ",
+				"f>'2000'",
+				"f > '2000' ",
+				"f>\"2000\"",
+				"f > \"2000\" ",
+				"f>=2001",
+				"f >= 2001 ",
+				"f>='2001'",
+				"f >= '2001' ",
+				"f>=\"2001\"",
+				"f >= \"2001\" "
+			))
+			assertObjectEquals("[{f:'2001/01/01 00:00:00'}]", run(in, s), ws);
+		for (String s : a(
+				"f<2001",
+				"f < 2001 ",
+				"f<'2001'",
+				"f < '2001'",
+				"f<\"2001\"",
+				"f < \"2001\" ",
+				"f<=2000",
+				"f <= 2000 ",
+				"f<='2000'",
+				"f <= '2000'",
+				"f<=\"2000\"",
+				"f <= \"2000\" "
+			))
+			assertObjectEquals("[{f:'2000/12/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_openEndedRanges_toMinute() throws Exception {
+		B[] in = B.create("2011-01-01T12:29:59", "2011-01-01T12:30:00");
+		assertObjectEquals("[{f:'2011/01/01 12:30:00'}]", run(in, "f>=2011-01-01T12:30"), ws);
+		assertObjectEquals("[{f:'2011/01/01 12:29:59'}]", run(in, "f<2011-01-01T12:30"), ws);
+	}
+
+	@Test
+	public void dateSearch_openEndedRanges_toSecond() throws Exception {
+		B[] in = B.create("2011-01-01T12:30:59", "2011-01-01T12:31:00");
+		assertObjectEquals("[{f:'2011/01/01 12:31:00'}]", run(in, "f>2011-01-01T12:30"), ws);
+		assertObjectEquals("[{f:'2011/01/01 12:30:59'}]", run(in, "f<=2011-01-01T12:30"), ws);
+	}
+
+	@Test
+	public void dateSearch_closedRanges() throws Exception {
+		B[] in = B.create("2000-12-31T23:59:59", "2001-01-01T00:00:00", "2003-06-30T23:59:59", "2003-07-01T00:00:00");
+
+		for (String s : a(
+				"f= 2001 - 2003-06-30 ",
+				"f= 2001 - 2003-06-30",
+				"f='2001'-'2003-06-30'",
+				"f= '2001' - '2003-06-30' ",
+				"f=\"2001\"-\"2003-06-30\"",
+				"f= \"2001\" - \"2003-06-30\" ",
+				"f=2001 -'2003-06-30'",
+				"f= 2001 - '2003-06-30' ",
+				"f=2001 -\"2003-06-30\"",
+				"f= 2001 - \"2003-06-30\" "
+			))
+			assertObjectEquals("[{f:'2001/01/01 00:00:00'},{f:'2003/06/30 23:59:59'}]", run(in, s), ws);
+
+		for (String s : a(
+			"f= 2001 - 2003-06-30 2000",
+			"f= 2001 - 2003-06-30 '2000'",
+			"f= 2001 - 2003-06-30 \"2000\"",
+			"f='2001'-'2003-06-30' 2000",
+			"f='2001'-'2003-06-30' '2000'",
+			"f='2001'-'2003-06-30' \"2000\"",
+			"f= '2001' - '2003-06-30'  2000",
+			"f= '2001' - '2003-06-30'  '2000'",
+			"f= '2001' - '2003-06-30'  \"2000\"",
+			"f=\"2001\"-\"2003-06-30\" 2000",
+			"f=\"2001\"-\"2003-06-30\" '2000'",
+			"f=\"2001\"-\"2003-06-30\" \"2000\"",
+			"f= \"2001\" - \"2003-06-30\"  2000",
+			"f= \"2001\" - \"2003-06-30\"  '2000'",
+			"f= \"2001\" - \"2003-06-30\"  \"2000\"",
+			"f= 2001 - '2003-06-30'  2000",
+			"f= 2001 - '2003-06-30'  '2000'",
+			"f= 2001 - '2003-06-30'  \"2000\"",
+			"f= 2001 - \"2003-06-30\"  2000",
+			"f= 2001 - \"2003-06-30\"  '2000'",
+			"f= 2001 - \"2003-06-30\"  \"2000\""
+		))
+		assertObjectEquals("[{f:'2000/12/31 23:59:59'},{f:'2001/01/01 00:00:00'},{f:'2003/06/30 23:59:59'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_or1() throws Exception {
+		B[] in = B.create("2000-12-31", "2001-01-01", "2001-12-31", "2002-01-01");
+		for (String s : a(
+				"f=2001 2003 2005",
+				"f= 2001  2003  2005 ",
+				"f='2001' '2003' '2005'",
+				"f= '2001'  '2003'  '2005' ",
+				"f=\"2001\" \"2003\" \"2005\"",
+				"f= \"2001\"  \"2003\"  \"2005\" "
+			))
+			assertObjectEquals("[{f:'2001/01/01 00:00:00'},{f:'2001/12/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_or2() throws Exception {
+		B[] in = B.create("2002-12-31", "2003-01-01", "2003-12-31", "2004-01-01");
+		for (String s : a(
+				"f=2001 2003 2005",
+				"f= 2001  2003  2005 ",
+				"f='2001' '2003' '2005'",
+				"f= '2001'  '2003'  '2005' ",
+				"f=\"2001\" \"2003\" \"2005\"",
+				"f= \"2001\"  \"2003\"  \"2005\" "
+			))
+			assertObjectEquals("[{f:'2003/01/01 00:00:00'},{f:'2003/12/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_or3() throws Exception {
+		B[] in = B.create("2004-12-31", "2005-01-01", "2005-12-31", "2006-01-01");
+		for (String s : a(
+				"f=2001 2003 2005",
+				"f= 2001  2003  2005 ",
+				"f='2001' '2003' '2005'",
+				"f= '2001'  '2003'  '2005' ",
+				"f=\"2001\" \"2003\" \"2005\"",
+				"f= \"2001\"  \"2003\"  \"2005\" "
+			))
+			assertObjectEquals("[{f:'2005/01/01 00:00:00'},{f:'2005/12/31 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_or_singleAndRange() throws Exception {
+		B[] in = B.create("2000-12-31", "2001-01-01", "2002-12-31", "2003-01-01");
+		for (String s : a(
+				"f=2001 >2002",
+				"f= 2001   >2002 ",
+				"f='2001' >'2002'",
+				"f= '2001'  >'2002' ",
+				"f=\"2001\" >\"2002\"",
+				"f= \"2001\"  >\"2002\" ",
+				"f=>2002 2001",
+				"f= >2002  2001 ",
+				"f=>'2002' '2001'",
+				"f= >'2002'  '2001' ",
+				"f=>\"2002\" \"2001\"",
+				"f= >\"2002\"  \"2001\" ",
+				"f=2001 >=2003",
+				"f= 2001  >=2003 ",
+				"f='2001' >='2003'",
+				"f= '2001'  >='2003' ",
+				"f=\"2001\" >=\"2003\"",
+				"f= \"2001\"  >=\"2003\" ",
+				"f=>=2003 2001",
+				"f= >=2003  2001 ",
+				"f=>='2003' '2001'",
+				"f= >='2003'  '2001' ",
+				"f=>=\"2003\" \"2001\"",
+				"f= >=\"2003\"  \"2001\" "
+			))
+			assertObjectEquals("[{f:'2001/01/01 00:00:00'},{f:'2003/01/01 00:00:00'}]", run(in, s), ws);
+		for (String s : a(
+				"f=<2001 2003",
+				"f= <2001  2003 ",
+				"f=<'2001' '2003'",
+				"f= <'2001'  '2003' ",
+				"f=<\"2001\" \"2003\"",
+				"f= <\"2001\"  \"2003\" ",
+				"f=2003 <2001",
+				"f= 2003  <2001 ",
+				"f='2003' <'2001'",
+				"f= '2003'  <'2001' ",
+				"f=\"2003\" <\"2001\"",
+				"f= \"2003\"  <\"2001\" ",
+				"f=<=2000 2003",
+				"f= <=2000  2003 ",
+				"f=<='2000' '2003'",
+				"f= <='2000'  '2003' ",
+				"f=<=\"2000\" \"2003\"",
+				"f= <=\"2000\"  \"2003\" ",
+				"f=2003 <=2000",
+				"f= 2003  <=2000 ",
+				"f='2003' <='2000'",
+				"f= '2003'  <='2000' ",
+				"f=\"2003\" <=\"2000\"",
+				"f= \"2003\"  <=\"2000\" "
+			))
+			assertObjectEquals("[{f:'2000/12/31 00:00:00'},{f:'2003/01/01 00:00:00'}]", run(in, s), ws);
+	}
+
+	@Test
+	public void dateSearch_badSearches() throws Exception {
+		B[] in = B.create("2000-12-31");
+		String[] ss = new String[] {
+			"f=X","(S01)",
+			"f=>X","(S02)",
+			"f=<X","(S03)",
+			"f=>=X","(S04)",
+			"f='1'X","(S07)",
+			"f=2000 X","(S09)",
+			"f=2000-X","(S10)",
+			"f=>","(ES02)",
+			"f=<","(ES03)",
+			"f=>=","(ES04)",
+			"f='","(ES05)",
+			"f=\"","(ES06)",
+			"f=2000-","(ES10)",
+			"f=2000-'","(ES11)",
+			"f=2000-\"","(ES12)"
+		};
+
+		for (int i = 0; i < ss.length; i+=2) {
+			try {
+				run(in, ss[i]);
+			} catch (PatternException e) {
+				assertTrue("["+e.getLocalizedMessage()+"]!=["+ss[i]+"]", e.getLocalizedMessage().contains(ss[i+1]));
+			}
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other data structures.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void d2ListOfMaps() throws Exception {
+		List<Map<?,?>> in = AList.create(
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null)
+		);
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2SetOfMaps() throws Exception {
+		Set<Map<?,?>> in = ASet.create(
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null)
+		);
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+
+	@Test
+	public void d2ArrayOfMaps() throws Exception {
+		Map<?,?>[] in = new Map[]{
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null)
+		};
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2ListOfObjects() throws Exception {
+		List<Object> in = AList.create(
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null),
+			"xxx",
+			123
+		);
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2SetOfObjects() throws Exception {
+		Set<Object> in = ASet.create(
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null),
+			"xxx",
+			123
+		);
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2ArrayOfObjects() throws Exception {
+		Object[] in = new Object[]{
+			AMap.create("f","foo"),
+			AMap.create("f","bar"),
+			null,
+			AMap.create(null,"qux"),
+			AMap.create("quux",null),
+			AMap.create(null,null),
+			"xxx",
+			123
+		};
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2ListOfMapsWithLists() throws Exception {
+		List<Map<?,?>> in = AList.create(
+			AMap.create("f",AList.create("foo")),
+			AMap.create("f",AList.create("bar")),
+			null,
+			AMap.create(null,AList.create("qux")),
+			AMap.create("quux",AList.create((Object)null)),
+			AMap.create(null,AList.create((Object)null))
+		);
+		assertObjectEquals("[{f:['foo']}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2SetOfMapsWithSets() throws Exception {
+		Set<Map<?,?>> in = ASet.create(
+			AMap.create("f",ASet.create("foo")),
+			AMap.create("f",ASet.create("bar")),
+			null,
+			AMap.create(null,ASet.create("qux")),
+			AMap.create("quux",ASet.create((Object)null)),
+			AMap.create(null,ASet.create((Object)null))
+		);
+		assertObjectEquals("[{f:['foo']}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2ArrayOfMapsWithArrays() throws Exception {
+		Map<?,?>[] in = new Map[]{
+			AMap.create("f",new Object[]{"foo"}),
+			AMap.create("f",new Object[]{"bar"}),
+			null,
+			AMap.create(null,new Object[]{"qux"}),
+			AMap.create("quux",new Object[]{null}),
+			AMap.create(null,new Object[]{null})
+		};
+		assertObjectEquals("[{f:['foo']}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d2ListOfBeans() throws Exception {
+		List<A> in = AList.create(
+			A.create("foo"),
+			A.create("bar"),
+			null,
+			A.create(null)
+		);
+		assertObjectEquals("[{f:'foo'}]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ListOfListOfMaps() throws Exception {
+		List<List<Map<?,?>>> in = AList.create(
+			AList.create(AMap.create("f","foo")),
+			AList.create(AMap.create("f","bar")),
+			AList.create((Map<?,?>)null),
+			AList.create(AMap.create(null,"qux")),
+			AList.create(AMap.create("quux",null)),
+			AList.create(AMap.create(null,null)),
+			null
+		);
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3SetOfSetOfMaps() throws Exception {
+		Set<Set<Map<?,?>>> in = ASet.create(
+			ASet.create(AMap.create("f","foo")),
+			ASet.create(AMap.create("f","bar")),
+			ASet.create(AMap.create("f","baz")),
+			ASet.create((Map<?,?>)null),
+			ASet.create(AMap.create(null,"qux")),
+			ASet.create(AMap.create("quux",null)),
+			ASet.create(AMap.create(null,null)),
+			null
+		);
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ArrayOfArrayOfMaps() throws Exception {
+		Map<?,?>[][] in = new Map[][]{
+			new Map[]{AMap.create("f","foo")},
+			new Map[]{AMap.create("f","bar")},
+			new Map[]{AMap.create("f","baz")},
+			new Map[]{null},
+			new Map[]{AMap.create(null,"qux")},
+			new Map[]{AMap.create("quux",null)},
+			new Map[]{AMap.create(null,null)},
+			null
+		};
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ListOfListOfObjects() throws Exception {
+		List<List<Object>> in = AList.create(
+			AList.create(AMap.create("f","foo")),
+			AList.create(AMap.create("f","bar")),
+			AList.create((Object)null),
+			AList.create(AMap.create(null,"qux")),
+			AList.create(AMap.create("quux",null)),
+			AList.create(AMap.create(null,null)),
+			AList.create("xxx"),
+			AList.create(123),
+			null
+		);
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3SetOfSetOfObjects() throws Exception {
+		Set<Set<Object>> in = ASet.create(
+			ASet.create(AMap.create("f","foo")),
+			ASet.create(AMap.create("f","bar")),
+			ASet.create((Map<?,?>)null),
+			ASet.create(AMap.create(null,"qux")),
+			ASet.create(AMap.create("quux",null)),
+			ASet.create(AMap.create(null,null)),
+			ASet.create("xxx"),
+			ASet.create(123),
+			null
+		);
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ArrayOfArrayOfObjects() throws Exception {
+		Object[][] in = new Object[][]{
+			new Object[]{AMap.create("f","foo")},
+			new Object[]{AMap.create("f","bar")},
+			new Object[]{null},
+			new Object[]{AMap.create(null,"qux")},
+			new Object[]{AMap.create("quux",null)},
+			new Object[]{AMap.create(null,null)},
+			new Object[]{"xxx"},
+			new Object[]{123},
+			null
+		};
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ListOfListOfMapsWithCollections() throws Exception {
+		List<List<Map<?,?>>> in = AList.create(
+			AList.create(AMap.create("f",AList.create("foo"))),
+			AList.create(AMap.create("f",AList.create("bar"))),
+			AList.create((Map<?,?>)null),
+			AList.create(AMap.create(null,AList.create("qux"))),
+			AList.create(AMap.create("quux",AList.create((Object)null))),
+			AList.create(AMap.create(null,AList.create((Object)null))),
+			null
+		);
+		assertObjectEquals("[[{f:['foo']}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3SetOfSetOfMapsWithCollections() throws Exception {
+		Set<Set<Map<?,?>>> in = ASet.create(
+			ASet.create(AMap.create("f",ASet.create("foo"))),
+			ASet.create(AMap.create("f",ASet.create("bar"))),
+			ASet.create((Map<?,?>)null),
+			ASet.create(AMap.create(null,ASet.create("qux"))),
+			ASet.create(AMap.create("quux",ASet.create((Object)null))),
+			ASet.create(AMap.create(null,ASet.create((Object)null))),
+			null
+		);
+		assertObjectEquals("[[{f:['foo']}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ArrayOfArrayOfMapsWithCollections() throws Exception {
+		Map<?,?>[][] in = new Map[][]{
+			new Map[]{AMap.create("f",new Object[]{"foo"})},
+			new Map[]{AMap.create("f",new Object[]{"bar"})},
+			new Map[]{null},
+			new Map[]{AMap.create(null,new Object[]{"qux"})},
+			new Map[]{AMap.create("quux",new Object[]{null})},
+			new Map[]{AMap.create(null,new Object[]{null})},
+			null
+		};
+		assertObjectEquals("[[{f:['foo']}]]", run(in, "f=foo"));
+	}
+
+	@Test
+	public void d3ArrayOfArrayOfBeans() throws Exception {
+		A[][] in = new A[][]{
+			new A[]{A.create("foo")},
+			new A[]{A.create("bar")},
+			new A[]{null},
+			new A[]{A.create(null)},
+			null
+		};
+		assertObjectEquals("[[{f:'foo'}]]", run(in, "f=foo"));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other tests
+	//-----------------------------------------------------------------------------------------------------------------
+
+//	@Test
+//	public void noSearchArgs() {
+//		SearchArgs sa = new SearchArgs();
+//		assertObjectEquals("'foo'", run("foo", sa));
+//	}
+//
+//	@Test
+//	public void invalidSearchArgs() {
+//		for (String s : a("", "x")) {
+//			try {
+//				run("foo", s);
+//				fail();
+//			} catch (PatternException e) {
+//				assertTrue(e.getLocalizedMessage().contains("Invalid search terms"));
+//			}
+//		}
+//		SearchArgs sa = new SearchArgs();
+//		assertObjectEquals("'foo'", run("foo", sa));
+//	}
+//
+//	@Test
+//	public void not2dPojo() {
+//		assertObjectEquals("'foo'", run("foo", "x=y"));
+//	}
+//
+//	@Test
+//	public void nullInput() {
+//		assertObjectEquals("null", run(null, "x=y"));
+//	}
+//
+//	@Test
+//	public void searchArgsEmptyKey() {
+//		SearchArgs sa = new SearchArgs().append(null, "foo");
+//		assertObjectEquals("'foo'", run("foo", sa));
+//	}
+//
+//	@Test
+//	public void searchArgsEmptyValue() {
+//		SearchArgs sa = new SearchArgs().append("foo", null);
+//		assertObjectEquals("'foo'", run("foo", sa));
+//	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSorterTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSorterTest.java
new file mode 100644
index 0000000..c164fc3
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoSorterTest.java
@@ -0,0 +1,265 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.junit.Assert.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+/**
+ * Tests the PojoPaginator class.
+ */
+public class PojoSorterTest {
+
+	PojoSorter p = new PojoSorter();
+	BeanSession bs = BeanContext.DEFAULT.createBeanSession();
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Null input
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void nullInput() {
+		assertNull(p.run(bs, null, null));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Arrays
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class A {
+		public String f;
+
+		public static A create(String f) {
+			A a = new A();
+			a.f = f;
+			return a;
+		}
+	}
+
+	@Test
+	public void beanArray() {
+		Object in = new A[]{A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d")};
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[{f:'a'},{f:'b'},{f:'c'},{f:'d'},{f:'e'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_reverse() {
+		Object in = new A[]{A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d")};
+		SortArgs sa = new SortArgs("f-");
+		assertObjectEquals("[{f:'e'},{f:'d'},{f:'c'},{f:'b'},{f:'a'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArrayContainingNulls() {
+		Object in = new A[]{A.create("c"),A.create("a"),null,null,A.create("b")};;
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,null,{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArrayContainingDups() {
+		Object in = new A[]{A.create("c"),A.create("a"),null,A.create("a"),A.create("b")};
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,{f:'a'},{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Lists
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanList() {
+		Object in = AList.create(A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[{f:'a'},{f:'b'},{f:'c'},{f:'d'},{f:'e'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_reverse() {
+		Object in = AList.create(A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d"));
+		SortArgs sa = new SortArgs("f-");
+		assertObjectEquals("[{f:'e'},{f:'d'},{f:'c'},{f:'b'},{f:'a'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanListContainingNull() {
+		Object in = AList.create(A.create("c"),A.create("a"),null,null,A.create("b"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,null,{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanListContainingDups() {
+		Object in = AList.create(A.create("c"),A.create("a"),null,A.create("a"),A.create("b"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,{f:'a'},{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Sets
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanSet() {
+		Object in = ASet.create(A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[{f:'a'},{f:'b'},{f:'c'},{f:'d'},{f:'e'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void betSet_reverse() {
+		Object in = ASet.create(A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d"));
+		SortArgs sa = new SortArgs("f-");
+		assertObjectEquals("[{f:'e'},{f:'d'},{f:'c'},{f:'b'},{f:'a'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSetContainingNull() {
+		Object in = ASet.create(A.create("c"),A.create("a"),null,null,A.create("b"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSetContainingDups() {
+		Object in = ASet.create(A.create("c"),A.create("a"),null,A.create("a"),A.create("b"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[null,{f:'a'},{f:'a'},{f:'b'},{f:'c'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void emptySort() {
+		Object in = ASet.create(A.create("c"),A.create("a"),A.create("b"));
+		SortArgs sa = new SortArgs();
+		assertObjectEquals("[{f:'c'},{f:'a'},{f:'b'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void invalidDataType() {
+		Object in = AMap.create("a","b");
+		SortArgs sa = new SortArgs("x");
+		in = p.run(bs, in, sa);
+		assertObjectEquals("{a:'b'}", in);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Lists of Maps
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void listOfMaps() {
+		Object in = AList.create(AMap.create("f","c"),AMap.create("f","a"),AMap.create("f","b"),AMap.create("f","e"),AMap.create("f","d"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[{f:'a'},{f:'b'},{f:'c'},{f:'d'},{f:'e'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void listOfMaps_reverse() {
+		Object in = AList.create(AMap.create("f","c"),AMap.create("f","a"),AMap.create("f","b"),AMap.create("f","e"),AMap.create("f","d"));
+		SortArgs sa = new SortArgs("f-");
+		assertObjectEquals("[{f:'e'},{f:'d'},{f:'c'},{f:'b'},{f:'a'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Lists of Other
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void listOfOther() {
+		Object in = AList.create(AList.create("c"),AList.create("a"),AList.create("b"));
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[['c'],['a'],['b']]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void listOfOther_reverse() {
+		Object in = AList.create(AList.create("c"),AList.create("a"),AList.create("b"));
+		SortArgs sa = new SortArgs("f-");
+		assertObjectEquals("[['c'],['a'],['b']]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void nonExistentField() {
+		Object in = new A[]{A.create("c"),A.create("a"),A.create("b"),A.create("e"),A.create("d")};
+		SortArgs sa = new SortArgs("fx");
+		assertObjectEquals("[{f:'c'},{f:'a'},{f:'b'},{f:'e'},{f:'d'}]", p.run(bs, in, sa));
+	}
+
+	public static class B {
+		public Object f;
+
+		public static B create(Object f) {
+			B b = new B();
+			b.f = f;
+			return b;
+		}
+	}
+
+	// Should gracefully handle different sorting data types.
+	@Test
+	public void mixtureOfTypes() {
+		Object in = new B[]{B.create(1),B.create(true),B.create("a")};
+		SortArgs sa = new SortArgs("f");
+		assertObjectEquals("[{f:1},{f:true},{f:'a'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Sort by multiple columns.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class C {
+		public int f1;
+		public float f2;
+
+		public static C create(int f1, float f2) {
+			C c = new C();
+			c.f1 = f1;
+			c.f2 = f2;
+			return c;
+		}
+	}
+
+	@Test
+	public void sortMultipleColumns() {
+		Object in = new C[]{C.create(1,1),C.create(3,2),C.create(3,1),C.create(2,1),C.create(2,2)};
+		SortArgs sa = new SortArgs("f1","f2");
+		assertObjectEquals("[{f1:1,f2:1.0},{f1:2,f2:1.0},{f1:2,f2:2.0},{f1:3,f2:1.0},{f1:3,f2:2.0}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void sortMultipleColumns_descending() {
+		Object in = new C[]{C.create(1,1),C.create(3,2),C.create(3,1),C.create(2,1),C.create(2,2)};
+		SortArgs sa = new SortArgs("f1-","f2-");
+		assertObjectEquals("[{f1:3,f2:2.0},{f1:3,f2:1.0},{f1:2,f2:2.0},{f1:2,f2:1.0},{f1:1,f2:1.0}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void sortMultipleColumns_ascendingAndDescending() {
+		Object in = new C[]{C.create(1,1),C.create(3,2),C.create(3,1),C.create(2,1),C.create(2,2)};
+		SortArgs sa = new SortArgs("f1-","f2+");
+		assertObjectEquals("[{f1:3,f2:1.0},{f1:3,f2:2.0},{f1:2,f2:1.0},{f1:2,f2:2.0},{f1:1,f2:1.0}]", p.run(bs, in, sa));
+	}
+}
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoViewerTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoViewerTest.java
new file mode 100644
index 0000000..f4e3875
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/pojotools/PojoViewerTest.java
@@ -0,0 +1,349 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.testutils.TestUtils.*;
+import static org.junit.Assert.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.utils.*;
+import org.junit.*;
+
+/**
+ * Tests the PojoPaginator class.
+ */
+public class PojoViewerTest {
+
+	PojoViewer p = new PojoViewer();
+	BeanSession bs = BeanContext.DEFAULT.createBeanSession();
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Null input
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void nullInput() {
+		assertNull(p.run(bs, null, null));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple bean
+	//-----------------------------------------------------------------------------------------------------------------
+
+	public static class A {
+		public String f1,f2;
+
+		public static A create(String f1, String f2) {
+			A a = new A();
+			a.f1 = f1;
+			a.f2 = f2;
+			return a;
+		}
+	}
+
+	@Test
+	public void simpleBean() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleBean_reverseColumns() {
+		ViewArgs sa = new ViewArgs("f2","f1");
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{f2:'x2',f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleBean_dupColumns() {
+		ViewArgs sa = new ViewArgs("f1","f1");
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleBean_nonExistentColumns() {
+		ViewArgs sa = new ViewArgs("fx");
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleBean_nullColumn() {
+		ViewArgs sa = new ViewArgs("f1",null);
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleBean_emptyArgs() {
+		ViewArgs sa = new ViewArgs();
+		Object in = A.create("x1","x2");
+		assertObjectEquals("{}", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple BeanMap
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void simpleBeanMap() {
+		ViewArgs sa = new ViewArgs("f1");
+		Object in = bs.toBeanMap(A.create("x1","x2"));
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple map
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void simpleMap() {
+		ViewArgs sa = new ViewArgs("f1");
+		Object in = AMap.create().append("f1","x1").append("f2","x2");
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleMap_reverseColumns() {
+		ViewArgs sa = new ViewArgs("f2","f1");
+		Object in = AMap.create().append("f1","x1").append("f2","x2");
+		assertObjectEquals("{f2:'x2',f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleMap_nonExistentColumns() {
+		ViewArgs sa = new ViewArgs("fx");
+		Object in = AMap.create().append("f1","x1").append("f2","x2");
+		assertObjectEquals("{}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleMap_nullColumn() {
+		ViewArgs sa = new ViewArgs("f1",null);
+		Object in = AMap.create().append("f1","x1").append("f2","x2");
+		assertObjectEquals("{f1:'x1'}", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void simpleMap_emptyView() {
+		ViewArgs sa = new ViewArgs();
+		Object in = AMap.create().append("f1","x1").append("f2","x2");
+		assertObjectEquals("{}", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Bean array
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanArray() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_reverseColumns() {
+		ViewArgs sa = new ViewArgs("f2","f1");
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{f2:'x2',f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_dupColumns() {
+		ViewArgs sa = new ViewArgs("f1","f1");
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_nonExistentColumns() {
+		ViewArgs sa = new ViewArgs("fx");
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_nullColumn() {
+		ViewArgs sa = new ViewArgs("f1",null);
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_emptyArgs() {
+		ViewArgs sa = new ViewArgs();
+		Object in = new A[]{A.create("x1","x2")};
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanArray_withNull() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = new A[]{A.create("x1","x2"),null};
+		assertObjectEquals("[{f1:'x1'},null]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Bean list
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanList() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_reverseColumns() {
+		ViewArgs sa = new ViewArgs("f2","f1");
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{f2:'x2',f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_dupColumns() {
+		ViewArgs sa = new ViewArgs("f1","f1");
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_nonExistentColumns() {
+		ViewArgs sa = new ViewArgs("fx");
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_nullColumn() {
+		ViewArgs sa = new ViewArgs("f1",null);
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_emptyArgs() {
+		ViewArgs sa = new ViewArgs();
+		Object in = AList.create(A.create("x1","x2"));
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanList_withNull() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = AList.create(A.create("x1","x2"),null);
+		assertObjectEquals("[{f1:'x1'},null]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Bean set
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanSet() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_reverseColumns() {
+		ViewArgs sa = new ViewArgs("f2","f1");
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{f2:'x2',f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_dupColumns() {
+		ViewArgs sa = new ViewArgs("f1","f1");
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_nonExistentColumns() {
+		ViewArgs sa = new ViewArgs("fx");
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_nullColumn() {
+		ViewArgs sa = new ViewArgs("f1",null);
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_emptyArgs() {
+		ViewArgs sa = new ViewArgs();
+		Object in = ASet.create(A.create("x1","x2"));
+		assertObjectEquals("[{}]", p.run(bs, in, sa));
+	}
+
+	@Test
+	public void beanSet_withNull() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = ASet.create(A.create("x1","x2"),null);
+		assertObjectEquals("[{f1:'x1'},null]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other object
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void otherObject() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = "foobar";
+		assertObjectEquals("'foobar'", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Map list
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void mapList() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = AList.create(AMap.create().append("f1","x1").append("f2","x2"));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// BeanMap list
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void beanMapList() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = AList.create(bs.toBeanMap(A.create("x1","x2")));
+		assertObjectEquals("[{f1:'x1'}]", p.run(bs, in, sa));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other object list
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void otherObjectList() {
+		ViewArgs sa = new ViewArgs("f1");;
+		Object in = AList.create("foobar");
+		assertObjectEquals("['foobar']", p.run(bs, in, sa));
+	}
+
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateBeanMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateBeanMap.java
index 1af9076..4b8a8b3 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateBeanMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateBeanMap.java
@@ -28,7 +28,7 @@ import org.apache.juneau.*;
  */
 public class DelegateBeanMap<T> extends BeanMap<T> {
 
-	private Set<String> keys = Collections.newSetFromMap(new LinkedHashMap<String,Boolean>());
+	private Set<String> keys = new LinkedHashSet<>();
 	private ObjectMap overrideValues = new ObjectMap();
 
 	/**
@@ -78,10 +78,12 @@ public class DelegateBeanMap<T> extends BeanMap<T> {
 	 * This does not affect the underlying bean.
 	 *
 	 * @param keys The remaining keys in the bean map (in the specified order).
+	 * @return This object (for method chaining).
 	 */
-	public void filterKeys(List<String> keys) {
+	public DelegateBeanMap<T> filterKeys(List<String> keys) {
 		this.keys.clear();
 		this.keys.addAll(keys);
+		return this;
 	}
 
 	@Override /* Map */
@@ -119,7 +121,7 @@ public class DelegateBeanMap<T> extends BeanMap<T> {
 			if (overrideValues.containsKey(key))
 				p = BeanPropertyMeta.builder(this.meta, key).overrideValue(overrideValues.get(key)).delegateFor(p).build();
 			if (p == null)
-				throw new BeanRuntimeException(super.getClassMeta().getInnerClass(), "Property ''{0}'' not found on class.", key);
+				p = BeanPropertyMeta.builder(this.meta, key).overrideValue(null).delegateFor(p).build();
 			l.add(p);
 		}
 		return l;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java
index 83d7440..daaf02c 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java
@@ -21,7 +21,8 @@ import org.apache.juneau.*;
  *
  * @param <T> The class type of the wrapped bean.
  */
-public class DelegateMap<T> extends ObjectMap implements Delegate<T> {
+@SuppressWarnings("rawtypes")
+public class DelegateMap<T extends Map> extends ObjectMap implements Delegate<T> {
 	private static final long serialVersionUID = 1L;
 
 	private transient ClassMeta<T> classMeta;
@@ -29,10 +30,13 @@ public class DelegateMap<T> extends ObjectMap implements Delegate<T> {
 	/**
 	 * Constructor.
 	 *
-	 * @param classMeta The metadata object that created this delegate object.
+	 * @param m The metadata object that created this delegate object.
+	 * @param session
 	 */
-	public DelegateMap(ClassMeta<T> classMeta) {
-		this.classMeta = classMeta;
+	public DelegateMap(T m, BeanSession session) {
+		this.classMeta = session.getClassMetaForObject(m);
+		for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
+			put(StringUtils.asString(e.getKey()), e.getValue());
 	}
 
 	@Override /* Delegate */
@@ -47,12 +51,15 @@ public class DelegateMap<T> extends ObjectMap implements Delegate<T> {
 	 * This does not affect the underlying map.
 	 *
 	 * @param keys The remaining keys in the map (in the specified order).
+	 * @return This object (for method chaining).
 	 */
-	public void filterKeys(List<String> keys) {
+	public DelegateMap<T> filterKeys(List<String> keys) {
 		ObjectMap m2 = new ObjectMap();
 		for (String k : keys)
-			m2.put(k, get(k));
+			if (containsKey(k))
+				m2.put(k, get(k));
 		this.clear();
 		this.putAll(m2);
+		return this;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ObjectUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ObjectUtils.java
index 25f225d..aa6d54f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ObjectUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ObjectUtils.java
@@ -350,4 +350,32 @@ public final class ObjectUtils {
 				return oo;
 		return null;
 	}
+
+	/**
+	 * Compares two objects for equality.
+	 *
+	 * <p>
+	 * Nulls are always considered less-than unless both are null.
+	 *
+	 * @param o1 Object 1.
+	 * @param o2 Object 2.
+	 * @return
+	 * 	<code>-1</code>, <code>0</code>, or <code>1</code> if <code>o1</code> is less-than, equal, or greater-than <code>o2</code>.
+	 *	<br><code>0</code> if objects are not of the same type or do not implement the {@link Comparable} interface.
+	 */
+	@SuppressWarnings({ "rawtypes", "unchecked" })
+	public static int compare(Object o1, Object o2) {
+		if (o1 == null) {
+			if (o2 == null)
+				return 0;
+			return -1;
+		} else if (o2 == null) {
+			return 1;
+		}
+
+		if (o1.getClass() == o2.getClass() && o1 instanceof Comparable)
+			return ((Comparable)o1).compareTo(o2);
+
+		return 0;
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StateMachineState.java
similarity index 65%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StateMachineState.java
index 5231a47..9232bff 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StateMachineState.java
@@ -10,33 +10,12 @@
 // * "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.rest.widget;
-
-import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
+package org.apache.juneau.internal;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
- *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * Enums for state-machine states.
  */
-public class PoweredByJuneau extends Widget {
-
-
-	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
-	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
-	}
+@SuppressWarnings("javadoc")
+public enum StateMachineState {
+	S01, S02, S03, S04, S05, S06, S07, S08, S09, S10, S11, S12, S13, S14, S15, S16, S17, S18, S19, S20;
 }
-
-
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
index fa8568e..c166d25 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
@@ -772,6 +772,19 @@ public final class StringUtils {
 	 * 	<br>An empty string results in an empty array.
 	 */
 	public static String[] splitQuoted(String s) {
+		return splitQuoted(s, false);
+	}
+
+	/**
+	 * Same as {@link #splitQuoted(String)} but allows you to optionally keep the quote characters.
+	 *
+	 * @param s The input string.
+	 * @param keepQuotes If <jk>true</jk>, quote characters are kept on the tokens.
+	 * @return
+	 * 	The results, or <jk>null</jk> if the input was <jk>null</jk>.
+	 * 	<br>An empty string results in an empty array.
+	 */
+	public static String[] splitQuoted(String s, boolean keepQuotes) {
 
 		if (s == null)
 			return null;
@@ -802,10 +815,10 @@ public final class StringUtils {
 			if (state == S1) {
 				if (c == '\'') {
 					state = S2;
-					mark = i+1;
+					mark = keepQuotes ? i : i+1;
 				} else if (c == '"') {
 					state = S3;
-					mark = i+1;
+					mark = keepQuotes ? i : i+1;
 				} else if (c != ' ' && c != '\t') {
 					state = S4;
 					mark = i;
@@ -813,10 +826,10 @@ public final class StringUtils {
 			} else if (state == S2 || state == S3) {
 				if (c == '\\') {
 					isInEscape = ! isInEscape;
-					needsUnescape = true;
+					needsUnescape = ! keepQuotes;
 				} else if (! isInEscape) {
 					if (c == (state == S2 ? '\'' : '"')) {
-						String s2 = s.substring(mark, i);
+						String s2 = s.substring(mark, keepQuotes ? i+1 : i);
 						if (needsUnescape)
 							s2 = unEscapeChars(s2, QUOTE_ESCAPE_SET);
 						l.add(s2);
@@ -1285,6 +1298,18 @@ public final class StringUtils {
 	}
 
 	/**
+	 * Strips the first and last character from a string.
+	 *
+	 * @param s The string to strip.
+	 * @return The striped string, or the same string if the input was <jk>null</jk> or less than length 2.
+	 */
+	public static String strip(String s) {
+		if (s == null || s.length() <= 1)
+			return s;
+		return s.substring(1, s.length()-1);
+	}
+
+	/**
 	 * Parses an ISO8601 string into a date.
 	 *
 	 * <p>
@@ -2584,4 +2609,47 @@ public final class StringUtils {
 		}
 		return l;
 	}
+
+	/**
+	 * Replaces tokens in a string with a different token.
+	 *
+	 * <p>
+	 * 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 s The string to replace characters in.
+	 * @param from The character to replace.
+	 * @param to The character to replace with.
+	 * @param ignoreEscapedChars Specify 'true' if escaped 'from' characters should be ignored.
+	 * @return The string with characters replaced.
+	 */
+	public static String replaceChars(String s, char from, char to, boolean ignoreEscapedChars) {
+		if (s == null)
+			return null;
+
+		if (s.indexOf(from) == -1)
+			return s;
+
+		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);
+	}
+
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Equality.java
similarity index 65%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Equality.java
index 5231a47..b567988 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Equality.java
@@ -10,33 +10,12 @@
 // * "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.rest.widget;
-
-import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
+package org.apache.juneau.pojotools;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
- *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * TODO
  */
-public class PoweredByJuneau extends Widget {
-
-
-	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
-	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
-	}
-}
-
-
+@SuppressWarnings("javadoc")
+public enum Equality {
+	GT, GTE, LT, LTE, NONE
+}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Matcher.java
similarity index 67%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Matcher.java
index 5231a47..34c125f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/Matcher.java
@@ -10,33 +10,21 @@
 // * "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.rest.widget;
+package org.apache.juneau.pojotools;
 
 import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
- *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * Common interface for matchers used by the {@link PojoSearcher} class.
  */
-public class PoweredByJuneau extends Widget {
-
+public abstract class Matcher {
 
 	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
+	 * Returns <jk>true</jk> if this matcher matches the specified object..
+	 *
+	 * @param cm The class type of the object being matched.  Never <jk>null</jk>.
+	 * @param o The object being matched.  Never <jk>null</jk>.
+	 * @return <jk>true</jk> if the specified object matches the specified pattern.
 	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
-	}
+	public abstract boolean matches(ClassMeta<?> cm, Object o);
 }
-
-
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/MatcherFactory.java
similarity index 67%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/MatcherFactory.java
index 5231a47..511b76d 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/MatcherFactory.java
@@ -10,33 +10,28 @@
 // * "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.rest.widget;
+package org.apache.juneau.pojotools;
 
 import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
- *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * Common interface for matchers used by the {@link PojoSearcher} class.
  */
-public class PoweredByJuneau extends Widget {
+public abstract class MatcherFactory {
 
+	/**
+	 * Returns <jk>true</jk> if this matcher can be used on the specified object.
+	 *
+	 * @param cm The class type of the object being matched.  Never <jk>null</jk>.
+	 * @return <jk>true</jk> if this matcher can be used on the specified object.
+	 */
+	public abstract boolean canMatch(ClassMeta<?> cm);
 
 	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
+	 * Instantiates a matcher for the specified pattern.
+	 *
+	 * @param pattern The pattern string.
+	 * @return A matcher for the specified pattern.
 	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
-	}
+	public abstract Matcher create(String pattern);
 }
-
-
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/NumberMatcherFactory.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/NumberMatcherFactory.java
new file mode 100644
index 0000000..0600397
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/NumberMatcherFactory.java
@@ -0,0 +1,291 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.internal.StateMachineState.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * TODO
+ *
+ */
+public class NumberMatcherFactory extends MatcherFactory {
+
+	/**
+	 * Default reusable matcher.
+	 */
+	public static final NumberMatcherFactory DEFAULT = new NumberMatcherFactory();
+
+	@Override
+	public boolean canMatch(ClassMeta<?> cm) {
+		return cm.isNumber();
+	}
+
+	@Override
+	public Matcher create(String pattern) {
+		return new NumberMatcher(pattern);
+	}
+
+	/**
+	 * A construct representing a single search pattern.
+	 */
+	private static class NumberMatcher extends Matcher {
+		NumberRange[] numberRanges;
+		private static final AsciiSet
+			SNUM = AsciiSet.create("-0123456789."),
+			NUM = AsciiSet.create("0123456789."),
+			WS = AsciiSet.create(" \t");
+
+		public NumberMatcher(String s) {
+
+			s = s.trim();
+
+			List<NumberRange> l = new LinkedList<>();
+
+			// Possible patterns:
+			// 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456, !123, !123-456, 123 - 456 (one token), 123 -456 (two separate tokens)
+
+			// Possible states:
+			// S01 = Looking for start (WS=S01, [!]=S01, [>]=S02, [<]=S03, SNUM=S06)
+			// S02 = Found [>], looking for [=]/SNUM ([=]=S04, WS=S05, SNUM=S06)
+			// S03 = Found [<], looking for [=]/SNUM ([=]=S05, WS=S05, SNUM=S06)
+			// S04 = Found [=], looking for SNUN (WS=S05, SNUM=S06)
+			// S05 = Found [... ], looking for SNUM (SNUM=S06)
+			// S06 = Found [1], looking for [-]/WS (WS=S07, [-]=S08)
+			// S07 = Found [123 ], looking for [-]/SNUM (if -, could be 123 - 456 or 123 -456) ([-]=S09, SNUM=S07)
+			// S08 = Found [123-], looking for SNUM (Could be 123- 456 or 123-456) (SNUM=S11)
+			// S09 = Found [123 -], looking for WS/SNUM (If WS, then it's 123 - 456, otherwise 123 -456) (WS=S10, SNUM=S06)
+			// S10 = Found [123 - ], looking for SNUM (SNUM=S12)
+			// S11 = Found [123 - 4], looking for WS (WS=S01)
+
+			StateMachineState state = S01;
+			int mark = 0;
+			boolean isNot = false;
+			Equality eq = Equality.NONE;
+			Integer n1 = null, n2 = null;
+
+			int i;
+			for (i = 0; i < s.length(); i++) {
+				char c = s.charAt(i);
+				if (state == S01) {
+					if (c == '!') {
+						isNot = true;
+					} else if (WS.contains(c)) {
+						state = S01;
+					} else if (c == '>') {
+						state = S02;
+						eq = Equality.GT;
+					} else if (c == '<') {
+						state = S03;
+						eq = Equality.LT;
+					} else if (SNUM.contains(c)) {
+						state = S06;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S02) {
+					if (c == '=') {
+						state = S04;
+						eq = Equality.GTE;
+					} else if (WS.contains(c)) {
+						state = S05;
+					} else if (SNUM.contains(c)) {
+						state = S06;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S03) {
+					if (c == '=') {
+						state = S04;
+						eq = Equality.LTE;
+					} else if (WS.contains(c)) {
+						state = S05;
+					} else if (SNUM.contains(c)) {
+						state = S06;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S04) {
+					if (WS.contains(c)) {
+						state = S05;
+					} else if (SNUM.contains(c)) {
+						mark = i;
+						state = S06;
+					} else {
+						break;
+					}
+				} else if (state == S05) {
+					if (WS.contains(c)) {
+						state = S05;
+					} else if (SNUM.contains(c)) {
+						state = S06;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S06) {
+					if (NUM.contains(c)) {
+						state = S06;
+					} else if (WS.contains(c)) {
+						state = S07;
+						n1 = Integer.parseInt(s.substring(mark, i));
+					} else if (c == '-') {
+						state = S08;
+						n1 = Integer.parseInt(s.substring(mark, i));
+					} else {
+						break;
+					}
+				} else if (state == S07) {
+					if (WS.contains(c)) {
+						state = S07;
+					} else if (c == '-') {
+						state = S09;
+					} else if (SNUM.contains(c)) {
+						state = S06;
+						l.add(new NumberRange(eq, n1, isNot));
+						eq = Equality.NONE;
+						n1 = null;
+						isNot = false;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S08) {
+					if (WS.contains(c)) {
+						state = S08;
+					} else if (SNUM.contains(c)) {
+						state = S11;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S09)  {
+					if (WS.contains(c)) {
+						state = S10;
+					} else if (NUM.contains(c)) {
+						state = S06;
+						l.add(new NumberRange(eq, n1, isNot));
+						eq = Equality.NONE;
+						n1 = null;
+						isNot = false;
+						mark = i-1;
+					} else {
+						break;
+					}
+				} else if (state == S10) {
+					if (WS.contains(c)) {
+						state = S10;
+					} else if (SNUM.contains(c)) {
+						state = S11;
+						mark = i;
+					} else {
+						break;
+					}
+				} else /* (state == S11) */ {
+					if (SNUM.contains(c)) {
+						state = S11;
+					} else if (WS.contains(c)) {
+						state = S01;
+						n2 = Integer.parseInt(s.substring(mark, i));
+						l.add(new NumberRange(eq, n1, n2, isNot));
+						eq = Equality.NONE;
+						n1 = n2 = null;
+						isNot = false;
+					} else {
+						break;
+					}
+				}
+			}
+
+			if (i != s.length())
+				throw new PatternException("Invalid range pattern ({0}): {1}", state, s);
+
+			if (state == S01) {
+				// No tokens found.
+			} else if (state == S02 || state == S03 || state == S04 || state == S08 || state == S09) {
+				throw new PatternException("Invalid range pattern (E{0}): {1}", state, s);
+			} else if (state == S06) {
+				n1 = Integer.parseInt(s.substring(mark).trim());
+				l.add(new NumberRange(eq, n1, isNot));
+			} else /* (state == S11) */ {
+				n2 = Integer.parseInt(s.substring(mark).trim());
+				l.add(new NumberRange(eq, n1, n2, isNot));
+			}
+
+			numberRanges = l.toArray(new NumberRange[l.size()]);
+		}
+
+		@Override /* Matcher */
+		public boolean matches(ClassMeta<?> cm, Object o) {
+			Number n = (Number)o;
+			if (numberRanges.length == 0) return true;
+			for (int i = 0; i < numberRanges.length; i++)
+				if (numberRanges[i].matches(n))
+					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(Equality eq, Integer num, boolean isNot) {
+			this(eq, num, null, isNot);
+		}
+
+		public NumberRange(Equality eq, Integer start, Integer end, boolean isNot) {
+			this.isNot = isNot;
+
+			// 123, >123, <123, >=123, <=123, >-123, >=-123, 123-456, -123--456
+			if (eq == Equality.NONE && end == null) { // 123
+				this.start = start;
+				this.end = this.start;
+			} else if (eq == Equality.GT) {
+				this.start = start+1;
+				this.end = Integer.MAX_VALUE;
+			} else if (eq == Equality.GTE) {
+				this.start = start;
+				this.end = Integer.MAX_VALUE;
+			} else if (eq == Equality.LT) {
+				this.start = Integer.MIN_VALUE;
+				this.end = start-1;
+			} else if (eq == Equality.LTE) {
+				this.start = Integer.MIN_VALUE;
+				this.end = start;
+			} else {
+				this.start = start;
+				this.end = end;
+			}
+		}
+
+		public boolean matches(Number n) {
+			long i = n.longValue();
+			boolean b = (i>=start && i<=end);
+			if (isNot) b = !b;
+			return b;
+		}
+	}
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PatternException.java
similarity index 67%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PatternException.java
index 5231a47..e6bfb4e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PatternException.java
@@ -10,33 +10,24 @@
 // * "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.rest.widget;
+package org.apache.juneau.pojotools;
 
 import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
- *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * Indicates an invalid search pattern was specified.
  */
-public class PoweredByJuneau extends Widget {
+public class PatternException extends FormattedRuntimeException {
 
+	private static final long serialVersionUID = 1L;
 
 	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
+	 * Constructor.
+	 *
+	 * @param message Message.
+	 * @param args Message arguments.
 	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
+	public PatternException(String message, Object...args) {
+		super(message, args);
 	}
 }
-
-
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoPaginator.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoPaginator.java
new file mode 100644
index 0000000..465bcd8
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoPaginator.java
@@ -0,0 +1,71 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import org.apache.juneau.*;
+
+/**
+ * Designed to provide paging on POJOs consisting of arrays and collections.
+ *
+ * <p>
+ * Allows you to quickly return subsets of arrays and collections based on position/limit arguments.
+ */
+public final class PojoPaginator implements PojoTool<Object> {
+
+	@Override /* PojoTool */
+	public Object run(BeanSession session, Object input, Object args) {
+
+//		if (input == null)
+//			return null;
+//
+//		ClassMeta type = session.getClassMetaForObject(input);
+//
+//		if (! type.isCollectionOrArray())
+//			return input;
+//
+//		int pos = args.getPosition();
+//		int limit = args.getLimit();
+//
+//		if (type.isArray()) {
+//			int size = Array.getLength(input);
+//			int end = (limit+pos >= size) ? size : limit + pos;
+//			pos = Math.min(pos, size);
+//			ClassMeta<?> et = type.getElementType();
+// 			if (! et.isPrimitive())
+//				return copyOfRange((Object[])input, pos, end);
+//			if (et.isType(boolean.class))
+//				return copyOfRange((boolean[])input, pos, end);
+//			if (et.isType(byte.class))
+//				return copyOfRange((byte[])input, pos, end);
+//			if (et.isType(char.class))
+//				return copyOfRange((char[])input, pos, end);
+//			if (et.isType(double.class))
+//				return copyOfRange((double[])input, pos, end);
+//			if (et.isType(float.class))
+//				return copyOfRange((float[])input, pos, end);
+//			if (et.isType(int.class))
+//				return copyOfRange((int[])input, pos, end);
+//			if (et.isType(long.class))
+//				return copyOfRange((long[])input, pos, end);
+//			if (et.isType(short.class))
+//				return copyOfRange((short[])input, pos, end);
+//			return null;
+//		}
+//
+//		List l = type.isList() ? (List)input : new ArrayList((Collection)input);
+//		int end = (limit+pos >= l.size()) ? l.size() : limit + pos;
+//		pos = Math.min(pos, l.size());
+//		return l.subList(pos, end);
+		return null;
+	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSearcher.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSearcher.java
new file mode 100644
index 0000000..1abeffe
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSearcher.java
@@ -0,0 +1,185 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+
+/**
+ * Designed to provide paging on POJOs consisting of arrays and collections.
+ *
+ * <p>
+ * Allows you to quickly return subsets of arrays and collections based on position/limit arguments.
+ */
+@SuppressWarnings({"rawtypes"})
+public final class PojoSearcher implements PojoTool<SearchArgs> {
+
+	/**
+	 * Default reusable searcher.
+	 */
+	public static final PojoSearcher DEFAULT = new PojoSearcher();
+
+	final MatcherFactory[] factories;
+
+	/**
+	 * TODO
+	 *
+	 * @param factories
+	 */
+	public PojoSearcher(MatcherFactory...factories) {
+		this.factories = factories;
+	}
+
+	/**
+	 * TODO
+	 *
+	 */
+	public PojoSearcher() {
+		this(NumberMatcherFactory.DEFAULT, TimeMatcherFactory.DEFAULT, StringMatcherFactory.DEFAULT);
+	}
+
+	@Override /* PojoTool */
+	public Object run(BeanSession session, Object input, SearchArgs args) {
+
+		ClassMeta<?> type = session.getClassMetaForObject(input);
+		Map<String,String> search = args.getSearch();
+
+		if (search.isEmpty() || type == null || ! type.isCollectionOrArray())
+			return input;
+
+		List<Object> l = null;
+		RowMatcher rowMatcher = new RowMatcher(session, search);
+
+		if (type.isCollection()) {
+			Collection c = (Collection)input;
+			l = new ArrayList<>(c.size());
+			for (Object o : c) {
+				if (rowMatcher.matches(o))
+					l.add(o);
+			}
+
+		} else /* isArray */ {
+			int size = Array.getLength(input);
+			l = new ArrayList<>(size);
+			for (int i = 0; i < size; i++) {
+				Object o = Array.get(input, i);
+				if (rowMatcher.matches(o))
+					l.add(o);
+			}
+		}
+
+		return l;
+	}
+
+	//====================================================================================================
+	// MapMatcher
+	//====================================================================================================
+	/*
+	 * Matches on a Map only if all specified entry matchers match.
+	 */
+	private class RowMatcher {
+
+		Map<String,ColumnMatcher> entryMatchers = new HashMap<>();
+		BeanSession bs;
+
+		RowMatcher(BeanSession bs, Map query) {
+			this.bs = bs;
+			for (Map.Entry e : (Set<Map.Entry>)query.entrySet())
+				entryMatchers.put(asString(e.getKey()), new ColumnMatcher(bs, asString(e.getValue())));
+		}
+
+		boolean matches(Object o) {
+			if (o == null)
+				return false;
+			ClassMeta<?> cm = bs.getClassMetaForObject(o);
+			if (cm.isMapOrBean()) {
+				Map m = cm.isMap() ? (Map)o : bs.toBeanMap(o);
+				for (Map.Entry<String,ColumnMatcher> e : entryMatchers.entrySet()) {
+					String key = e.getKey();
+					Object val = null;
+					if (m instanceof BeanMap) {
+						val = ((BeanMap)m).getRaw(key);
+					} else {
+						val = m.get(key);
+					}
+					if (! e.getValue().matches(val))
+						return false;
+				}
+				return true;
+			}
+			if (cm.isCollection()) {
+				for (Object o2 : (Collection)o)
+					if (! matches(o2))
+						return false;
+				return true;
+			}
+			if (cm.isArray()) {
+				for (int i = 0; i < Array.getLength(o); i++)
+					if (! matches(Array.get(o, i)))
+						return false;
+				return true;
+			}
+			return false;
+		}
+	}
+
+	//====================================================================================================
+	// ObjectMatcher
+	//====================================================================================================
+	/*
+	 * Matcher that uses the correct matcher based on object type.
+	 * Used for objects when we can't determine the object type beforehand.
+	 */
+	private class ColumnMatcher {
+
+		String searchPattern;
+		Matcher[] matchers;
+		BeanSession bs;
+
+		ColumnMatcher(BeanSession bs, String searchPattern) {
+			this.bs = bs;
+			this.searchPattern = searchPattern;
+			this.matchers = new Matcher[factories.length];
+		}
+
+		boolean matches(Object o) {
+			ClassMeta<?> cm = bs.getClassMetaForObject(o);
+			if (cm == null)
+				return false;
+			if (cm.isCollection()) {
+				for (Object o2 : (Collection)o)
+					if (matches(o2))
+						return true;
+				return false;
+			}
+			if (cm.isArray()) {
+				for (int i = 0; i < Array.getLength(o); i++)
+					if (matches(Array.get(o, i)))
+						return true;
+				return false;
+			}
+			for (int i = 0; i < factories.length; i++) {
+				if (factories[i].canMatch(cm)) {
+					if (matchers[i] == null)
+						matchers[i] = factories[i].create(searchPattern);
+					return matchers[i].matches(cm, o);
+				}
+			}
+			return false;
+		}
+	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSorter.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSorter.java
new file mode 100644
index 0000000..67ac4e6
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoSorter.java
@@ -0,0 +1,110 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Sorts arrays and collections of maps and beans.
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public final class PojoSorter implements PojoTool<SortArgs> {
+
+	@Override /* PojoTool */
+	public Object run(BeanSession session, Object input, SortArgs args) {
+		if (input == null)
+			return null;
+
+		// If sort or view isn't empty, then we need to make sure that all entries in the
+		// list are maps.
+		Map<String,Boolean> sort = args.getSort();
+
+		if (sort.isEmpty())
+			return input;
+
+		ClassMeta type = session.getClassMetaForObject(input);
+
+		if (! type.isCollectionOrArray())
+			return input;
+
+		ArrayList<SortEntry> l = null;
+
+		if (type.isArray()) {
+			int size = Array.getLength(input);
+			l = new ArrayList<>(size);
+			for (int i = 0; i < size; i++)
+				l.add(new SortEntry(session, Array.get(input, i)));
+		} else /* isCollection() */ {
+			Collection c = (Collection)input;
+			l = new ArrayList<>(c.size());
+			for (Object o : c)
+				l.add(new SortEntry(session, o));
+		}
+
+		// We reverse the list and sort last to first.
+		List<String> columns = new ArrayList<>(sort.keySet());
+		Collections.reverse(columns);
+
+		for (final String c : columns) {
+			final boolean isDesc = sort.get(c);
+			for (SortEntry se : l)
+				se.setSort(c, isDesc);
+			Collections.sort(l);
+		}
+
+		ArrayList<Object> l2 = new ArrayList<>(l.size());
+		for (SortEntry se : l)
+			l2.add(se.o);
+
+		return l2;
+	}
+
+	private static class SortEntry implements Comparable {
+		Object o;
+		ClassMeta<?> cm;
+		BeanSession bs;
+
+		Object sortVal;
+		boolean isDesc;
+
+		SortEntry(BeanSession bs, Object o) {
+			this.o = o;
+			this.bs = bs;
+			this.cm = bs.getClassMetaForObject(o);
+		}
+
+		void setSort(String sortCol, boolean isDesc) {
+			this.isDesc = isDesc;
+
+			if (cm == null)
+				sortVal = null;
+			else if (cm.isMap())
+				sortVal = ((Map)o).get(sortCol);
+			else if (cm.isBean())
+				sortVal = bs.toBeanMap(o).get(sortCol);
+			else
+				sortVal = null;
+		}
+
+		@Override
+		public int compareTo(Object o) {
+			if (isDesc)
+				return ObjectUtils.compare(((SortEntry)o).sortVal, this.sortVal);
+			return ObjectUtils.compare(this.sortVal, ((SortEntry)o).sortVal);
+		}
+	}
+}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoTool.java
similarity index 67%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoTool.java
index 5231a47..ead70f8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoTool.java
@@ -10,33 +10,24 @@
 // * "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.rest.widget;
+package org.apache.juneau.pojotools;
 
 import org.apache.juneau.*;
-import org.apache.juneau.rest.*;
 
 /**
- * Widget that places a powered-by-Juneau message on the page.
+ * Interface for classes that convert POJOs in some way using some predefined arguments object.
  *
- * <p>
- * The variable it resolves is <js>"$W{PoweredByJuneau}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * @param <T> The argument object type.
  */
-public class PoweredByJuneau extends Widget {
-
+public interface PojoTool<T> {
 
 	/**
-	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
+	 * Converts the specified input to some other output.
+	 *
+	 * @param session The current bean session.
+	 * @param input The input POJO.
+	 * @param args The arguments.
+	 * @return The output POJO.
 	 */
-	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
-		UriResolver r = req.getUriResolver();
-		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
-	}
+	public Object run(BeanSession session, Object input, T args);
 }
-
-
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoViewer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoViewer.java
new file mode 100644
index 0000000..b2bd530
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/PojoViewer.java
@@ -0,0 +1,80 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Designed to provide paging on POJOs consisting of arrays and collections.
+ *
+ * <p>
+ * Allows you to quickly return subsets of arrays and collections based on position/limit arguments.
+ */
+@SuppressWarnings({"unchecked","rawtypes"})
+public final class PojoViewer implements PojoTool<ViewArgs> {
+
+	@Override /* PojoTool */
+	public Object run(BeanSession session, Object input, ViewArgs args) {
+
+		if (input == null)
+			return null;
+
+		List<String> view = args.getView();
+		ClassMeta type = session.getClassMetaForObject(input);
+
+		if (type.isBeanMap())
+			return new DelegateBeanMap(((BeanMap)input).getBean(), session).filterKeys(view);
+		if (type.isMap())
+			return new DelegateMap((Map)input, session).filterKeys(view);
+		if (type.isBean())
+			return new DelegateBeanMap(input, session).filterKeys(view);
+
+		ArrayList<Object> l = null;
+
+		if (type.isArray()) {
+			int size = Array.getLength(input);
+			l = new ArrayList<>(size);
+			for (int i = 0; i < size; i++)
+				l.add(Array.get(input, i));
+		} else if (type.isCollection()) {
+			Collection c = (Collection)input;
+			l = new ArrayList<>(c.size());
+			for (Object o : c)
+				l.add(o);
+		} else {
+			return input;
+		}
+
+		for (ListIterator li = l.listIterator(); li.hasNext();) {
+			Object o = li.next();
+			ClassMeta cm2 = session.getClassMetaForObject(o);
+
+			if (cm2 == null)
+				o = null;
+			else if (cm2.isBeanMap())
+				o = new DelegateBeanMap(((BeanMap)o).getBean(), session).filterKeys(view);
+			else if (cm2.isMap())
+				o = new DelegateMap((Map)o, session).filterKeys(view);
+			else if (cm2.isBean())
+				o = new DelegateBeanMap(o, session).filterKeys(view);
+
+			li.set(o);
+		}
+
+		return l;
+	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SearchArgs.java
similarity index 55%
copy from juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SearchArgs.java
index 4d8c46d..54fc752 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SearchArgs.java
@@ -10,69 +10,68 @@
 // * "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;
+package org.apache.juneau.pojotools;
 
 import java.util.*;
 
+import org.apache.juneau.internal.*;
+
 /**
- * An extension of {@link LinkedHashSet} with a convenience {@link #append(Object)} method.
- *
- * <p>
- * Primarily used for testing purposes for quickly creating populated sets.
- * <p class='bcode w800'>
- * 	<jc>// Example:</jc>
- * 	Set&lt;String&gt; s = <jk>new</jk> ASet&lt;String&gt;().append(<js>"foo"</js>).append(<js>"bar"</js>);
- * </p>
- *
- * @param <T> The entry type.
+ * Encapsulates arguments for the {@link PojoSorter} class.
  */
-@SuppressWarnings({"unchecked"})
-public final class ASet<T> extends LinkedHashSet<T> {
+public class SearchArgs {
+
+	private final Map<String,String> search = new LinkedHashMap<>();
 
-	private static final long serialVersionUID = 1L;
 
 	/**
-	 * Convenience method for creating a list of objects.
+	 * Constructor.
 	 *
-	 * @param t The initial values.
-	 * @return A new list.
+	 * @param searchArgs Search arguments.
 	 */
-	public static <T> ASet<T> create(T...t) {
-		return new ASet<T>().appendAll(t);
+	public SearchArgs(String searchArgs) {
+		this(Arrays.asList(StringUtils.split(searchArgs, ',')));
 	}
 
 	/**
-	 * Adds an entry to this set.
+	 * Constructor.
 	 *
-	 * @param t The entry to add to this set.
-	 * @return This object (for method chaining).
+	 * @param searchArgs Search arguments.
 	 */
-	public ASet<T> append(T t) {
-		add(t);
-		return this;
+	public SearchArgs(List<String> searchArgs) {
+		for (String s : searchArgs) {
+			int i = StringUtils.indexOf(s, '=', '>', '<');
+			if (i == -1)
+				throw new PatternException("Invalid search terms: ''{0}''", searchArgs);
+			char c = s.charAt(i);
+			append(s.substring(0, i).trim(), s.substring(c == '=' ? i+1 : i).trim());
+		}
 	}
 
 	/**
-	 * Adds multiple entries to this set.
+	 * Appends the specified search argument.
 	 *
-	 * @param t The entries to add to this set.
+	 * @param column The column name to search.
+	 * @param searchTerm The search term.
 	 * @return This object (for method chaining).
 	 */
-	public ASet<T> appendAll(T...t) {
-		addAll(Arrays.asList(t));
+	public SearchArgs append(String column, String searchTerm) {
+		this.search.put(column, searchTerm);
 		return this;
 	}
 
 	/**
-	 * Adds a value to this set if the boolean value is <jk>true</jk>
+	 * The query search terms.
 	 *
-	 * @param b The boolean value.
-	 * @param t The value to add.
-	 * @return This object (for method chaining).
+	 * <p>
+	 * The search terms are key/value pairs consisting of column-names and search tokens.
+	 *
+	 * <p>
+	 * It's up to implementers to decide the syntax and meaning of the search term.
+	 *
+	 * @return An unmodifiable map of query search terms.
 	 */
-	public ASet<T> appendIf(boolean b, T t) {
-		if (b)
-			append(t);
-		return this;
+	public Map<String,String> getSearch() {
+		return search;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SortArgs.java
similarity index 51%
copy from juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SortArgs.java
index 83d7440..45d9652 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/DelegateMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/SortArgs.java
@@ -1,58 +1,80 @@
-// ***************************************************************************************************************************
-// * 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.internal;
-
-import java.util.*;
-
-import org.apache.juneau.*;
-
-/**
- * Represents a wrapped {@link Map} where entries in the map can be removed without affecting the underlying map.
- *
- * @param <T> The class type of the wrapped bean.
- */
-public class DelegateMap<T> extends ObjectMap implements Delegate<T> {
-	private static final long serialVersionUID = 1L;
-
-	private transient ClassMeta<T> classMeta;
-
-	/**
-	 * Constructor.
-	 *
-	 * @param classMeta The metadata object that created this delegate object.
-	 */
-	public DelegateMap(ClassMeta<T> classMeta) {
-		this.classMeta = classMeta;
-	}
-
-	@Override /* Delegate */
-	public ClassMeta<T> getClassMeta() {
-		return classMeta;
-	}
-
-	/**
-	 * Remove all but the specified keys from this map.
-	 *
-	 * <p>
-	 * This does not affect the underlying map.
-	 *
-	 * @param keys The remaining keys in the map (in the specified order).
-	 */
-	public void filterKeys(List<String> keys) {
-		ObjectMap m2 = new ObjectMap();
-		for (String k : keys)
-			m2.put(k, get(k));
-		this.clear();
-		this.putAll(m2);
-	}
-}
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static java.util.Collections.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.util.*;
+
+/**
+ * Encapsulates arguments for the {@link PojoSorter} class.
+ */
+public class SortArgs {
+
+	private final Map<String,Boolean> sort;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param sortArgs
+	 * 	Sort arguments.
+	 * 	<br>Values are of the following forms:
+	 * 	<ul>
+	 * 		<li><js>"column"</js> - Sort column ascending.
+	 * 		<li><js>"column+"</js> - Sort column ascending.
+	 * 		<li><js>"column-"</js> - Sort column descending.
+	 * 	</ul>
+	 */
+	public SortArgs(String...sortArgs) {
+		this(Arrays.asList(sortArgs));
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param sortArgs
+	 * 	Sort arguments.
+	 * 	<br>Values are of the following forms:
+	 * 	<ul>
+	 * 		<li><js>"column"</js> - Sort column ascending.
+	 * 		<li><js>"column+"</js> - Sort column ascending.
+	 * 		<li><js>"column-"</js> - Sort column descending.
+	 * 	</ul>
+	 */
+	public SortArgs(Collection<String> sortArgs) {
+		Map<String,Boolean> sort = new LinkedHashMap<>();
+		for (String s : sortArgs) {
+			boolean isDesc = false;
+			if (endsWith(s, '-', '+')) {
+				isDesc = endsWith(s, '-');
+				s = s.substring(0, s.length()-1);
+			}
+			sort.put(s, isDesc);
+		}
+		this.sort = unmodifiableMap(sort);
+	}
+
+	/**
+	 * The sort columns.
+	 *
+	 * <p>
+	 * The sort columns are key/value pairs consisting of column-names and direction flags
+	 * (<jk>false</jk> = ascending, <jk>true</jk> = descending).
+	 *
+	 * @return An unmodifiable ordered map of sort columns and directions.
+	 */
+	public Map<String,Boolean> getSort() {
+		return sort;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/StringMatcherFactory.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/StringMatcherFactory.java
new file mode 100644
index 0000000..2e63d6f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/StringMatcherFactory.java
@@ -0,0 +1,145 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.util.*;
+import java.util.regex.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * TODO
+ *
+ */
+public class StringMatcherFactory extends MatcherFactory {
+
+	/**
+	 * Default reusable matcher.
+	 */
+	public static final StringMatcherFactory DEFAULT = new StringMatcherFactory();
+
+	@Override
+	public boolean canMatch(ClassMeta<?> cm) {
+		return true;
+	}
+
+	@Override
+	public Matcher create(String pattern) {
+		return new StringMatcher(pattern);
+	}
+
+	/**
+	 * A construct representing a single search pattern.
+	 */
+	private static class StringMatcher extends Matcher {
+		private static final AsciiSet
+			META_CHARS = AsciiSet.create("*?'\""),
+			SQ_CHAR = AsciiSet.create("'"),
+			DQ_CHAR = AsciiSet.create("\""),
+			REGEX_CHARS = AsciiSet.create("+\\[]{}()^$.");
+
+		Pattern[] orPatterns, andPatterns, notPatterns;
+
+		public StringMatcher(String searchPattern) {
+
+			List<Pattern> ors = new LinkedList<>();
+			List<Pattern> ands = new LinkedList<>();
+			List<Pattern> nots = new LinkedList<>();
+
+			for (String s : splitQuoted(searchPattern, true)) {
+				char c0 = s.charAt(0), c9 = s.charAt(s.length()-1);
+
+				if (c0 == '/' && c9 == '/' && s.length() > 1) {
+					ands.add(Pattern.compile(strip(s)));
+				} else {
+					char prefix = '^';
+					boolean ignoreCase = false;
+					if (s.length() > 1 && (c0 == '^' || c0 == '+' || c0 == '-')) {
+						prefix = c0;
+						s = s.substring(1);
+						c0 = s.charAt(0);
+					}
+
+					if (c0 == '\'') {
+						s = unEscapeChars(strip(s), SQ_CHAR);
+						ignoreCase = true;
+					} else if (c0 == '"') {
+						s = unEscapeChars(strip(s), DQ_CHAR);
+					}
+
+					if (REGEX_CHARS.contains(s) || META_CHARS.contains(s)) {
+						StringBuilder sb = new StringBuilder();
+						boolean isInEscape = false;
+						for (int i = 0; i < s.length(); i++) {
+							char c = s.charAt(i);
+							if (isInEscape) {
+								if (c == '?' || c == '*' || c == '\\')
+									sb.append('\\').append(c);
+								else
+									sb.append(c);
+								isInEscape = false;
+							} else {
+								if (c == '\\')
+									isInEscape = true;
+								else if (c == '?')
+									sb.append(".?");
+								else if (c == '*')
+									sb.append(".*");
+								else if (REGEX_CHARS.contains(c))
+									sb.append("\\").append(c);
+								else
+									sb.append(c);
+							}
+						}
+						s = sb.toString();
+					}
+
+
+					int flags = Pattern.DOTALL;
+					if (ignoreCase)
+						flags |= Pattern.CASE_INSENSITIVE;
+
+					Pattern p = Pattern.compile(s, flags);
+
+					if (prefix == '-')
+						nots.add(p);
+					else if (prefix == '+')
+						ands.add(p);
+					else
+						ors.add(p);
+				}
+			}
+			orPatterns = ors.toArray(new Pattern[ors.size()]);
+			andPatterns = ands.toArray(new Pattern[ands.size()]);
+			notPatterns = nots.toArray(new Pattern[nots.size()]);
+		}
+
+		@Override
+		public boolean matches(ClassMeta<?> cm, Object o) {
+			String s = (String)o;
+			for (int i = 0; i < andPatterns.length; i++)
+				if (! andPatterns[i].matcher(s).matches())
+					return false;
+			for (int i = 0; i < notPatterns.length; i++)
+				if (notPatterns[i].matcher(s).matches())
+					return false;
+			for (int i = 0; i < orPatterns.length; i++)
+				if (orPatterns[i].matcher(s).matches())
+					return true;
+			return orPatterns.length == 0;
+		}
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/TimeMatcherFactory.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/TimeMatcherFactory.java
new file mode 100644
index 0000000..b27ea5b
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/TimeMatcherFactory.java
@@ -0,0 +1,463 @@
+// ***************************************************************************************************************************
+// * 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.pojotools;
+
+import static java.util.Calendar.*;
+import static org.apache.juneau.internal.StateMachineState.*;
+
+import java.text.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * TODO
+ */
+public class TimeMatcherFactory extends MatcherFactory {
+
+	/**
+	 * Default reusable matcher.
+	 */
+	public static final TimeMatcherFactory DEFAULT = new TimeMatcherFactory();
+
+	private final SimpleDateFormat[] formats;
+
+	/**
+	 * Constructor.
+	 */
+	protected TimeMatcherFactory() {
+		this.formats = getTimestampFormats();
+	}
+
+	/**
+	 * TODO
+	 *
+	 * @return TODO
+	 */
+	protected SimpleDateFormat[] getTimestampFormats() {
+		String[] s = getTimestampFormatStrings();
+		SimpleDateFormat[] a = new SimpleDateFormat[s.length];
+		for (int i = 0; i < s.length; i++)
+			a[i] = new SimpleDateFormat(s[i]);
+		return a;
+	}
+
+	/**
+	 * TODO
+	 *
+	 * @return TODO
+	 */
+	protected String[] getTimestampFormatStrings() {
+		return new String[]{
+			"yyyy-MM-dd'T'HH:mm:ss",
+			"yyyy-MM-dd'T'HH:mm",
+			"yyyy-MM-dd'T'HH",
+			"yyyy-MM-dd",
+			"yyyy-MM",
+			"yyyy"
+		};
+	}
+
+	@Override
+	public boolean canMatch(ClassMeta<?> cm) {
+		return cm.isDateOrCalendar();
+	}
+
+	@Override
+	public Matcher create(String pattern) {
+		return new TimeMatcher(formats, pattern);
+	}
+
+	/**
+	 * A construct representing a single search pattern.
+	 */
+	private static class TimeMatcher extends Matcher {
+
+		private static final AsciiSet
+			DT = AsciiSet.create("0123456789-:T./"),
+			WS = AsciiSet.create(" \t");
+
+		TimestampRange[] ranges;
+		List<TimestampRange> l = new LinkedList<>();
+
+		public TimeMatcher(SimpleDateFormat[] f, String s) {
+
+			// Possible patterns:
+			// >2000, <2000, >=2000, <=2000, > 2000, 2000 - 2001, '2000', >'2000', '2000'-'2001', '2000' - '2001'
+
+			// Possible states:
+			// S01 = Looking for [<]/[>]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
+			// S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
+			// S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
+			// S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08)
+			// S05 = Found ['], looking for ['] ([']=S01)
+			// S06 = Found ["], looking for ["] (["]=S01)
+			// S07 = Found [123"] or [123'], looking for WS (WS=S09)
+			// S08 = Found [2], looking for WS (WS=S09)
+			// S09 = Found [2000 ], looking for [-]/quote/NUM ([-]=S10, [']=S11, ["]=S12, NUM=S13)
+			// S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
+			// S11 = Found [2000 - '], looking for ['] ([']=S01)
+			// S12 = Found [2000 - "], looking for ["] (["]=S01)
+			// S13 = Found [2000 - 2], looking for WS (WS=S01)
+
+			StateMachineState state = S01;
+			int mark = 0;
+			Equality eq = Equality.NONE;
+			String s1 = null, s2 = null;
+
+			int i;
+			char c = 0;
+			for (i = 0; i < s.trim().length(); i++) {
+				c = s.charAt(i);
+				if (state == S01) {
+					// S01 = Looking for [>]/[<]/quote/NUM ([>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
+					if (WS.contains(c)) {
+						state = S01;
+					} else if (c == '>') {
+						state = S02;
+						eq = Equality.GT;
+					} else if (c == '<') {
+						state = S03;
+						eq = Equality.LT;
+					} else if (c == '\'') {
+						state = S05;
+						mark = i+1;
+					} else if (c == '"') {
+						state = S06;
+						mark = i+1;
+					} else if (DT.contains(c)) {
+						state = S08;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S02) {
+					// S02 = Found [>], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
+					if (WS.contains(c)) {
+						state = S02;
+					} else if (c == '=') {
+						state = S04;
+						eq = Equality.GTE;
+					} else if (c == '\'') {
+						state = S05;
+						mark = i+1;
+					} else if (c == '"') {
+						state = S06;
+						mark = i+1;
+					} else if (DT.contains(c)) {
+						state = S08;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S03) {
+					// S03 = Found [<], looking for [=]/quote/NUM ([=]=S04, [']=S05, ["]=S06, NUM=S08)
+					if (WS.contains(c)) {
+						state = S03;
+					} else if (c == '=') {
+						state = S04;
+						eq = Equality.LTE;
+					} else if (c == '\'') {
+						state = S05;
+						mark = i+1;
+					} else if (c == '"') {
+						state = S06;
+						mark = i+1;
+					} else if (DT.contains(c)) {
+						state = S08;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S04) {
+					// S04 = Found [>=] or [<=], looking for quote/NUM ([']=S05, ["]=S06, NUM=S08)
+					if (WS.contains(c)) {
+						state = S04;
+					} else if (c == '\'') {
+						state = S05;
+						mark = i+1;
+					} else if (c == '"') {
+						state = S06;
+						mark = i+1;
+					} else if (DT.contains(c)) {
+						state = S08;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S05) {
+					// S05 = Found ['], looking for ['] ([']=S07)
+					if (c == '\'') {
+						state = S07;
+						s1 = s.substring(mark, i);
+					}
+				} else if (state == S06) {
+					// S06 = Found ["], looking for ["] (["]=S07)
+					if (c == '"') {
+						state = S07;
+						s1 = s.substring(mark, i);
+					}
+				} else if (state == S07) {
+					// S07 = Found [123"] or [123'], looking for WS (WS=S09)
+					if (WS.contains(c)) {
+						state = S09;
+					} else if (c == '-') {
+						state = S10;
+					} else {
+						break;
+					}
+				} else if (state == S08) {
+					// S08 = Found [1], looking for WS (WS=S09)
+					if (WS.contains(c)) {
+						state = S09;
+						s1 = s.substring(mark, i);
+					}
+				} else if (state == S09) {
+					// S09 = Found [2000 ], looking for [-]/[>]/[<]/quote/NUM ([-]=S10, [>]=S02, [<]=S03, [']=S05, ["]=S06, NUM=S08)
+					if (WS.contains(c)) {
+						state = S09;
+					} else if (c == '-') {
+						state = S10;
+					} else if (c == '>') {
+						state = S02;
+						l.add(new TimestampRange(f, eq, s1));
+						eq = Equality.GT;
+						s1 = null;
+					} else if (c == '<') {
+						state = S03;
+						l.add(new TimestampRange(f, eq, s1));
+						eq = Equality.LT;
+						s1 = null;
+					} else if (c == '\'') {
+						state = S05;
+						l.add(new TimestampRange(f, eq, s1));
+						mark = i+1;
+						eq = null;
+						s1 = null;
+					} else if (c == '"') {
+						state = S06;
+						l.add(new TimestampRange(f, eq, s1));
+						mark = i+1;
+						eq = null;
+						s1 = null;
+					} else if (DT.contains(c)) {
+						state = S08;
+						l.add(new TimestampRange(f, eq, s1));
+						eq = null;
+						s1 = null;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S10) {
+					// S10 = Found [2000 -], looking for quote/NUM ([']=S11, ["]=S12, NUM=S13)
+					if (WS.contains(c)) {
+						state = S10;
+					} else if (c == '\'') {
+						state = S11;
+						mark = i+1;
+					} else if (c == '"') {
+						state = S12;
+						mark = i+1;
+					} else if (DT.contains(c)) {
+						state = S13;
+						mark = i;
+					} else {
+						break;
+					}
+				} else if (state == S11) {
+					// S11 = Found [2000 - '], looking for ['] ([']=S01)
+					if (c == '\'') {
+						state = S01;
+						s2 = s.substring(mark, i);
+						l.add(new TimestampRange(f, s1, s2));
+						s1 = null;
+						s2 = null;
+					}
+				} else if (state == S12) {
+					// S12 = Found [2000 - "], looking for ["] (["]=S01)
+					if (c == '"') {
+						state = S01;
+						s2 = s.substring(mark, i);
+						l.add(new TimestampRange(f, s1, s2));
+						s1 = null;
+						s2 = null;
+					}
+				} else /* (state == S13) */ {
+					// S13 = Found [2000 - 2], looking for WS (WS=S01)
+					if (WS.contains(c)) {
+						state = S01;
+						s2 = s.substring(mark, i);
+						l.add(new TimestampRange(f, s1, s2));
+						s1 = null;
+						s2 = null;
+					}
+				}
+			}
+
+			if (i != s.length())
+				throw new PatternException("Invalid range pattern ({0}): pattern=[{1}], pos=[{2}], char=[{3}]", state, s, i, c);
+
+			if (state == S01) {
+				// No tokens found.
+			} else if (state == S02 || state == S03 || state == S04 || state == S05 || state == S06 || state == S10 || state == S11 || state == S12) {
+				System.err.println("state=["+state+"]");
+				throw new PatternException("Invalid range pattern (E{0}): {1}", state, s);
+			} else if (state == S07) {
+				l.add(new TimestampRange(f, eq, s1));
+			} else if (state == S08) {
+				s1 = s.substring(mark).trim();
+				l.add(new TimestampRange(f, eq, s1));
+			} else /* (state == S13) */ {
+				s2 = s.substring(mark).trim();
+				l.add(new TimestampRange(f, s1, s2));
+			}
+
+			ranges = l.toArray(new TimestampRange[l.size()]);
+		}
+
+		@Override
+		public boolean matches(ClassMeta<?> cm, Object o) {
+			if (ranges.length == 0) return true;
+
+			Calendar c = null;
+			if (cm.isCalendar())
+				c = (Calendar)o;
+			else {
+				c = Calendar.getInstance();
+				c.setTime((Date)o);
+			}
+			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(SimpleDateFormat[] formats, String start, String end) {
+			CalendarP start1 = parseDate(formats, start);
+			CalendarP end1 = parseDate(formats, end);
+			this.start = start1.copy().roll(MILLISECOND, -1).getCalendar();
+			this.end = end1.roll(1).getCalendar();
+		}
+
+		public TimestampRange(SimpleDateFormat[] formats, Equality eq, String singleDate) {
+			System.err.println("eq=["+eq+"], singleDate=["+singleDate+"]");
+
+			CalendarP singleDate1 = parseDate(formats, singleDate);
+			if (eq == Equality.GT) {
+				this.start = singleDate1.roll(1).roll(MILLISECOND, -1).getCalendar();
+				this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
+			} else if (eq == Equality.LT) {
+				this.start = new CalendarP(new Date(0), 0).getCalendar();
+				this.end = singleDate1.getCalendar();
+			} else if (eq == Equality.GTE) {
+				this.start = singleDate1.roll(MILLISECOND, -1).getCalendar();
+				this.end = new CalendarP(new Date(Long.MAX_VALUE), 0).getCalendar();
+			} else if (eq == Equality.LTE) {
+				this.start = new CalendarP(new Date(0), 0).getCalendar();
+				this.end = singleDate1.roll(1).getCalendar();
+			} else {
+				this.start = singleDate1.copy().roll(MILLISECOND, -1).getCalendar();
+				this.end = singleDate1.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 representing a timestamp.
+	 */
+	static CalendarP parseDate(SimpleDateFormat[] formats, String seg) {
+		ParsePosition pp = new ParsePosition(0);
+		for (int i = 0; i < formats.length; i++) {
+			SimpleDateFormat f = formats[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))
+					return new CalendarP(d, getPrecisionField(f.toPattern()));
+			}
+		}
+
+		throw new FormattedRuntimeException("Invalid date encountered:  ''{0}''", seg);
+	}
+
+	/**
+	 * 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;
+		}
+	}
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/ViewArgs.java
similarity index 64%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java
copy to juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/ViewArgs.java
index f01d403..6061465 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/pojotools/ViewArgs.java
@@ -10,40 +10,51 @@
 // * "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.rest.widget;
+package org.apache.juneau.pojotools;
 
-import org.apache.juneau.rest.*;
+import static java.util.Collections.*;
+
+import java.util.*;
 
 /**
- * Widget that returns a menu-item drop-down form for entering search/view/sort arguments.
- *
- * <p>
- * The variable it resolves is <js>"$W{QueryMenuItem}"</js>.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.HtmlDocAnnotation.PredefinedWidgets}
- * </ul>
+ * Encapsulates arguments for the {@link PojoViewer} class.
  */
-public class QueryMenuItem extends MenuItemWidget {
+public class ViewArgs {
+
+	private final List<String> view;
 
 	/**
-	 * Returns CSS for the tooltips.
+	 * Constructor.
+	 *
+	 * @param viewArgs
+	 * 	View arguments.
+	 * 	<br>Values are column names.
 	 */
-	@Override
-	public String getStyle(RestRequest req) throws Exception {
-		return super.getStyle(req)
-			+ "\n"
-			+ loadStyle("QueryMenuItem.css");
+	public ViewArgs(String...viewArgs) {
+		this(Arrays.asList(viewArgs));
 	}
 
-	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) throws Exception {
-		return "query";
+	/**
+	 * Constructor.
+	 *
+	 * @param viewArgs
+	 * 	View arguments.
+	 * 	<br>Values are column names.
+	 */
+	public ViewArgs(Collection<String> viewArgs) {
+		this.view = unmodifiableList(new ArrayList<>(viewArgs));
 	}
 
-	@Override /* MenuItemWidget */
-	public String getContent(RestRequest req) throws Exception {
-		return loadHtml("QueryMenuItem.html");
+	/**
+	 * The view columns.
+	 *
+	 * <p>
+	 * The view columns are the list of columns that should be displayed.
+	 * An empty list implies all columns should be displayed.
+	 *
+	 * @return An unmodifiable list of columns to view.
+	 */
+	public List<String> getView() {
+		return view;
 	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java
index 4d8c46d..5cd70aa 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ASet.java
@@ -37,6 +37,7 @@ public final class ASet<T> extends LinkedHashSet<T> {
 	 * @param t The initial values.
 	 * @return A new list.
 	 */
+	@SafeVarargs
 	public static <T> ASet<T> create(T...t) {
 		return new ASet<T>().appendAll(t);
 	}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/PojoQuery.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/PojoQuery.java
index 0fab839..3f05cf3 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/PojoQuery.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/PojoQuery.java
@@ -292,7 +292,7 @@ public final class PojoQuery {
 		}
 		if (cm.isMap()) {
 			Map m = (Map)o;
-			DelegateMap dm = new DelegateMap(session.getClassMetaForObject(m));
+			DelegateMap dm = new DelegateMap(m, session);
 			for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
 				dm.put(e.getKey().toString(), e.getValue());
 			return dm;
diff --git a/juneau-doc/docs/ReleaseNotes/8.0.1.html b/juneau-doc/docs/ReleaseNotes/8.0.1.html
index 9667543..7cebcb0 100644
--- a/juneau-doc/docs/ReleaseNotes/8.0.1.html
+++ b/juneau-doc/docs/ReleaseNotes/8.0.1.html
@@ -54,5 +54,7 @@
 			<li class='jac'>{@link oajr.BasicRestJena} - Non-servlet equivalent to {@link oajr.BasicRestServletJena}
 			<li class='jac'>{@link oajr.BasicRestJenaGroup} - Non-servlet equivalent to {@link oajr.BasicRestServletJenaGroup}
 		</ul>
+	<li>
+		HTML widgets now have access to the <code>RestResponse</code> object if they need access to the output bean.
 </ul>
 
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddOrderMenuItem.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddOrderMenuItem.java
index 16a0a9c..fd35548 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddOrderMenuItem.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddOrderMenuItem.java
@@ -29,17 +29,17 @@ import org.apache.juneau.rest.widget.*;
 public class AddOrderMenuItem extends MenuItemWidget {
 
 	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) throws Exception {
+	public String getLabel(RestRequest req, RestResponse res) throws Exception {
 		return "add";
 	}
 
 	@Override /* MenuItemWidget */
-	public String getBeforeShowScript(RestRequest req) throws Exception {
+	public String getBeforeShowScript(RestRequest req, RestResponse res) throws Exception {
 		return loadScript("AddOrderMenuItem_beforeShow.js");
 	}
 
 	@Override /* Widget */
-	public Object getContent(RestRequest req) throws Exception {
+	public Object getContent(RestRequest req, RestResponse res) throws Exception {
 
 		return div(
 			form().id("form").action("servlet:/store/order").method(POST).children(
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddPetMenuItem.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddPetMenuItem.java
index d5b9744..c438410 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddPetMenuItem.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/AddPetMenuItem.java
@@ -29,12 +29,12 @@ import org.apache.juneau.rest.widget.*;
 public class AddPetMenuItem extends MenuItemWidget {
 
 	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) throws Exception {
+	public String getLabel(RestRequest req, RestResponse res) throws Exception {
 		return "add";
 	}
 
 	@Override /* Widget */
-	public Object getContent(RestRequest req) throws Exception {
+	public Object getContent(RestRequest req, RestResponse res) throws Exception {
 		return div(
 			form().id("form").action("servlet:/pet").method(POST).children(
 				table(
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/UploadPhotoMenuItem.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/UploadPhotoMenuItem.java
index 60fb02b..d5b8cf0 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/UploadPhotoMenuItem.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/petstore/rest/UploadPhotoMenuItem.java
@@ -29,12 +29,12 @@ import org.apache.juneau.rest.widget.*;
 public class UploadPhotoMenuItem extends MenuItemWidget {
 
 	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) throws Exception {
+	public String getLabel(RestRequest req, RestResponse res) throws Exception {
 		return "upload";
 	}
 
 	@Override /* Widget */
-	public Object getContent(RestRequest req) throws Exception {
+	public Object getContent(RestRequest req, RestResponse res) throws Exception {
 		return div(
 			form().id("form").action("servlet:/upload").method(POST).enctype("multipart/form-data").children(
 				table(
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
index a844db3..2c524f9 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
@@ -332,9 +332,10 @@ public class BasicRestCallHandler implements RestCallHandler {
 	 * @return The session objects for that request.
 	 */
 	@Override /* RestCallHandler */
-	public Map<String,Object> getSessionObjects(RestRequest req) {
+	public Map<String,Object> getSessionObjects(RestRequest req, RestResponse res) {
 		Map<String,Object> m = new HashMap<>();
 		m.put(RequestVar.SESSION_req, req);
+		m.put(RequestVar.SESSION_res, res);
 		return m;
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
index d45be08..bc147ba 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
@@ -103,7 +103,8 @@ public interface RestCallHandler {
 	 * Returns the session objects for the specified request.
 	 *
 	 * @param req The REST request.
+	 * @param res The REST response.
 	 * @return The session objects for that request.
 	 */
-	public Map<String,Object> getSessionObjects(RestRequest req);
+	public Map<String,Object> getSessionObjects(RestRequest req, RestResponse res);
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 32a3e60..30cd36c 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -2920,11 +2920,11 @@ public final class RestContext extends BeanContext {
 	 *
 	 * Widgets resolve the following variables:
 	 * <ul class='spaced-list'>
-	 * 	<li><js>"$W{name}"</js> - Contents returned by {@link Widget#getHtml(RestRequest)}.
-	 * 	<li><js>"$W{name.script}"</js> - Contents returned by {@link Widget#getScript(RestRequest)}.
+	 * 	<li><js>"$W{name}"</js> - Contents returned by {@link Widget#getHtml(RestRequest,RestResponse)}.
+	 * 	<li><js>"$W{name.script}"</js> - Contents returned by {@link Widget#getScript(RestRequest,RestResponse)}.
 	 * 		<br>The script contents are automatically inserted into the <xt>&lt;head/script&gt;</xt> section
 	 * 			 in the HTML page.
-	 * 	<li><js>"$W{name.style}"</js> - Contents returned by {@link Widget#getStyle(RestRequest)}.
+	 * 	<li><js>"$W{name.style}"</js> - Contents returned by {@link Widget#getStyle(RestRequest,RestResponse)}.
 	 * 		<br>The styles contents are automatically inserted into the <xt>&lt;head/style&gt;</xt> section
 	 * 			 in the HTML page.
 	 * </ul>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 14fe47d..114d0e8 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -111,6 +111,7 @@ public final class RestRequest extends HttpServletRequestWrapper {
 	private Swagger swagger;
 	private SerializerSessionArgs serializerSessionArgs;
 	private ParserSessionArgs parserSessionArgs;
+	private RestResponse res;
 
 	/**
 	 * Constructor.
@@ -1315,7 +1316,7 @@ public final class RestRequest extends HttpServletRequestWrapper {
 	 */
 	public VarResolverSession getVarResolverSession() {
 		if (varSession == null)
-			varSession = context.getVarResolver().createSession(context.getCallHandler().getSessionObjects(this));
+			varSession = context.getVarResolver().createSession(context.getCallHandler().getSessionObjects(this, context.getResponse()));
 		return varSession;
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
index 36728ee..52dda82 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/HtmlDoc.java
@@ -557,13 +557,13 @@ public @interface HtmlDoc {
 	 *
 	 * <ul class='spaced-list'>
 	 * 	<li>
-	 * 		<js>"$W{name}"</js> - Contents returned by {@link Widget#getHtml(RestRequest)}.
+	 * 		<js>"$W{name}"</js> - Contents returned by {@link Widget#getHtml(RestRequest,RestResponse)}.
 	 * 	<li>
-	 * 		<js>"$W{name.script}"</js> - Contents returned by {@link Widget#getScript(RestRequest)}.
+	 * 		<js>"$W{name.script}"</js> - Contents returned by {@link Widget#getScript(RestRequest,RestResponse)}.
 	 * 		<br>The script contents are automatically inserted into the <xt>&lt;head/script&gt;</xt> section
 	 * 			 in the HTML page.
 	 * 	<li>
-	 * 		<js>"$W{name.style}"</js> - Contents returned by {@link Widget#getStyle(RestRequest)}.
+	 * 		<js>"$W{name.style}"</js> - Contents returned by {@link Widget#getStyle(RestRequest,RestResponse)}.
 	 * 		<br>The styles contents are automatically inserted into the <xt>&lt;head/style&gt;</xt> section
 	 * 			 in the HTML page.
 	 * </ul>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestVar.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestVar.java
index cc589b7..b6cf13d 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestVar.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestVar.java
@@ -69,6 +69,11 @@ public class RequestVar extends MultipartResolvingVar {
 	 */
 	public static final String SESSION_req = "req";
 
+	/**
+	 * The name of the session or context object that identifies the {@link RestResponse} object.
+	 */
+	public static final String SESSION_res = "res";
+
 
 	/** The name of this variable. */
 	public static final String NAME = "R";
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/WidgetVar.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/WidgetVar.java
index d55203f..f0d1288 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/WidgetVar.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/WidgetVar.java
@@ -43,6 +43,7 @@ public class WidgetVar extends SimpleVar {
 	 * The name of the session or context object that identifies the {@link RestRequest} object.
 	 */
 	private static final String SESSION_req = "req";
+	private static final String SESSION_res = "res";
 
 	/**
 	 * The name of this variable.
@@ -59,6 +60,7 @@ public class WidgetVar extends SimpleVar {
 	@Override /* Parameter */
 	public String resolve(VarResolverSession session, String key) throws Exception {
 		RestRequest req = session.getSessionObject(RestRequest.class, SESSION_req, true);
+		RestResponse res = session.getSessionObject(RestResponse.class, SESSION_res, true);
 		boolean isScript = false, isStyle = false;
 
 		if (key.endsWith(".script")) {
@@ -76,9 +78,9 @@ public class WidgetVar extends SimpleVar {
 			return "unknown-widget-"+key;
 
 		if (isScript)
-			return w.getScript(req);
+			return w.getScript(req, res);
 		if (isStyle)
-			return w.getStyle(req);
-		return w.getHtml(req);
+			return w.getStyle(req, res);
+		return w.getHtml(req, res);
 	}
 }
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ContentTypeMenuItem.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ContentTypeMenuItem.java
index af35301..bafb48b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ContentTypeMenuItem.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ContentTypeMenuItem.java
@@ -61,12 +61,12 @@ import org.apache.juneau.utils.*;
 public class ContentTypeMenuItem extends MenuItemWidget {
 
 	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) {
+	public String getLabel(RestRequest req, RestResponse res) {
 		return "content-type";
 	}
 
 	@Override /* MenuItemWidget */
-	public Div getContent(RestRequest req) {
+	public Div getContent(RestRequest req, RestResponse res) {
 		Div div = div();
 		Set<MediaType> l = new TreeSet<>();
 		for (Serializer s : req.getSerializers().getSerializers())
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/MenuItemWidget.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/MenuItemWidget.java
index ad2e8ea..23d6a60 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/MenuItemWidget.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/MenuItemWidget.java
@@ -35,7 +35,7 @@ public abstract class MenuItemWidget extends Widget {
 	 * Returns the Javascript needed for the show and hide actions of the menu item.
 	 */
 	@Override /* Widget */
-	public String getScript(RestRequest req) throws Exception {
+	public String getScript(RestRequest req, RestResponse res) throws Exception {
 		return loadScript("MenuItemWidget.js");
 	}
 
@@ -81,11 +81,12 @@ public abstract class MenuItemWidget extends Widget {
 	 * 	}
 	 * </p>
 	 *
-	 * @param req The current request.
+	 * @param req The HTTP request object.
+	 * @param res The HTTP response object.
 	 * @return Javascript code to execute, or <jk>null</jk> if there isn't any.
 	 * @throws Exception
 	 */
-	public String getBeforeShowScript(RestRequest req) throws Exception {
+	public String getBeforeShowScript(RestRequest req, RestResponse res) throws Exception {
 		return null;
 	}
 
@@ -93,33 +94,34 @@ public abstract class MenuItemWidget extends Widget {
 	 * Optional Javascript to execute immediately after a menu item is shown.
 	 *
 	 * <p>
-	 * Same as {@link #getBeforeShowScript(RestRequest)} except this Javascript gets executed after the popup dialog has become visible.
+	 * Same as {@link #getBeforeShowScript(RestRequest,RestResponse)} except this Javascript gets executed after the popup dialog has become visible.
 	 *
-	 * @param req The current request.
+	 * @param req The HTTP request object.
+	 * @param res The HTTP response object.
 	 * @return Javascript code to execute, or <jk>null</jk> if there isn't any.
 	 * @throws Exception
 	 */
-	public String getAfterShowScript(RestRequest req) throws Exception {
+	public String getAfterShowScript(RestRequest req, RestResponse res) throws Exception {
 		return null;
 	}
 
 	/**
 	 * Defines a <js>"menu-item"</js> class that needs to be used on the outer element of the HTML returned by the
-	 * {@link #getHtml(RestRequest)} method.
+	 * {@link #getHtml(RestRequest,RestResponse)} method.
 	 */
 	@Override /* Widget */
-	public String getStyle(RestRequest req) throws Exception {
+	public String getStyle(RestRequest req, RestResponse res) throws Exception {
 		return loadStyle("MenuItemWidget.css");
 	}
 
 	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
+	public String getHtml(RestRequest req, RestResponse res) throws Exception {
 		StringBuilder sb = new StringBuilder();
 
 		// Need a unique number to define unique function names.
 		Integer id = null;
 
-		String pre = nullIfEmpty(getBeforeShowScript(req)), post = nullIfEmpty(getAfterShowScript(req));
+		String pre = nullIfEmpty(getBeforeShowScript(req, res)), post = nullIfEmpty(getAfterShowScript(req, res));
 
 		sb.append("\n<div class='menu-item'>");
 		if (pre != null || post != null) {
@@ -140,10 +142,10 @@ public abstract class MenuItemWidget extends Widget {
 		}
 		String onclick = (pre == null ? "" : "onPreShow"+id+"();") + "menuClick(this);" + (post == null ? "" : "onPostShow"+id+"();");
 		sb.append(""
-			+ "\n\t<a onclick='"+onclick+"'>"+getLabel(req)+"</a>"
+			+ "\n\t<a onclick='"+onclick+"'>"+getLabel(req, res)+"</a>"
 			+ "\n<div class='popup-content'>"
 		);
-		Object o = getContent(req);
+		Object o = getContent(req, res);
 		if (o instanceof Reader) {
 			try (Reader r = (Reader)o; Writer w = new StringBuilderWriter(sb)) {
 				IOUtils.pipe(r, w);
@@ -177,15 +179,17 @@ public abstract class MenuItemWidget extends Widget {
 	 * The label for the menu item as it's rendered in the menu bar.
 	 *
 	 * @param req The HTTP request object.
+	 * @param res The HTTP response object.
 	 * @return The menu item label.
 	 * @throws Exception
 	 */
-	public abstract String getLabel(RestRequest req) throws Exception;
+	public abstract String getLabel(RestRequest req, RestResponse res) throws Exception;
 
 	/**
 	 * The content of the popup.
 	 *
 	 * @param req The HTTP request object.
+	 * @param res The HTTP response object.
 	 * @return
 	 * 	The content of the popup.
 	 * 	<br>Can be any of the following types:
@@ -197,5 +201,5 @@ public abstract class MenuItemWidget extends Widget {
 	 * 	</ul>
 	 * @throws Exception
 	 */
-	public abstract Object getContent(RestRequest req) throws Exception;
+	public abstract Object getContent(RestRequest req, RestResponse res) throws Exception;
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByApache.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByApache.java
index a237919..cb68a0f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByApache.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByApache.java
@@ -53,7 +53,7 @@ public class PoweredByApache extends Widget {
 	 * Returns an Apache image tag hyperlinked to <js>"http://apache.org"</js>
 	 */
 	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
+	public String getHtml(RestRequest req, RestResponse res) throws Exception {
 		UriResolver r = req.getUriResolver();
 		return "<a href='http://apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/asf.png")+"'>";
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
index 5231a47..9b2d5c5 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/PoweredByJuneau.java
@@ -33,7 +33,7 @@ public class PoweredByJuneau extends Widget {
 	 * Returns an Apache Juneau image tag hyperlinked to <js>"http://juneau.apache.org"</js>
 	 */
 	@Override /* Widget */
-	public String getHtml(RestRequest req) throws Exception {
+	public String getHtml(RestRequest req, RestResponse res) throws Exception {
 		UriResolver r = req.getUriResolver();
 		return "<a href='http://juneau.apache.org'><img style='float:right;padding-right:20px;height:32px' src='"+r.resolve("servlet:/htdocs/images/juneau.png")+"'>";
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java
index f01d403..d14f142 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/QueryMenuItem.java
@@ -31,19 +31,19 @@ public class QueryMenuItem extends MenuItemWidget {
 	 * Returns CSS for the tooltips.
 	 */
 	@Override
-	public String getStyle(RestRequest req) throws Exception {
-		return super.getStyle(req)
+	public String getStyle(RestRequest req, RestResponse res) throws Exception {
+		return super.getStyle(req, res)
 			+ "\n"
 			+ loadStyle("QueryMenuItem.css");
 	}
 
 	@Override /* MenuItemWidget */
-	public String getLabel(RestRequest req) throws Exception {
+	public String getLabel(RestRequest req, RestResponse res) throws Exception {
 		return "query";
 	}
 
 	@Override /* MenuItemWidget */
-	public String getContent(RestRequest req) throws Exception {
+	public String getContent(RestRequest req, RestResponse res) throws Exception {
 		return loadHtml("QueryMenuItem.html");
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ThemeMenuItem.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ThemeMenuItem.java
index b9e495a..7ce1c68 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ThemeMenuItem.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/ThemeMenuItem.java
@@ -34,12 +34,12 @@ public class ThemeMenuItem extends MenuItemWidget {
 	private static final String[] BUILT_IN_STYLES = {"devops", "light", "original", "dark"};
 
 	@Override /* Widget */
-	public String getLabel(RestRequest req) {
+	public String getLabel(RestRequest req, RestResponse res) {
 		return "themes";
 	}
 
 	@Override /* MenuItemWidget */
-	public Div getContent(RestRequest req) throws Exception {
+	public Div getContent(RestRequest req, RestResponse res) throws Exception {
 		Div div = div();
 		for (String s : BUILT_IN_STYLES) {
 			java.net.URI uri = req.getUri(true, new AMap<String,String>().append("stylesheet", "htdocs/themes/"+s+".css"));
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/Widget.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/Widget.java
index 2b60969..85bb1e1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/Widget.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/widget/Widget.java
@@ -65,10 +65,11 @@ public abstract class Widget {
 	 * A returned value of <jk>null</jk> will cause nothing to be added to the page.
 	 *
 	 * @param req The HTTP request object.
+	 * @param res The current HTTP response.
 	 * @return The HTML content of this widget.
 	 * @throws Exception
 	 */
-	public String getHtml(RestRequest req) throws Exception {
+	public String getHtml(RestRequest req, RestResponse res) throws Exception {
 		return null;
 	}
 
@@ -79,10 +80,11 @@ public abstract class Widget {
 	 * A returned value of <jk>null</jk> will cause nothing to be added to the page.
 	 *
 	 * @param req The HTTP request object.
+	 * @param res The current HTTP response.
 	 * @return The Javascript needed by this widget.
 	 * @throws Exception
 	 */
-	public String getScript(RestRequest req) throws Exception {
+	public String getScript(RestRequest req, RestResponse res) throws Exception {
 		return null;
 	}
 
@@ -93,10 +95,11 @@ public abstract class Widget {
 	 * A returned value of <jk>null</jk> will cause nothing to be added to the page.
 	 *
 	 * @param req The HTTP request object.
+	 * @param res The current HTTP response.
 	 * @return The CSS styles needed by this widget.
 	 * @throws Exception
 	 */
-	public String getStyle(RestRequest req) throws Exception {
+	public String getStyle(RestRequest req, RestResponse res) throws Exception {
 		return null;
 	}
 
@@ -169,11 +172,12 @@ public abstract class Widget {
 	 * </ul>
 	 *
 	 * @param req The current HTTP request.
+	 * @param res The current HTTP response.
 	 * @param name Name of the desired resource.
 	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
 	 * @throws IOException
 	 */
-	protected String loadScriptWithVars(RestRequest req, String name) throws IOException {
+	protected String loadScriptWithVars(RestRequest req, RestResponse res, String name) throws IOException {
 		return req.getVarResolverSession().resolve(loadScript(name));
 	}
 
@@ -205,11 +209,12 @@ public abstract class Widget {
 	 * </ul>
 	 *
 	 * @param req The current HTTP request.
+	 * @param res The current HTTP response.
 	 * @param name Name of the desired resource.
 	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
 	 * @throws IOException
 	 */
-	protected String loadStyleWithVars(RestRequest req, String name) throws IOException {
+	protected String loadStyleWithVars(RestRequest req, RestResponse res, String name) throws IOException {
 		return req.getVarResolverSession().resolve(loadStyle(name));
 	}
 
@@ -241,11 +246,12 @@ public abstract class Widget {
 	 * </ul>
 	 *
 	 * @param req The current HTTP request.
+	 * @param res The current HTTP response.
 	 * @param name Name of the desired resource.
 	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
 	 * @throws IOException
 	 */
-	protected String loadHtmlWithVars(RestRequest req, String name) throws IOException {
+	protected String loadHtmlWithVars(RestRequest req, RestResponse res, String name) throws IOException {
 		return req.getVarResolverSession().resolve(loadHtml(name));
 	}
 }