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 2017/05/21 05:17:19 UTC

[2/2] incubator-juneau git commit: HtmlRender support.

HtmlRender support.

Project: http://git-wip-us.apache.org/repos/asf/incubator-juneau/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-juneau/commit/e1a50566
Tree: http://git-wip-us.apache.org/repos/asf/incubator-juneau/tree/e1a50566
Diff: http://git-wip-us.apache.org/repos/asf/incubator-juneau/diff/e1a50566

Branch: refs/heads/master
Commit: e1a505668d7aa4d2aa816f697a8203dfbc4bd8c8
Parents: 9b04bb9
Author: JamesBognar <ja...@apache.org>
Authored: Sun May 21 01:17:14 2017 -0400
Committer: JamesBognar <ja...@apache.org>
Committed: Sun May 21 01:17:14 2017 -0400

----------------------------------------------------------------------
 .../org/apache/juneau/jena/RdfSerializer.java   |   4 +-
 .../juneau/jena/RdfSerializerSession.java       |  14 +-
 .../apache/juneau/utils/StringUtilsTest.java    |  28 +
 .../utils/UriContextResolutionComboTest.java    | 630 +++++++++++++++++++
 .../juneau/utils/UriContextUriComboTest.java    | 234 +++++++
 .../org/apache/juneau/xml/XmlContentTest.java   |   8 +-
 .../main/java/org/apache/juneau/BeanMap.java    |  11 +
 .../main/java/org/apache/juneau/UriContext.java | 421 +++++++++++++
 .../java/org/apache/juneau/UriRelativity.java   |  29 +
 .../java/org/apache/juneau/UriResolution.java   |  34 +
 .../apache/juneau/csv/CsvSerializerSession.java |  14 +-
 .../juneau/html/HtmlBeanPropertyMeta.java       |  78 ++-
 .../apache/juneau/html/HtmlDocSerializer.java   |   4 +-
 .../juneau/html/HtmlDocSerializerSession.java   |  16 +-
 .../java/org/apache/juneau/html/HtmlRender.java | 149 +++++
 .../juneau/html/HtmlSchemaDocSerializer.java    |   4 +-
 .../org/apache/juneau/html/HtmlSerializer.java  |  38 +-
 .../juneau/html/HtmlSerializerSession.java      |  16 +-
 .../java/org/apache/juneau/html/HtmlWriter.java |   7 +-
 .../apache/juneau/html/SimpleHtmlWriter.java    |   2 +-
 .../org/apache/juneau/html/annotation/Html.java |  35 ++
 .../juneau/html/doc-files/HtmlRender_1.png      | Bin 0 -> 60161 bytes
 .../org/apache/juneau/internal/StringUtils.java | 102 +++
 .../juneau/json/JsonSchemaSerializer.java       |   4 +-
 .../org/apache/juneau/json/JsonSerializer.java  |   4 +-
 .../juneau/json/JsonSerializerSession.java      |  16 +-
 .../java/org/apache/juneau/json/JsonWriter.java |   7 +-
 .../juneau/msgpack/MsgPackSerializer.java       |   4 +-
 .../msgpack/MsgPackSerializerSession.java       |  14 +-
 .../apache/juneau/serializer/Serializer.java    |  12 +-
 .../juneau/serializer/SerializerSession.java    |  45 +-
 .../juneau/serializer/SerializerWriter.java     |  12 +-
 .../juneau/serializer/WriterSerializer.java     |   2 +-
 .../org/apache/juneau/uon/UonSerializer.java    |   4 +-
 .../apache/juneau/uon/UonSerializerSession.java |  16 +-
 .../java/org/apache/juneau/uon/UonWriter.java   |   7 +-
 .../urlencoding/UrlEncodingSerializer.java      |   6 +-
 .../UrlEncodingSerializerSession.java           |  14 +-
 .../apache/juneau/xml/XmlSchemaSerializer.java  |   8 +-
 .../org/apache/juneau/xml/XmlSerializer.java    |   4 +-
 .../apache/juneau/xml/XmlSerializerSession.java |  16 +-
 .../java/org/apache/juneau/xml/XmlWriter.java   |   7 +-
 .../src/main/javadoc/doc-files/HtmlRender_1.png | Bin 0 -> 60161 bytes
 juneau-core/src/main/javadoc/overview.html      |   3 +
 .../juneau/examples/rest/FileSpaceResource.java | 148 +++++
 .../juneau/examples/rest/RootResources.java     |   1 +
 .../apache/juneau/examples/rest/htdocs/ok.png   | Bin 0 -> 455 bytes
 .../juneau/examples/rest/htdocs/severe.png      | Bin 0 -> 335 bytes
 .../juneau/examples/rest/htdocs/warning.png     | Bin 0 -> 444 bytes
 .../juneau/rest/client/RestClientBuilder.java   |   2 +-
 .../apache/juneau/rest/jaxrs/BaseProvider.java  |   4 +-
 .../juneau/rest/test/HtmlPropertiesTest.java    |   1 -
 .../org/apache/juneau/rest/RestRequest.java     |  17 +
 .../juneau/rest/response/DefaultHandler.java    |   4 +-
 54 files changed, 2112 insertions(+), 148 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java b/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
index 37b7d2c..734f38a 100644
--- a/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
+++ b/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
@@ -450,7 +450,7 @@ public class RdfSerializer extends WriterSerializer {
 	//--------------------------------------------------------------------------------
 
 	@Override /* Serializer */
-	public RdfSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		return new RdfSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	public RdfSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		return new RdfSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializerSession.java b/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializerSession.java
index 199f144..55060ef 100644
--- a/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializerSession.java
+++ b/juneau-core-rdf/src/main/java/org/apache/juneau/jena/RdfSerializerSession.java
@@ -54,19 +54,21 @@ public final class RdfSerializerSession extends SerializerSession {
 	 * Create a new session using properties specified in the context.
 	 *
 	 * @param ctx The context creating this session object.
-	 * The context contains all the configuration settings for this object.
+	 * 	The context contains all the configuration settings for this object.
 	 * @param output The output object.  See {@link JsonSerializerSession#getWriter()} for valid class types.
 	 * @param op The override properties.
-	 * These override any context properties defined in the context.
+	 * 	These override any context properties defined in the context.
 	 * @param javaMethod The java method that called this serializer, usually the method in a REST servlet.
 	 * @param locale The session locale.
-	 * If <jk>null</jk>, then the locale defined on the context is used.
+	 * 	If <jk>null</jk>, then the locale defined on the context is used.
 	 * @param timeZone The session timezone.
-	 * If <jk>null</jk>, then the timezone defined on the context is used.
+	 * 	If <jk>null</jk>, then the timezone defined on the context is used.
 	 * @param mediaType The session media type (e.g. <js>"application/json"</js>).
+	 * @param uriContext The URI context.
+	 * 	Identifies the current request URI used for resolution of URIs to absolute or root-relative form. 
 	 */
-	protected RdfSerializerSession(RdfSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		super(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	protected RdfSerializerSession(RdfSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		super(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 		ObjectMap jenaSettings = new ObjectMap();
 		jenaSettings.put("rdfXml.tab", isUseWhitespace() ? 2 : 0);
 		jenaSettings.put("rdfXml.attributeQuoteChar", Character.toString(getQuoteChar()));

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
----------------------------------------------------------------------
diff --git a/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java b/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
index f74d75e..c7c3052 100755
--- a/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
+++ b/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
@@ -717,4 +717,32 @@ public class StringUtilsTest {
 		assertObjectEquals("{'a=':'1'}", splitMap("a\\==1", ',', '=', true));
 		assertObjectEquals("{'a\\\\':'1'}", splitMap("a\\\\=1", ',', '=', true));
 	}
+	
+	//====================================================================================================
+	// isAbsoluteUri(String)
+	//====================================================================================================
+	@Test
+	public void testIsAbsoluteUri() {
+		assertFalse(isAbsoluteUri(null));
+		assertFalse(isAbsoluteUri(""));
+		assertTrue(isAbsoluteUri("http://foo"));
+		assertTrue(isAbsoluteUri("x://x"));
+		assertFalse(isAbsoluteUri("xX://x"));
+		assertFalse(isAbsoluteUri("x ://x"));
+		assertFalse(isAbsoluteUri("x: //x"));
+		assertFalse(isAbsoluteUri("x:/ /x"));
+		assertFalse(isAbsoluteUri("x:x//x"));
+		assertFalse(isAbsoluteUri("x:/x/x"));
+	}
+
+	//====================================================================================================
+	// getAuthorityUri(String)
+	//====================================================================================================
+	@Test
+	public void testGetAuthorityUri() {
+		assertEquals("http://foo", getAuthorityUri("http://foo"));
+		assertEquals("http://foo:123", getAuthorityUri("http://foo:123"));
+		assertEquals("http://foo:123", getAuthorityUri("http://foo:123/"));
+		assertEquals("http://foo:123", getAuthorityUri("http://foo:123/bar"));
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextResolutionComboTest.java
----------------------------------------------------------------------
diff --git a/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextResolutionComboTest.java b/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextResolutionComboTest.java
new file mode 100644
index 0000000..e69a635
--- /dev/null
+++ b/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextResolutionComboTest.java
@@ -0,0 +1,630 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.utils;
+
+import static org.apache.juneau.TestUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.*;
+import org.junit.runner.*;
+import org.junit.runners.*;
+
+/**
+ * Verifies that the resolveUri() methods in UriContext work correctly.
+ */
+@RunWith(Parameterized.class)
+public class UriContextResolutionComboTest {
+
+	@Parameterized.Parameters
+	public static Collection<Object[]> getInput() {
+		return Arrays.asList(new Object[][] {
+
+			// Happy cases - All URL parts known.
+			{
+				input(
+					"Happy-1",
+					"http://host:port","/context","/resource","/path",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"Happy-2",
+					"http://host:port","/context","/resource","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"Happy-3",
+					"http://host:port","/context","/resource","/path",
+					"/foobar",
+					"http://host:port/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"Happy-4",
+					"http://host:port","/context","/resource","/path",
+					"/",
+					"http://host:port",
+					"/"
+				)
+			},
+			{
+				input(
+					"Happy-5",
+					"http://host:port","/context","/resource","/path",
+					"foobar",
+					"http://host:port/context/resource/foobar",
+					"/context/resource/foobar"
+				)
+			},
+			{
+				input(
+					"Happy-6",
+					"http://host:port","/context","/resource","/path",
+					"",
+					"http://host:port/context/resource/path",
+					"/context/resource/path"
+				)
+			},
+			{
+				input(
+					"Happy-7",
+					"http://host:port","/context","/resource","/path",
+					"context:/foo",
+					"http://host:port/context/foo",
+					"/context/foo"
+				)
+			},
+			{
+				input(
+					"Happy-8",
+					"http://host:port","/context","/resource","/path",
+					"context:/",
+					"http://host:port/context",
+					"/context"
+				)
+			},
+			{
+				input(
+					"Happy-9",
+					"http://host:port","/context","/resource","/path",
+					"servlet:/foo",
+					"http://host:port/context/resource/foo",
+					"/context/resource/foo"
+				)
+			},
+			{
+				input(
+					"Happy-10",
+					"http://host:port","/context","/resource","/path",
+					"servlet:/",
+					"http://host:port/context/resource",
+					"/context/resource"
+				)
+			},
+			
+			// Multiple context and resource parts
+			{
+				input(
+					"MultiContextResource-1",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-2",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-3",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"/foobar",
+					"http://host:port/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-4",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"/",
+					"http://host:port",
+					"/"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-5",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"foobar",
+					"http://host:port/c1/c2/r1/r2/p1/foobar",
+					"/c1/c2/r1/r2/p1/foobar"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-6",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"",
+					"http://host:port/c1/c2/r1/r2/p1/p2",
+					"/c1/c2/r1/r2/p1/p2"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-7",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"context:/foo",
+					"http://host:port/c1/c2/foo",
+					"/c1/c2/foo"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-8",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"context:/",
+					"http://host:port/c1/c2",
+					"/c1/c2"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-9",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"servlet:/foo",
+					"http://host:port/c1/c2/r1/r2/foo",
+					"/c1/c2/r1/r2/foo"
+				)
+			},
+			{
+				input(
+					"MultiContextResource-10",
+					"http://host:port","/c1/c2","/r1/r2","/p1/p2",
+					"servlet:/",
+					"http://host:port/c1/c2/r1/r2",
+					"/c1/c2/r1/r2"
+				)
+			},
+			
+			// No authority given
+			{
+				input(
+					"NoAuthority-1",
+					"","/context","/resource","/path",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthority-2",
+					"","/context","/resource","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"NoAuthority-3",
+					"","/context","/resource","/path",
+					"/foobar",
+					"/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthority-4",
+					"","/context","/resource","/path",
+					"/",
+					"/",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoAuthority-5",
+					"","/context","/resource","/path",
+					"foobar",
+					"/context/resource/foobar",
+					"/context/resource/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthority-6",
+					"","/context","/resource","/path",
+					"",
+					"/context/resource/path",
+					"/context/resource/path"
+				)
+			},
+			{
+				input(
+					"NoAuthority-7",
+					"","/context","/resource","/path",
+					"context:/foo",
+					"/context/foo",
+					"/context/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthority-8",
+					"","/context","/resource","/path",
+					"context:/",
+					"/context",
+					"/context"
+				)
+			},
+			{
+				input(
+					"NoAuthority-9",
+					"","/context","/resource","/path",
+					"servlet:/foo",
+					"/context/resource/foo",
+					"/context/resource/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthority-10",
+					"","/context","/resource","/path",
+					"servlet:/",
+					"/context/resource",
+					"/context/resource"
+				)
+			},
+			
+			// No authority or context given
+			{
+				input(
+					"NoAuthorityOrContext-1",
+					"","","/resource","/path",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-2",
+					"","","/resource","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-3",
+					"","","/resource","/path",
+					"/foobar",
+					"/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-4",
+					"","","/resource","/path",
+					"/",
+					"/",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-5",
+					"","","/resource","/path",
+					"foobar",
+					"/resource/foobar",
+					"/resource/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-6",
+					"","","/resource","/path",
+					"",
+					"/resource/path",
+					"/resource/path"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-7",
+					"","","/resource","/path",
+					"context:/foo",
+					"/foo",
+					"/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-8",
+					"","","/resource","/path",
+					"context:/",
+					"/",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-9",
+					"","","/resource","/path",
+					"servlet:/foo",
+					"/resource/foo",
+					"/resource/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContext-10",
+					"","","/resource","/path",
+					"servlet:/",
+					"/resource",
+					"/resource"
+				)
+			},
+
+			// No authority or context or resource given
+			{
+				input(
+					"NoAuthorityOrContextOrResource-1",
+					"","","","/path",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-2",
+					"","","","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-3",
+					"","","","/path",
+					"/foobar",
+					"/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-4",
+					"","","","/path",
+					"/",
+					"/",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-5",
+					"","","","/path",
+					"foobar",
+					"/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-6",
+					"","","","/path",
+					"",
+					"/path",
+					"/path"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-7",
+					"","","","/path",
+					"context:/foo",
+					"/foo",
+					"/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-8",
+					"","","","/path",
+					"context:/",
+					"/",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-9",
+					"","","","/path",
+					"servlet:/foo",
+					"/foo",
+					"/foo"
+				)
+			},
+			{
+				input(
+					"NoAuthorityOrContextOrResource-10",
+					"","","","/path",
+					"servlet:/",
+					"/",
+					"/"
+				)
+			},
+			
+			// No context or resource given.
+			{
+				input(
+					"NoContextOrResource-1",
+					"http://host:port","","","/path",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar",
+					"http://foo.com:123/foobar"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-2",
+					"http://host:port","","","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-3",
+					"http://host:port","","","/path",
+					"/foobar",
+					"http://host:port/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-4",
+					"http://host:port","","","/path",
+					"/",
+					"http://host:port",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-5",
+					"http://host:port","","","/path",
+					"foobar",
+					"http://host:port/foobar",
+					"/foobar"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-6",
+					"http://host:port","","","/path",
+					"",
+					"http://host:port/path",
+					"/path"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-7",
+					"http://host:port","","","/path",
+					"context:/foo",
+					"http://host:port/foo",
+					"/foo"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-8",
+					"http://host:port","","","/path",
+					"context:/",
+					"http://host:port",
+					"/"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-9",
+					"http://host:port","","","/path",
+					"servlet:/foo",
+					"http://host:port/foo",
+					"/foo"
+				)
+			},
+			{
+				input(
+					"NoContextOrResource-10",
+					"http://host:port","","","/path",
+					"servlet:/",
+					"http://host:port",
+					"/"
+				)
+			},
+		});		
+	}
+	
+	public static Input input(String label, String authority, String context, String resource, String path, String uri, String expectedAbsolute, String expectedRootRelative) {
+		return new Input(label, authority, context, resource, path, uri, expectedAbsolute, expectedRootRelative);
+	}
+	
+	public static class Input {
+		private final UriContext uriContext;
+		private final String label, uri, expectedAbsolute, expectedRootRelative;
+		
+		public Input(String label, String authority, String context, String resource, String path, String uri, String expectedAbsolute, String expectedRootRelative) {
+			this.label = label;
+			this.uriContext = new UriContext(authority, context, resource, path);
+			this.uri = uri;
+			this.expectedAbsolute = expectedAbsolute;
+			this.expectedRootRelative = expectedRootRelative;
+		}
+	}
+	
+	private Input in;
+	
+	public UriContextResolutionComboTest(Input in) throws Exception {
+		this.in = in;
+	}
+	
+	@Test
+	public void testAbsolute() {
+		assertEquals(in.expectedAbsolute, in.uriContext.resolveAbsolute(in.uri), "{0}: testAbsolute() failed", in.label);
+	}
+		
+	@Test
+	public void testRootRelative() {
+		assertEquals(in.expectedRootRelative, in.uriContext.resolveRootRelative(in.uri), "{0}: testRootRelative() failed", in.label);
+	}
+
+	@Test
+	public void testAbsoluteAppend() {
+		assertEquals(in.expectedAbsolute, in.uriContext.appendAbsolute(new StringBuilder(), in.uri).toString(), "{0}: testAbsolute() failed", in.label);
+	}
+		
+	@Test
+	public void testRootRelativeAppend() {
+		assertEquals(in.expectedRootRelative, in.uriContext.appendRootRelative(new StringBuilder(), in.uri).toString(), "{0}: testRootRelative() failed", in.label);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextUriComboTest.java
----------------------------------------------------------------------
diff --git a/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextUriComboTest.java b/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextUriComboTest.java
new file mode 100644
index 0000000..b3a3b60
--- /dev/null
+++ b/juneau-core-test/src/test/java/org/apache/juneau/utils/UriContextUriComboTest.java
@@ -0,0 +1,234 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.utils;
+
+import static org.apache.juneau.TestUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.*;
+import org.junit.runner.*;
+import org.junit.runners.*;
+
+/**
+ * Verifies that the getUri() methods in UriContext work correctly.
+ */
+@RunWith(Parameterized.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class UriContextUriComboTest {
+
+	@Parameterized.Parameters
+	public static Collection<Object[]> getInput() {
+		return Arrays.asList(new Object[][] {
+
+			// Happy cases - All URL parts known.
+			{
+				input(
+					"Happy-1",
+					"http://foo.com:123","/context","/resource","/path",
+					"http://foo.com:123",
+					"http://foo.com:123/context",
+					"http://foo.com:123/context/resource",
+					"http://foo.com:123/context/resource/path",
+					"/context",
+					"/context/resource",
+					"/context/resource/path"
+				)
+			},
+			{
+				input(
+					"Happy-2",
+					"http://foo.com:123","/c1/c2","/r1/r2","/p1/p2",
+					"http://foo.com:123",
+					"http://foo.com:123/c1/c2",
+					"http://foo.com:123/c1/c2/r1/r2",
+					"http://foo.com:123/c1/c2/r1/r2/p1/p2",
+					"/c1/c2",
+					"/c1/c2/r1/r2",
+					"/c1/c2/r1/r2/p1/p2"
+				)
+			},
+			{
+				input(
+					"NoAuthority-1",
+					"","/context","/resource","/path",
+					"/",
+					"/context",
+					"/context/resource",
+					"/context/resource/path",
+					"/context",
+					"/context/resource",
+					"/context/resource/path"
+				)
+			},
+			{
+				input(
+					"NoContext-1",
+					"http://foo.com:123","","/resource","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123/resource",
+					"http://foo.com:123/resource/path",
+					"/",
+					"/resource",
+					"/resource/path"
+				)
+			},
+			{
+				input(
+					"NoResource-1",
+					"http://foo.com:123","/context","","/path",
+					"http://foo.com:123",
+					"http://foo.com:123/context",
+					"http://foo.com:123/context",
+					"http://foo.com:123/context/path",
+					"/context",
+					"/context",
+					"/context/path"
+				)
+			},
+			{
+				input(
+					"NoPath-1",
+					"http://foo.com:123","/context","/resource","",
+					"http://foo.com:123",
+					"http://foo.com:123/context",
+					"http://foo.com:123/context/resource",
+					"http://foo.com:123/context/resource",
+					"/context",
+					"/context/resource",
+					"/context/resource"
+				)
+			},
+			{
+				input(
+					"NoAuthorityNoContext-1",
+					"","","/resource","/path",
+					"/",
+					"/",
+					"/resource",
+					"/resource/path",
+					"/",
+					"/resource",
+					"/resource/path"
+				)
+			},
+			{
+				input(
+					"NoContextNoResource-1",
+					"http://foo.com:123","","","/path",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123",
+					"http://foo.com:123/path",
+					"/",
+					"/",
+					"/path"
+				)
+			},
+			{
+				input(
+					"NoAuthorityNoContextNoResource-1",
+					"","","","/path",
+					"/",
+					"/",
+					"/",
+					"/path",
+					"/",
+					"/",
+					"/path"
+				)
+			},
+			{
+				input(
+					"Nothing-1",
+					"","","","",
+					"/",
+					"/",
+					"/",
+					"/",
+					"/",
+					"/",
+					"/"
+				)
+			},
+		});		
+	}
+	
+	public static Input input(String label, String authority, String context, String resource, String path, 
+			String eAbsoluteAuthority, String eAbsoluteContext, String eAbsoluteResource, String eAbsolutePath, 
+			String eRootRelativeContext, String eRootRelativeResource, String eRootRelativePath) {
+		return new Input(label, authority, context, resource, path, eAbsoluteAuthority, eAbsoluteContext, eAbsoluteResource, eAbsolutePath, eRootRelativeContext, eRootRelativeResource, eRootRelativePath);
+	}
+	
+	public static class Input {
+		private final UriContext uriContext;
+		private final String label, eAbsoluteAuthority, eAbsoluteContext, eAbsoluteResource, eAbsolutePath, eRootRelativeContext, eRootRelativeResource, eRootRelativePath;
+		
+		public Input(String label, String authority, String context, String resource, String path, 
+					String eAbsoluteAuthority, String eAbsoluteContext, String eAbsoluteResource, String eAbsolutePath, 
+					String eRootRelativeContext, String eRootRelativeResource, String eRootRelativePath) {
+			this.label = label;
+			this.uriContext = new UriContext(authority, context, resource, path);
+			this.eAbsoluteAuthority = eAbsoluteAuthority;
+			this.eAbsoluteContext = eAbsoluteContext;
+			this.eAbsoluteResource = eAbsoluteResource;
+			this.eAbsolutePath = eAbsolutePath;
+			this.eRootRelativeContext = eRootRelativeContext;
+			this.eRootRelativeResource = eRootRelativeResource;
+			this.eRootRelativePath = eRootRelativePath;
+		}
+	}
+	
+	private Input in;
+	
+	public UriContextUriComboTest(Input in) throws Exception {
+		this.in = in;
+	}
+	
+	@Test
+	public void a1_testAbsoluteAuthority() {
+		assertEquals(in.eAbsoluteAuthority, in.uriContext.getAbsoluteAuthority(), "{0}: testAbsoluteAuthority() failed", in.label);
+	}
+
+	@Test
+	public void a2_testAbsoluteContext() {
+		assertEquals(in.eAbsoluteContext, in.uriContext.getAbsoluteContextRoot(), "{0}: testAbsoluteContext() failed", in.label);
+	}
+	
+	@Test
+	public void a3_testAbsoluteResource() {
+		assertEquals(in.eAbsoluteResource, in.uriContext.getAbsoluteServletPath(), "{0}: testAbsoluteResource() failed", in.label);
+	}
+	
+	@Test
+	public void a4_testAbsolutePath() {
+		assertEquals(in.eAbsolutePath, in.uriContext.getAbsolutePathInfo(), "{0}: testAbsolutePath() failed", in.label);
+	}
+	
+	@Test
+	public void a5_testRootRelativeContext() {
+		assertEquals(in.eRootRelativeContext, in.uriContext.getRootRelativeContextRoot(), "{0}: testRootRelativeContext() failed", in.label);
+	}
+	
+	@Test
+	public void a6_testRootRelativeResource() {
+		assertEquals(in.eRootRelativeResource, in.uriContext.getRootRelativeServletPath(), "{0}: testRootRelativeResource() failed", in.label);
+	}
+	
+	@Test
+	public void a7_testRootRelativePath() {
+		assertEquals(in.eRootRelativePath, in.uriContext.getRootRelativePathInfo(), "{0}: testRootRelativePath() failed", in.label);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core-test/src/test/java/org/apache/juneau/xml/XmlContentTest.java
----------------------------------------------------------------------
diff --git a/juneau-core-test/src/test/java/org/apache/juneau/xml/XmlContentTest.java b/juneau-core-test/src/test/java/org/apache/juneau/xml/XmlContentTest.java
index 6f0738e..65e23e7 100755
--- a/juneau-core-test/src/test/java/org/apache/juneau/xml/XmlContentTest.java
+++ b/juneau-core-test/src/test/java/org/apache/juneau/xml/XmlContentTest.java
@@ -46,7 +46,7 @@ public class XmlContentTest {
 		t.f2 = null;
 
 		sw = new StringWriter();
-		session = s1.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null);
+		session = s1.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null, null);
 		s1.serialize(session, t);
 		r = sw.toString();
 		assertEquals("<A f1='f1'>_x0000_</A>", r);
@@ -54,7 +54,7 @@ public class XmlContentTest {
 		assertEqualObjects(t, t2);
 
 		sw = new StringWriter();
-		session = s2.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null);
+		session = s2.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null, null);
 		s2.serialize(session, t);
 		r = sw.toString();
 		assertEquals("<A f1='f1'>_x0000_</A>\n", r);
@@ -154,7 +154,7 @@ public class XmlContentTest {
 		t.f2 = null;
 
 		sw = new StringWriter();
-		session = s1.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null);
+		session = s1.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null, null);
 		s1.serialize(session, t);
 		r = sw.toString();
 		assertEquals("<A f1='f1'>_x0000_</A>", r);
@@ -162,7 +162,7 @@ public class XmlContentTest {
 		assertEqualObjects(t, t2);
 
 		sw = new StringWriter();
-		session = s2.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null);
+		session = s2.createSession(sw, new ObjectMap("{"+SERIALIZER_trimNullProperties+":false}"), null, null, null, null, null);
 		s2.serialize(session, t);
 		r = sw.toString();
 		assertEquals("<A f1='f1'>_x0000_</A>\n", r);

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/BeanMap.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/src/main/java/org/apache/juneau/BeanMap.java
index e6a1566..af5634e 100644
--- a/juneau-core/src/main/java/org/apache/juneau/BeanMap.java
+++ b/juneau-core/src/main/java/org/apache/juneau/BeanMap.java
@@ -440,6 +440,17 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
 	}
 
 	/**
+	 * Given a string containing variables of the form <code>"{property}"</code>, replaces those variables with
+	 * property values in this bean.
+	 *
+	 * @param s The string containing variables.
+	 * @return A new string with variables replaced, or the same string if no variables were found.
+	 */
+	public String resolveVars(String s) {
+		return StringUtils.replaceVars(s, this);
+	}
+
+	/**
 	 * Returns a simple collection of properties for this bean map.
 	 * @return A simple collection of properties for this bean map.
 	 */

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/UriContext.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/UriContext.java b/juneau-core/src/main/java/org/apache/juneau/UriContext.java
new file mode 100644
index 0000000..99ddd7f
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/UriContext.java
@@ -0,0 +1,421 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.io.*;
+
+/**
+ * Represents a URL broken into authority/context-root/servlet-path/path-info parts.
+ * <p>
+ * A typical request against a URL takes the following form:
+ * <p class='bcode'>
+ * 	http://host:port/context-root/servlet-path/path-info
+ * 	|   authority   |  context   |  resource  |  path  |
+ *  +--------------------------------------------------+
+ * </p>
+ * <p>
+ * This class allows you to convert URL strings to absolute (e.g. <js>"http://host:port/foo/bar"</js>) or root-relative
+ * 	(e.g. <js>"/foo/bar"</js>) URLs.
+ * <p>
+ * Two special protocols are used to represent context-root-relative and servlet-relative URIs:
+ * 	<js>"context:/"</js> and <js>"servlet:/"</js>.
+ *
+ * The following list shows the types of URLs that can be resolved with this class:
+ * <ul>
+ * 	<li><js>"foo://foo"</js> - Absolute URI.
+ * 	<li><js>"/foo"</js> - Root-relative URI.
+ * 	<li><js>"/"</js> - Root URI.
+ * 	<li><js>"context:/foo"</js> - Context-root-relative URI.
+ * 	<li><js>"context:/"</js> - Context-root URI.
+ * 	<li><js>"servlet:/foo"</js> - Servlet-path-relative URI.
+ * 	<li><js>"servlet:/"</js> - Servlet-path URI.
+ * 	<li><js>"foo"</js> - Path-info-relative URI.
+ * 	<li><js>""</js> - Path-info URI.
+ * </ul>
+ *
+ * The following class shows how
+ */
+public class UriContext {
+
+	private final String authority, contextRoot, servletPath, pathInfo, parentPath;
+
+	// Lazy-initialized fields.
+	private String aContextRoot, rContextRoot, aServletPath, rResource, aPathInfo, rPath;
+
+
+	/**
+	 * Constructor.
+	 * <p>
+	 * Leading and trailing slashes are trimmed of all parameters.
+	 * <p>
+	 * Any parameter can be <jk>null</jk>.  Blanks and nulls are equivalent.
+	 *
+	 * @param authority - The authority portion of URL (e.g. <js>"http://hostname:port"</js>)
+	 * @param contextRoot - The context root of the application (e.g. <js>"/context-root"</js>, or <js>"context-root"</js>)
+	 * @param servletPath - The servlet path (e.g. <js>"/servlet-path"</js>, or <js>"servlet-path"</js>)
+	 * @param pathInfo - The path info (e.g. <js>"/path-info"</js>, or <js>"path-info"</js>)
+	 */
+	public UriContext(String authority, String contextRoot, String servletPath, String pathInfo) {
+		this.authority = nullIfEmpty(trimSlashes(authority));
+		this.contextRoot = nullIfEmpty(trimSlashes(contextRoot));
+		this.servletPath = nullIfEmpty(trimSlashes(servletPath));
+		this.pathInfo = nullIfEmpty(trimSlashes(pathInfo));
+		this.parentPath = this.pathInfo == null || this.pathInfo.indexOf('/') == -1 ? null : this.pathInfo.substring(0, this.pathInfo.lastIndexOf('/'));
+	}
+
+	/**
+	 * Returns the absolute URI of just the authority portion of this URI context.
+	 * <p>
+	 * Example:  <js>"http://hostname:port"</js>
+	 * <p>
+	 * If the authority is null/empty, returns <js>"/"</js>.
+	 *
+	 * @return The absolute URI of just the authority portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getAbsoluteAuthority() {
+		return authority == null ? "/" : authority;
+	}
+
+	/**
+	 * Returns the absolute URI of the context-root portion of this URI context.
+	 * <p>
+	 * Example:  <js>"http://hostname:port/context-root"</js>
+	 *
+	 * @return The absolute URI of the context-root portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getAbsoluteContextRoot() {
+		if (aContextRoot == null) {
+			if (authority == null)
+				aContextRoot = getRootRelativeContextRoot();
+			else
+				aContextRoot = (contextRoot == null ? authority : (authority + '/' + contextRoot));
+		}
+		return aContextRoot;
+	}
+
+	/**
+	 * Returns the root-relative URI of the context portion of this URI context.
+	 * <p>
+	 * Example:  <js>"/context-root"</js>
+	 *
+	 * @return The root-relative URI of the context portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getRootRelativeContextRoot() {
+		if (rContextRoot == null)
+			rContextRoot = contextRoot == null ? "/" : ('/' + contextRoot);
+		return rContextRoot;
+	}
+
+	/**
+	 * Returns the absolute URI of the resource portion of this URI context.
+	 * <p>
+	 * Example:  <js>"http://hostname:port/context-root/servlet-path"</js>
+	 *
+	 * @return The absolute URI of the resource portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getAbsoluteServletPath() {
+		if (aServletPath == null) {
+			if (authority == null)
+				aServletPath = getRootRelativeServletPath();
+			else {
+				if (contextRoot == null)
+					aServletPath = (servletPath == null ? authority : authority + '/' + servletPath);
+				else
+					aServletPath = (servletPath == null ? (authority + '/' + contextRoot) : (authority + '/' + contextRoot + '/' + servletPath));
+			}
+		}
+		return aServletPath;
+	}
+
+	/**
+	 * Returns the root-relative URI of the resource portion of this URI context.
+	 * <p>
+	 * Example:  <js>"/context-root/servlet-path"</js>
+	 *
+	 * @return The root-relative URI of the resource portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getRootRelativeServletPath() {
+		if (rResource == null) {
+			if (contextRoot == null)
+				rResource = (servletPath == null ? "/" : ('/' + servletPath));
+			else
+				rResource = (servletPath == null ? ('/' + contextRoot) : ('/' + contextRoot + '/' + servletPath));
+		}
+		return rResource;
+	}
+
+	/**
+	 * Returns the parent of the URL returned by {@link #getAbsoluteServletPath()}.
+	 *
+	 * @return The parent of the URL returned by {@link #getAbsoluteServletPath()}.
+	 */
+	public String getAbsoluteServletPathParent() {
+		return getParent(getAbsoluteServletPath());
+	}
+
+	/**
+	 * Returns the parent of the URL returned by {@link #getRootRelativeServletPath()}.
+	 *
+	 * @return The parent of the URL returned by {@link #getRootRelativeServletPath()}.
+	 */
+	public String getRootRelativeServletPathParent() {
+		return getParent(getRootRelativeServletPath());
+	}
+
+	/**
+	 * Returns the absolute URI of the path portion of this URI context.
+	 * <p>
+	 * Example:  <js>"http://hostname:port/context-root/servlet-path/path-info"</js>
+	 *
+	 * @return The absolute URI of the path portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getAbsolutePathInfo() {
+		if (aPathInfo == null) {
+			if (authority == null)
+				aPathInfo = getRootRelativePathInfo();
+			else {
+				if (contextRoot == null) {
+					if (servletPath == null)
+						aPathInfo = (pathInfo == null ? authority : (authority + '/' + pathInfo));
+					else
+						aPathInfo = (pathInfo == null ? (authority + '/' + servletPath) : (authority + '/' + servletPath + '/' + pathInfo));
+				} else {
+					if (servletPath == null)
+						aPathInfo = (pathInfo == null ? authority + '/' + contextRoot : (authority + '/' + contextRoot + '/' + pathInfo));
+					else
+						aPathInfo = (pathInfo == null ? (authority + '/' + contextRoot + '/' + servletPath) : (authority + '/' + contextRoot + '/' + servletPath + '/' + pathInfo));
+				}
+			}
+		}
+		return aPathInfo;
+	}
+
+	/**
+	 * Returns the root-relative URI of the path portion of this URI context.
+	 * <p>
+	 * Example:  <js>"/context-root/servlet-path/path-info"</js>
+	 *
+	 * @return The root-relative URI of the path portion of this URI context.
+	 * 	Never <jk>null</jk>.
+	 */
+	public String getRootRelativePathInfo() {
+		if (rPath == null) {
+			if (contextRoot == null) {
+				if (servletPath == null)
+					rPath = (pathInfo == null ? "/" : ('/' + pathInfo));
+				else
+					rPath = (pathInfo == null ? ('/' + servletPath) : ('/' + servletPath + '/' + pathInfo));
+			} else {
+				if (servletPath == null)
+					rPath = (pathInfo == null ? ('/' + contextRoot) : ('/' + contextRoot + '/' + pathInfo));
+				else
+					rPath = (pathInfo == null ? ('/' + contextRoot + '/' + servletPath) : ('/' + contextRoot + '/' + servletPath + '/' + pathInfo));
+			}
+		}
+		return rPath;
+	}
+
+	/**
+	 * Returns the parent of the URL returned by {@link #getAbsolutePathInfo()}.
+	 *
+	 * @return The parent of the URL returned by {@link #getAbsolutePathInfo()}.
+	 */
+	public String getAbsolutePathInfoParent() {
+		return getParent(getAbsolutePathInfo());
+	}
+
+	/**
+	 * Returns the parent of the URL returned by {@link #getRootRelativePathInfo()}.
+	 *
+	 * @return The parent of the URL returned by {@link #getRootRelativePathInfo()}.
+	 */
+	public String getRootRelativePathInfoParent() {
+		return getParent(getRootRelativePathInfo());
+	}
+
+	/**
+	 * Converts the specified URI to absolute form based on values in this context.
+	 *
+	 * @param uri The URI to convert to absolute form.
+	 * @return The converted URI.
+	 */
+	public String resolveAbsolute(String uri) {
+		if (isAbsoluteUri(uri))
+			return uri;
+		return appendAbsolute(new StringBuilder(), uri).toString();
+	}
+
+	/**
+	 * Converts the specified URI to root-relative form based on values in this context.
+	 *
+	 * @param uri The URI to convert to root-relative form.
+	 * @return The converted URI.
+	 */
+	public String resolveRootRelative(String uri) {
+		if (isAbsoluteUri(uri))
+			return uri;
+		if (startsWith(uri, '/'))
+			return uri;
+		return appendRootRelative(new StringBuilder(), uri).toString();
+	}
+
+	/**
+	 * Same as {@link #resolveAbsolute(String)} except appends result to the specified appendable.
+	 *
+	 * @param a The appendable to append the URL to.
+	 * @param uri The URI to convert to absolute form.
+	 * @return The same appendable passed in.
+	 */
+	public Appendable appendAbsolute(Appendable a, String uri) {
+
+		try {
+			uri = nullIfEmpty(uri);
+
+			// Absolute paths are not changed.
+			if (isAbsoluteUri(uri))
+				return a.append(uri);
+
+			// Root-relative path
+			if (startsWith(uri, '/')) {
+				if (authority != null){
+					a.append(authority);
+					if (uri.length() == 1)
+						return a;
+				}
+				return a.append(uri);
+
+			// Context-relative path
+			} else if (uri != null && uri.startsWith("context:/")) {
+				if (authority != null)
+					a.append(authority);
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (uri.length() > 9)
+					a.append('/').append(uri.substring(9));
+				else if (contextRoot == null && authority == null)
+					a.append('/');
+
+			// Resource-relative path
+			} else if (uri != null && uri.startsWith("servlet:/")) {
+				if (authority != null)
+					a.append(authority);
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (servletPath != null)
+					a.append('/').append(servletPath);
+				if (uri.length() > 9)
+					a.append('/').append(uri.substring(9));
+				else if (servletPath == null && contextRoot == null && authority == null)
+					a.append('/');
+
+			// Relative path
+			} else {
+				if (authority != null)
+					a.append(authority);
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (servletPath != null)
+					a.append('/').append(servletPath);
+				if (uri == null) {
+					if (pathInfo != null)
+						a.append('/').append(pathInfo);
+				} else {
+					if (parentPath != null)
+						a.append('/').append(parentPath);
+					a.append('/').append(uri);
+				}
+			}
+
+			return a;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * Same as {@link #resolveRootRelative(String)} except appends result to the specified appendable.
+	 *
+	 * @param a The appendable to append the URL to.
+	 * @param uri The URI to convert to root-relative form.
+	 * @return The same appendable passed in.
+	 */
+	public Appendable appendRootRelative(Appendable a, String uri) {
+
+		try {
+			uri = nullIfEmpty(uri);
+
+			// Absolute paths are not changed.
+			if (isAbsoluteUri(uri))
+				return a.append(uri);
+
+			// Root-relative path
+			if (startsWith(uri, '/')) {
+				return a.append(uri);
+
+			// Context-relative path
+			} else if (uri != null && uri.startsWith("context:/")) {
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (uri.length() > 9)
+					a.append('/').append(uri.substring(9));
+				else if (contextRoot == null)
+					a.append('/');
+
+			// Resource-relative path
+			} else if (uri != null && uri.startsWith("servlet:/")) {
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (servletPath != null)
+					a.append('/').append(servletPath);
+				if (uri.length() > 9)
+					a.append('/').append(uri.substring(9));
+				else if (servletPath == null && contextRoot == null)
+					a.append('/');
+
+			// Relative path
+			} else {
+				if (contextRoot != null)
+					a.append('/').append(contextRoot);
+				if (servletPath != null)
+					a.append('/').append(servletPath);
+				if (uri == null) {
+					if (pathInfo != null)
+						a.append('/').append(pathInfo);
+				} else {
+					if (parentPath != null)
+						a.append('/').append(parentPath);
+					a.append('/').append(uri);
+				}
+			}
+
+			return a;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private static String getParent(String uri) {
+		int i = uri.lastIndexOf('/');
+		if (i <= 1)
+			return "/";
+		return uri.substring(0, i);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/UriRelativity.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/UriRelativity.java b/juneau-core/src/main/java/org/apache/juneau/UriRelativity.java
new file mode 100644
index 0000000..95fc46b
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/UriRelativity.java
@@ -0,0 +1,29 @@
+// ***************************************************************************************************************************
+// * 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;
+
+/**
+ * Identifies how relative URIs should resolve against.
+ */
+public enum UriRelativity {
+
+	/**
+	 * Relative URIs should be considered relative to the servlet URI.
+	 */
+	RESOURCE,
+
+	/**
+	 * Relative URIs should be considered relative to the request URI.
+	 */
+	PATH_INFO;
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/UriResolution.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/UriResolution.java b/juneau-core/src/main/java/org/apache/juneau/UriResolution.java
new file mode 100644
index 0000000..aacfe6d
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/UriResolution.java
@@ -0,0 +1,34 @@
+// ***************************************************************************************************************************
+// * 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;
+
+/**
+ * Identifies the possible types of URL resolution.
+ */
+public enum UriResolution {
+
+	/**
+	 * Resolve to an absolute URL (e.g. <js>"http://host:port/context-root/servlet-path/path-info"</js>).
+	 */
+	ABSOLUTE,
+
+	/**
+	 * Resolve to a root-relative URL (e.g. <js>"/context-root/servlet-path/path-info"</js>).
+	 */
+	ROOT_RELATIVE,
+
+	/**
+	 * Don't do any URL resolution.
+	 */
+	NONE;
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index 163928b..16a6c14 100644
--- a/juneau-core/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -30,18 +30,20 @@ public final class CsvSerializerSession extends SerializerSession {
 	 * Create a new session using properties specified in the context.
 	 *
 	 * @param ctx The context creating this session object.
-	 * The context contains all the configuration settings for this object.
+	 * 	The context contains all the configuration settings for this object.
 	 * @param output The output object.
 	 * @param op The override properties.
-	 * These override any context properties defined in the context.
+	 * 	These override any context properties defined in the context.
 	 * @param javaMethod The java method that called this serializer, usually the method in a REST servlet.
 	 * @param locale The session locale.
-	 * If <jk>null</jk>, then the locale defined on the context is used.
+	 * 	If <jk>null</jk>, then the locale defined on the context is used.
 	 * @param timeZone The session timezone.
-	 * If <jk>null</jk>, then the timezone defined on the context is used.
+	 * 	If <jk>null</jk>, then the timezone defined on the context is used.
 	 * @param mediaType The session media type (e.g. <js>"application/json"</js>).
+	 * @param uriContext The URI context.
+	 * 	Identifies the current request URI used for resolution of URIs to absolute or root-relative form.
 	 */
-	protected CsvSerializerSession(CsvSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		super(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	protected CsvSerializerSession(CsvSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		super(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlBeanPropertyMeta.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlBeanPropertyMeta.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlBeanPropertyMeta.java
index a2c3a6c..63ecbb4 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlBeanPropertyMeta.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlBeanPropertyMeta.java
@@ -18,36 +18,58 @@ import org.apache.juneau.html.annotation.*;
 /**
  * Metadata on bean properties specific to the HTML serializers and parsers pulled from the {@link Html @Html} annotation on the bean property.
  */
-public class HtmlBeanPropertyMeta extends BeanPropertyMetaExtended {
+@SuppressWarnings("rawtypes")
+public final class HtmlBeanPropertyMeta extends BeanPropertyMetaExtended {
 
-	private boolean asXml, noTables, noTableHeaders, asPlainText;
+	private final boolean asXml, noTables, noTableHeaders, asPlainText;
+	private final HtmlRender render;
+	private final String link;
 
 	/**
 	 * Constructor.
 	 *
 	 * @param bpm The metadata of the bean property of this additional metadata.
+	 * @throws Exception If render class could not be instantiated.
 	 */
-	public HtmlBeanPropertyMeta(BeanPropertyMeta bpm) {
+	public HtmlBeanPropertyMeta(BeanPropertyMeta bpm) throws Exception {
 		super(bpm);
+		Builder b = new Builder();
 		if (bpm.getField() != null)
-			findHtmlInfo(bpm.getField().getAnnotation(Html.class));
+			b.findHtmlInfo(bpm.getField().getAnnotation(Html.class));
 		if (bpm.getGetter() != null)
-			findHtmlInfo(bpm.getGetter().getAnnotation(Html.class));
+			b.findHtmlInfo(bpm.getGetter().getAnnotation(Html.class));
 		if (bpm.getSetter() != null)
-			findHtmlInfo(bpm.getSetter().getAnnotation(Html.class));
+			b.findHtmlInfo(bpm.getSetter().getAnnotation(Html.class));
+
+		this.asXml = b.asXml;
+		this.noTables = b.noTables;
+		this.noTableHeaders = b.noTableHeaders;
+		this.asPlainText = b.asPlainText;
+		this.render = b.render.newInstance();
+		this.link = b.link;
 	}
 
-	private void findHtmlInfo(Html html) {
-		if (html == null)
-			return;
-		if (html.asXml())
-			asXml = html.asXml();
-		if (html.noTables())
-			noTables = html.noTables();
-		if (html.noTableHeaders())
-			noTableHeaders = html.noTableHeaders();
-		if (html.asPlainText())
-			asPlainText = html.asPlainText();
+	private static class Builder {
+		boolean asXml, noTables, noTableHeaders, asPlainText;
+		Class<? extends HtmlRender> render = HtmlRender.class;
+		String link;
+
+		void findHtmlInfo(Html html) {
+			if (html == null)
+				return;
+			if (html.asXml())
+				asXml = html.asXml();
+			if (html.noTables())
+				noTables = html.noTables();
+			if (html.noTableHeaders())
+				noTableHeaders = html.noTableHeaders();
+			if (html.asPlainText())
+				asPlainText = html.asPlainText();
+			if (html.render() != HtmlRender.class)
+				render = html.render();
+			if (! html.link().isEmpty())
+				link = html.link();
+		}
 	}
 
 	/**
@@ -85,4 +107,26 @@ public class HtmlBeanPropertyMeta extends BeanPropertyMetaExtended {
 	public boolean isNoTableHeaders() {
 		return noTableHeaders;
 	}
+
+	/**
+	 * Returns the render class for rendering the style and contents of this property value in HTML.
+	 * <p>
+	 * This value is specified via the {@link Html#render()} annotation.
+	 *
+	 * @return The render class, never <jk>null</jk>.
+	 */
+	public HtmlRender getRender() {
+		return render;
+	}
+
+	/**
+	 * Adds a hyperlink to this value in HTML.
+	 * <p>
+	 * This value is specified via the {@link Html#link()} annotation.
+	 *
+	 * @return The link string, or <jk>null</jk> if not specified.
+	 */
+	public String getLink() {
+		return link;
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
index d97afe4..68e19e2 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java
@@ -75,8 +75,8 @@ public class HtmlDocSerializer extends HtmlStrippedDocSerializer {
 	//--------------------------------------------------------------------------------
 
 	@Override /* Serializer */
-	public HtmlDocSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		return new HtmlDocSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	public HtmlDocSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		return new HtmlDocSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 	}
 
 	@Override /* Serializer */

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
index 0e334d1..f04d2b7 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java
@@ -41,20 +41,22 @@ public final class HtmlDocSerializerSession extends HtmlSerializerSession {
 	 * Create a new session using properties specified in the context.
 	 *
 	 * @param ctx The context creating this session object.
-	 * The context contains all the configuration settings for this object.
+	 * 	The context contains all the configuration settings for this object.
 	 * @param output The output object.  See {@link JsonSerializerSession#getWriter()} for valid class types.
 	 * @param op The override properties.
-	 * These override any context properties defined in the context.
+	 * 	These override any context properties defined in the context.
 	 * @param javaMethod The java method that called this serializer, usually the method in a REST servlet.
 	 * @param locale The session locale.
-	 * If <jk>null</jk>, then the locale defined on the context is used.
+	 * 	If <jk>null</jk>, then the locale defined on the context is used.
 	 * @param timeZone The session timezone.
-	 * If <jk>null</jk>, then the timezone defined on the context is used.
+	 * 	If <jk>null</jk>, then the timezone defined on the context is used.
 	 * @param mediaType The session media type (e.g. <js>"application/json"</js>).
+	 * @param uriContext The URI context.
+	 * 	Identifies the current request URI used for resolution of URIs to absolute or root-relative form.
 	 */
 	@SuppressWarnings({ "unchecked", "rawtypes" })
-	protected HtmlDocSerializerSession(HtmlDocSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		super(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	protected HtmlDocSerializerSession(HtmlDocSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		super(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 		if (op == null || op.isEmpty()) {
 			title = ctx.title;
 			text = ctx.text;
@@ -131,6 +133,6 @@ public final class HtmlDocSerializerSession extends HtmlSerializerSession {
 		Object output = getOutput();
 		if (output instanceof HtmlWriter)
 			return (HtmlWriter)output;
-		return new HtmlWriter(super.getWriter(), isUseWhitespace(), isTrimStrings(), getQuoteChar(), getRelativeUriBase(), getAbsolutePathUriBase());
+		return new HtmlWriter(super.getWriter(), isUseWhitespace(), isTrimStrings(), getQuoteChar(), getRelativeUriBase(), getAbsolutePathUriBase(), getUriContext());
 	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlRender.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlRender.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlRender.java
new file mode 100644
index 0000000..b30c67e
--- /dev/null
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlRender.java
@@ -0,0 +1,149 @@
+// ***************************************************************************************************************************
+// * 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.html;
+
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Allows custom rendering of bean property values when serialized as HTML.
+ * <p>
+ * Associated with bean properties using the {@link Html#render() @Html.render()} annotation.
+ * <p>
+ * Using this class, you can alter the CSS style and HTML content of the bean property.
+ * <p>
+ * The following example shows two render classes that customize the appearance of the <code>pctFull</code> and
+ * 	<code>status</code> columns shown below:
+ * <p>
+ * <img class='bordered' src='doc-files/HtmlRender_1.png'>
+ *
+ * <p class='bcode'>
+ *
+ * 	<jc>// Our bean class</jc>
+ * 	<jk>public class</jk> FileSpace {
+ *
+ * 		<jk>private final</jk> String <jf>drive</jf>;
+ * 		<jk>private final long</jk> <jf>total</jf>, <jf>available</jf>;
+ *
+ * 		<jk>public</jk> FileSpace(String drive, <jk>long</jk> total, <jk>long</jk> available) {
+ * 			<jk>this</jk>.<jf>drive</jf> = drive;
+ * 			<jk>this</jk>.<jf>total</jf> = total;
+ * 			<jk>this</jk>.<jf>available</jf> = available;
+ * 		}
+ *
+ * 		<ja>@Html</ja>(link=<js>"drive/{drive}"</js>)
+ * 		<jk>public</jk> String getDrive() {
+ * 			<jk>return</jk> <jf>drive</jf>;
+ * 		}
+ *
+ * 		<jk>public long</jk> getTotal() {
+ * 			<jk>return</jk> <jf>total</jf>;
+ * 		}
+ *
+ * 		<jk>public long</jk> getAvailable() {
+ * 			<jk>return</jk> <jf>available</jf>;
+ * 		}
+ *
+ * 		<ja>@Html</ja>(render=FileSpacePctRender.<jk>class</jk>)
+ * 		<jk>public float</jk> getPctFull() {
+ * 			<jk>return</jk> ((100 * <jf>available</jf>) / <jf>total</jf>);
+ * 		}
+ *
+ * 		<ja>@Html</ja>(render=FileSpaceStatusRender.<jk>class</jk>)
+ * 		<jk>public</jk> FileSpaceStatus getStatus() {
+ * 			<jk>float</jk> pf = getPctFull();
+ * 			<jk>if</jk> (pf < 80)
+ * 				<jk>return</jk> FileSpaceStatus.<jsf>OK</jsf>;
+ * 			<jk>if</jk> (pf < 90)
+ * 				<jk>return</jk> FileSpaceStatus.<jsf>WARNING</jsf>;
+ * 			<jk>return</jk> FileSpaceStatus.<jsf>SEVERE</jsf>;
+ * 		}
+ * 	}
+ *
+ * 	<jc>// Possible values for the getStatus() method</jc>
+ * 	<jk>public static enum</jk> FileSpaceStatus {
+ * 		<jsf>OK</jsf>, <jsf>WARNING</jsf>, <jsf>SEVERE</jsf>;
+ * 	}
+ *
+ * 	<jc>// Custom render for getPctFull() method</jc>
+ * 	<jk>public static class</jk> FileSpacePctRender <jk>extends</jk> HtmlRender&lt;Float&gt; {
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public</jk> String getStyle(SerializerSession session, Float value) {
+ * 			<jk>if</jk> (value < 80)
+ * 				<jk>return</jk> <js>"background-color:lightgreen;text-align:center"</js>;
+ * 			<jk>if</jk> (value < 90)
+ * 				<jk>return</jk> <js>"background-color:yellow;text-align:center"</js>;
+ * 			<jk>return</jk> <js>"background-color:red;text-align:center;border:;animation:color_change 0.5s infinite alternate"</js>;
+ * 		}
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public</jk> Object getContent(SerializerSession session, Float value) {
+ * 			<jk>if</jk> (value >= 90)
+ * 				<jk>return</jk> <jsm>div</jsm>(
+ * 					String.<jsm>format</jsm>(<js>"%.0f%%"</js>, value),
+ * 					<jsm>style</jsm>(<js>"@keyframes color_change { from { background-color: red; } to { background-color: yellow; }"</js>)
+ * 				);
+ * 			<jk>return</jk> String.<jsm>format</jsm>(<js>"%.0f%%"</js>, value);
+ * 		}
+ * 	}
+ *
+ * 	<jc>// Custom render for getStatus() method</jc>
+ * 	<jk>public static class</jk> FileSpaceStatusRender <jk>extends</jk> HtmlRender&lt;FileSpaceStatus&gt; {
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public</jk> String getStyle(SerializerSession session, FileSpaceStatus value) {
+ * 			<jk>return</jk> <js>"text-align:center"</js>;
+ * 		}
+ *
+ * 		<ja>@Override</ja>
+ * 		<jk>public</jk> Object getContent(SerializerSession session, FileSpaceStatus value) {
+ * 			<jk>switch</jk> (value) {
+ * 				<jk>case</jk> <jsf>OK</jsf>:  <jk>return</jk> <jsm>img</jsm>().src(URI.<jsm>create</jsm>(<js>"servlet:/htdocs/ok.png"</js>));
+ * 				<jk>case</jk> <jsf>WARNING</jsf>:  <jk>return</jk> <jsm>img</jsm>().src(URI.<jsm>create</jsm>(<js>"servlet:/htdocs/warning.png"</js>));
+ * 				<jk>default</jk>: <jk>return</jk> <jsm>img</jsm>().src(URI.<jsm>create</jsm>(<js>"servlet:/htdocs/severe.png"</js>));
+ * 			}
+ * 		}
+ * 	}
+ * </p>
+ * @param <T> The bean property type.
+ */
+public class HtmlRender<T> {
+
+	/**
+	 * Returns the CSS style of the element containing the bean property value.
+	 *
+	 * @param session The current serializer session.
+	 * 	Can be used to retrieve properties and session-level information.
+	 * @param value The bean property value.
+	 * @return The CSS style string, or <jk>null</jk> if no style should be added.
+	 */
+	public String getStyle(SerializerSession session, T value) {
+		return null;
+	}
+
+	/**
+	 * Returns the delegate value for the specified bean property value.
+	 * <p>
+	 * The default implementation simply returns the same value.
+	 * A typical use is to return an HTML element using one of the HTML5 DOM beans.
+	 *
+	 * @param session The current serializer session.
+	 * 	Can be used to retrieve properties and session-level information.
+	 * @param value The bean property value.
+	 * @return The new bean property value.
+	 */
+	public Object getContent(SerializerSession session, T value) {
+		return value;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
index bc17387..fabcdaa 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSchemaDocSerializer.java
@@ -74,8 +74,8 @@ public final class HtmlSchemaDocSerializer extends HtmlDocSerializer {
 	}
 
 	@Override /* Serializer */
-	public HtmlDocSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		return new HtmlDocSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	public HtmlDocSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		return new HtmlDocSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 	}
 
 	@Override /* ISchemaSerializer */

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
index c3d1088..91b8f16 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializer.java
@@ -400,6 +400,7 @@ public class HtmlSerializer extends XmlSerializer {
 		out.eTag(i, "table").nl();
 	}
 
+	@SuppressWarnings({ "rawtypes", "unchecked" })
 	private void serializeBeanMap(HtmlSerializerSession session, HtmlWriter out, BeanMap<?> m, ClassMeta<?> eType, BeanPropertyMeta ppMeta) throws Exception {
 		int i = session.getIndent();
 
@@ -420,6 +421,9 @@ public class HtmlSerializer extends XmlSerializer {
 		for (BeanPropertyValue p : m.getValues(session.isTrimNulls())) {
 			BeanPropertyMeta pMeta = p.getMeta();
 			ClassMeta<?> cMeta = p.getClassMeta();
+			HtmlBeanPropertyMeta hbpMeta = pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class);
+			String link = hbpMeta.getLink();
+			HtmlRender render = hbpMeta.getRender();
 
 			String key = p.getName();
 			Object value = p.getValue();
@@ -432,11 +436,20 @@ public class HtmlSerializer extends XmlSerializer {
 
 			out.sTag(i+1, "tr").nl();
 			out.sTag(i+2, "td").text(key).eTag("td").nl();
-			out.sTag(i+2, "td");
+			out.oTag(i+2, "td");
+			String style = render.getStyle(session, value);
+			if (style != null)
+				out.attr("style", style);
+			out.cTag();
+
 			try {
-				ContentResult cr = serializeAnything(session, out, value, cMeta, key, 2, pMeta, false);
+				if (link != null) 
+					out.oTag(i+3, "a").attrUri("href", m.resolveVars(link)).cTag();
+				ContentResult cr = serializeAnything(session, out, render.getContent(session, value), cMeta, key, 2, pMeta, false);
 				if (cr == CR_NORMAL)
 					out.i(i+2);
+				if (link != null) 
+					out.eTag("a");
 			} catch (SerializeException e) {
 				throw e;
 			} catch (Error e) {
@@ -533,10 +546,23 @@ public class HtmlSerializer extends XmlSerializer {
 					for (Object k : th) {
 						BeanMapEntry p = m2.getProperty(session.toString(k));
 						BeanPropertyMeta pMeta = p.getMeta();
-						out.sTag(i+2, "td");
-						ContentResult cr = serializeAnything(session, out, p.getValue(), pMeta.getClassMeta(), p.getKey().toString(), 2, pMeta, false);
+						HtmlBeanPropertyMeta hpMeta = pMeta.getExtendedMeta(HtmlBeanPropertyMeta.class);
+						String link = hpMeta.getLink();
+						HtmlRender render = hpMeta.getRender();
+
+						Object value = p.getValue();
+						out.oTag(i+2, "td");
+						String style = render.getStyle(session, value);
+						if (style != null)
+							out.attr("style", style);
+						out.cTag();
+						if (link != null) 
+							out.oTag(i+3, "a").attrUri("href", m2.resolveVars(link)).cTag();
+						ContentResult cr = serializeAnything(session, out, render.getContent(session, value), pMeta.getClassMeta(), p.getKey().toString(), 2, pMeta, false);
 						if (cr == CR_NORMAL)
 							out.i(i+2);
+						if (link != null) 
+							out.eTag("a");
 						out.eTag("td").nl();
 					}
 				}
@@ -689,8 +715,8 @@ public class HtmlSerializer extends XmlSerializer {
 	//--------------------------------------------------------------------------------
 
 	@Override /* Serializer */
-	public HtmlSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		return new HtmlSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	public HtmlSerializerSession createSession(Object output, ObjectMap op, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		return new HtmlSerializerSession(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 	}
 
 	@Override /* Serializer */

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
index 3faf271..48b16dd 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -52,19 +52,21 @@ public class HtmlSerializerSession extends XmlSerializerSession {
 	 * Create a new session using properties specified in the context.
 	 *
 	 * @param ctx The context creating this session object.
-	 * The context contains all the configuration settings for this object.
+	 * 	The context contains all the configuration settings for this object.
 	 * @param output The output object.  See {@link JsonSerializerSession#getWriter()} for valid class types.
 	 * @param op The override properties.
-	 * These override any context properties defined in the context.
+	 * 	These override any context properties defined in the context.
 	 * @param javaMethod The java method that called this serializer, usually the method in a REST servlet.
 	 * @param locale The session locale.
-	 * If <jk>null</jk>, then the locale defined on the context is used.
+	 * 	If <jk>null</jk>, then the locale defined on the context is used.
 	 * @param timeZone The session timezone.
-	 * If <jk>null</jk>, then the timezone defined on the context is used.
+	 * 	If <jk>null</jk>, then the timezone defined on the context is used.
 	 * @param mediaType The session media type (e.g. <js>"application/json"</js>).
+	 * @param uriContext The URI context.
+	 * 	Identifies the current request URI used for resolution of URIs to absolute or root-relative form.
 	 */
-	protected HtmlSerializerSession(HtmlSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType) {
-		super(ctx, op, output, javaMethod, locale, timeZone, mediaType);
+	protected HtmlSerializerSession(HtmlSerializerContext ctx, ObjectMap op, Object output, Method javaMethod, Locale locale, TimeZone timeZone, MediaType mediaType, UriContext uriContext) {
+		super(ctx, op, output, javaMethod, locale, timeZone, mediaType, uriContext);
 		String labelParameter;
 		if (op == null || op.isEmpty()) {
 			anchorText = Enum.valueOf(AnchorText.class, ctx.uriAnchorText);
@@ -91,7 +93,7 @@ public class HtmlSerializerSession extends XmlSerializerSession {
 		Object output = getOutput();
 		if (output instanceof HtmlWriter)
 			return (HtmlWriter)output;
-		return new HtmlWriter(super.getWriter(), isUseWhitespace(), isTrimStrings(), getQuoteChar(), getRelativeUriBase(), getAbsolutePathUriBase());
+		return new HtmlWriter(super.getWriter(), isUseWhitespace(), isTrimStrings(), getQuoteChar(), getRelativeUriBase(), getAbsolutePathUriBase(), getUriContext());
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/HtmlWriter.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/HtmlWriter.java b/juneau-core/src/main/java/org/apache/juneau/html/HtmlWriter.java
index 95b6092..30114c3 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/HtmlWriter.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/HtmlWriter.java
@@ -14,6 +14,7 @@ package org.apache.juneau.html;
 
 import java.io.*;
 
+import org.apache.juneau.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.xml.*;
 
@@ -31,9 +32,11 @@ public class HtmlWriter extends XmlWriter {
 	 * @param quoteChar The quote character to use (i.e. <js>'\''</js> or <js>'"'</js>)
 	 * @param uriContext The web application context path (e.g. "/contextRoot").
 	 * @param uriAuthority The web application URI authority (e.g. "http://hostname:9080")
+	 * @param uriContext2 The URI context.
+	 * 	Identifies the current request URI used for resolution of URIs to absolute or root-relative form.
 	 */
-	public HtmlWriter(Writer out, boolean useWhitespace, boolean trimStrings, char quoteChar, String uriContext, String uriAuthority) {
-		super(out, useWhitespace, trimStrings, quoteChar, uriContext, uriAuthority, false, null);
+	public HtmlWriter(Writer out, boolean useWhitespace, boolean trimStrings, char quoteChar, String uriContext, String uriAuthority, UriContext uriContext2) {
+		super(out, useWhitespace, trimStrings, quoteChar, uriContext, uriAuthority, uriContext2, false, null);
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/SimpleHtmlWriter.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/SimpleHtmlWriter.java b/juneau-core/src/main/java/org/apache/juneau/html/SimpleHtmlWriter.java
index 2ae07f8..b0b6cc3 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/SimpleHtmlWriter.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/SimpleHtmlWriter.java
@@ -28,7 +28,7 @@ public class SimpleHtmlWriter extends HtmlWriter {
 	 * Constructor.
 	 */
 	public SimpleHtmlWriter() {
-		super(new StringWriter(), true, false, '\'', null, null);
+		super(new StringWriter(), true, false, '\'', null, null, null);
 	}
 
 	@Override /* Object */

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/annotation/Html.java
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/annotation/Html.java b/juneau-core/src/main/java/org/apache/juneau/html/annotation/Html.java
index c63cd2e..dcfc53c 100644
--- a/juneau-core/src/main/java/org/apache/juneau/html/annotation/Html.java
+++ b/juneau-core/src/main/java/org/apache/juneau/html/annotation/Html.java
@@ -53,4 +53,39 @@ public @interface Html {
 	 * Default is <jk>false</jk>.
 	 */
 	boolean noTableHeaders() default false;
+
+	/**
+	 * Associates an {@link HtmlRender} with a bean property for custom HTML rendering of the property.
+	 * <p>
+	 * This annotation applies to bean properties only.
+	 */
+	@SuppressWarnings("rawtypes")
+	Class<? extends HtmlRender> render() default HtmlRender.class;
+
+	/**
+	 * Adds a hyperlink to a bean property when rendered as HTML.
+	 * <p>
+	 * The text can contain any bean property values resolved through variables of the form <js>"{property-name}"</js>.
+	 * <p>
+	 * The URLs can be any of the following forms:
+	 * <ul>
+	 * 	<li>Absolute - e.g. <js>"http://host:123/myContext/myServlet/myPath"</js>
+	 * 	<li>Context-root-relative - e.g. <js>"/myContext/myServlet/myPath"</js>
+	 * 	<li>Context-relative - e.g. <js>"context:/myServlet/myPath"</js>
+	 * 	<li>Servlet-relative - e.g. <js>"servlet:/myPath"</js>
+	 * 	<li>Path-info-relative - e.g. <js>"myPath"</js>
+	 * </ul>
+	 *
+	 * <h6 class='figure'>Example:</h6>
+	 * <p class='bcode'>
+	 * 	<jk>public class</jk> FileSpace {
+	 *
+	 * 		<ja>@Html</ja>(link=<js>"servlet:/drive/{drive}"</js>)
+	 * 		<jk>public</jk> String getDrive() {
+	 * 			...;
+	 * 		}
+	 * 	}
+	 * </p>
+	 */
+	String link() default "";
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e1a50566/juneau-core/src/main/java/org/apache/juneau/html/doc-files/HtmlRender_1.png
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/java/org/apache/juneau/html/doc-files/HtmlRender_1.png b/juneau-core/src/main/java/org/apache/juneau/html/doc-files/HtmlRender_1.png
new file mode 100644
index 0000000..f070aea
Binary files /dev/null and b/juneau-core/src/main/java/org/apache/juneau/html/doc-files/HtmlRender_1.png differ