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 2018/07/30 13:26:47 UTC

[juneau] branch master updated: Rework Response API.

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


The following commit(s) were added to refs/heads/master by this push:
     new 8e408b5  Rework Response API.
8e408b5 is described below

commit 8e408b5270ab40cc7f69a4aaa69e88c530821339
Author: JamesBognar <ja...@apache.org>
AuthorDate: Mon Jul 30 09:26:34 2018 -0400

    Rework Response API.
---
 .../org/apache/juneau/httppart/ResponseMeta.java   | 231 +++++++++++++++++++++
 .../juneau/httppart/ResponsePropertyMeta.java      | 136 ++++++++++++
 .../apache/juneau/rest/BasicRestCallHandler.java   |  42 ++--
 .../org/apache/juneau/rest/ResponseHandler.java    |   2 +-
 .../MovedPermanently.java => ResponseObject.java}  |  82 +++++---
 .../PermanentRedirect.java => ResponsePart.java}   |  69 +++---
 .../org/apache/juneau/rest/RestCallHandler.java    |   7 +-
 .../java/org/apache/juneau/rest/RestContext.java   |  31 ++-
 .../org/apache/juneau/rest/RestJavaMethod.java     |  36 +++-
 .../org/apache/juneau/rest/RestMethodReturn.java   |  29 +--
 .../org/apache/juneau/rest/RestMethodThrown.java   |  29 +--
 .../org/apache/juneau/rest/RestParamDefaults.java  |  25 +--
 .../java/org/apache/juneau/rest/RestRequest.java   |  14 +-
 .../java/org/apache/juneau/rest/RestResponse.java  |  33 +--
 .../juneau/rest/annotation/RestResource.java       |   2 +-
 .../juneau/rest/reshandlers/DefaultHandler.java    |  62 ++++--
 .../rest/reshandlers/InputStreamHandler.java       |   6 +-
 .../juneau/rest/reshandlers/ReaderHandler.java     |   6 +-
 .../juneau/rest/reshandlers/RedirectHandler.java   |   6 +-
 .../juneau/rest/reshandlers/StreamableHandler.java |  10 +-
 .../juneau/rest/reshandlers/WritableHandler.java   |  10 +-
 .../reshandlers/ZipFileListResponseHandler.java    |   6 +-
 .../juneau/rest/response/MovedPermanently.java     |  13 +-
 .../juneau/rest/response/PermanentRedirect.java    |  13 +-
 .../org/apache/juneau/rest/response/SeeOther.java  |  13 +-
 .../juneau/rest/response/TemporaryRedirect.java    |  13 +-
 .../rest/annotation/ResponseAnnotationTest.java    |   4 +-
 .../org/apache/juneau/rest/response/BasicTest.java |  35 ++++
 28 files changed, 728 insertions(+), 237 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponseMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponseMeta.java
new file mode 100644
index 0000000..e447ae7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponseMeta.java
@@ -0,0 +1,231 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import static org.apache.juneau.internal.ClassFlags.*;
+import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.ReflectionUtils.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents the metadata gathered from a parameter or class annotated with {@link Response}.
+ */
+public class ResponseMeta {
+
+	/**
+	 * Represents a non-existent meta object.
+	 */
+	public static ResponseMeta NULL = new ResponseMeta(new Builder(PropertyStore.DEFAULT));
+
+	/**
+	 * Create metadata from specified parameter.
+	 *
+	 * @param m The method containing the parameter or parameter type annotated with {@link Response}.
+	 * @param i The parameter index.
+	 * @param ps
+	 * 	Configuration information used to instantiate part serializers and part parsers.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @return Metadata about the parameter, or <jk>null</jk> if parameter or parameter type not annotated with {@link Response}.
+	 */
+	public static ResponseMeta create(Method m, int i, PropertyStore ps) {
+		if (! hasAnnotation(Response.class, m, i))
+			return null;
+		return new ResponseMeta.Builder(ps).apply(m, i).build();
+	}
+
+	/**
+	 * Create metadata from specified method return.
+	 *
+	 * @param m The method annotated with {@link Response}.
+	 * @param ps
+	 * 	Configuration information used to instantiate part serializers and part parsers.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @return Metadata about the parameter, or <jk>null</jk> if parameter or parameter type not annotated with {@link Response}.
+	 */
+	public static ResponseMeta create(Method m, PropertyStore ps) {
+		if (! hasAnnotation(Response.class, m))
+			return null;
+		return new ResponseMeta.Builder(ps).apply(m).build();
+	}
+
+	/**
+	 * Create metadata from specified class.
+	 *
+	 * @param c The class annotated with {@link Response}.
+	 * @param ps
+	 * 	Configuration information used to instantiate part serializers and part parsers.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @return Metadata about the class, or <jk>null</jk> if class not annotated with {@link Response}.
+	 */
+	public static ResponseMeta create(Class<?> c, PropertyStore ps) {
+		if (! hasAnnotation(Response.class, c))
+			return null;
+		return new ResponseMeta.Builder(ps).apply(c).build();
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Instance
+	//-----------------------------------------------------------------------------------------------------------------
+
+	private final ClassMeta<?> cm;
+	private final int code;
+	private final Map<String,ResponsePropertyMeta> headers;
+	private final HttpPartSerializer partSerializer;
+	private final HttpPartSchema schema;
+	private final boolean usePartSerializer;
+
+	ResponseMeta(Builder b) {
+		this.cm = b.cm;
+		this.code = b.code;
+		this.partSerializer = ClassUtils.newInstance(HttpPartSerializer.class, b.partSerializer, true, b.ps);
+		this.schema = b.schema.build();
+		this.usePartSerializer = b.usePartSerializer || partSerializer != null;
+		Map<String,ResponsePropertyMeta> headers = new LinkedHashMap<>();
+		for (Map.Entry<String,ResponsePropertyMeta.Builder> e : b.headers.entrySet()) {
+			ResponsePropertyMeta pm = e.getValue().build(partSerializer);
+			headers.put(e.getKey(), pm);
+
+		}
+		this.headers = Collections.unmodifiableMap(headers);
+	}
+
+	static class Builder {
+		ClassMeta<?> cm;
+		int code;
+		PropertyStore ps;
+		boolean usePartSerializer;
+		Class<? extends HttpPartSerializer> partSerializer;
+		HttpPartSchemaBuilder schema = HttpPartSchema.create();
+		Map<String,ResponsePropertyMeta.Builder> headers = new LinkedHashMap<>();
+
+		Builder(PropertyStore ps) {
+			this.ps = ps;
+		}
+
+		Builder apply(Method m, int i) {
+			return apply(m.getParameterTypes()[i]).apply(getAnnotation(Response.class, m, i));
+		}
+
+		Builder apply(Method m) {
+			return apply(m.getReturnType()).apply(getAnnotation(Response.class, m));
+		}
+
+		Builder apply(Class<?> c) {
+			apply(getAnnotation(Response.class, c));
+			this.cm = BeanContext.DEFAULT.getClassMeta(c);
+			for (Method m : ClassUtils.getAllMethods(c, false)) {
+				if (isAll(m, PUBLIC, HAS_NO_ARGS)) {
+					Header h = m.getAnnotation(Header.class);
+					if (h != null) {
+						String n = h.name();
+						if (n.isEmpty())
+							n = m.getName();
+						HttpPartSchemaBuilder s = HttpPartSchema.create().apply(h);
+						getProperty(n, HttpPartType.HEADER).apply(s).getter(m);
+					}
+				}
+			}
+			return this;
+		}
+
+		Builder apply(Response rb) {
+			if (rb != null) {
+				if (rb.partSerializer() != HttpPartSerializer.Null.class)
+					partSerializer = rb.partSerializer();
+				if (rb.usePartSerializer())
+					usePartSerializer = true;
+				if (rb.value().length > 0)
+					code = rb.value()[0];
+				if (rb.code().length > 0)
+					code = rb.code()[0];
+				schema.apply(rb.schema());
+			}
+			return this;
+		}
+
+		ResponseMeta build() {
+			return new ResponseMeta(this);
+		}
+
+		private ResponsePropertyMeta.Builder getProperty(String name, HttpPartType partType) {
+			ResponsePropertyMeta.Builder b = headers.get(name);
+			if (b == null) {
+				b = ResponsePropertyMeta.create().name(name).partType(partType);
+				headers.put(name, b);
+			}
+			return b;
+		}
+	}
+
+	/**
+	 * Returns the HTTP status code.
+	 *
+	 * @return The HTTP status code.
+	 */
+	public int getCode() {
+		return code;
+	}
+
+	/**
+	 * Returns the schema information about the response object.
+	 *
+	 * @return The schema information about the response object.
+	 */
+	public HttpPartSchema getSchema() {
+		return schema;
+	}
+
+	/**
+	 * Returns metadata about headers defined on the response object.
+	 *
+	 * @return Metadata about headers defined on the response object.
+	 */
+	public Collection<ResponsePropertyMeta> getHeaderMetas() {
+		return headers.values();
+	}
+
+	/**
+	 * Returns the flag for whether the part serializer should be used to serialize this response.
+	 *
+	 * @return The flag for whether the part serializer should be used to serialize this response.
+	 */
+	public boolean isUsePartSerializer() {
+		return usePartSerializer;
+	}
+
+	/**
+	 * Returns the part serializer to use to serialize this response.
+	 *
+	 * @param _default The default serializer to use if it's not defined on this metadata.
+	 * @return The part serializer to use to serialize this response.
+	 */
+	public HttpPartSerializer getPartSerializer(HttpPartSerializer _default) {
+		return partSerializer == null ? _default : partSerializer;
+	}
+
+	/**
+	 * Returns metadata about the class.
+	 *
+	 * @return Metadata about the class.
+	 */
+	public ClassMeta<?> getClassMeta() {
+		return cm;
+	}
+
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponsePropertyMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponsePropertyMeta.java
new file mode 100644
index 0000000..53f149f
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/ResponsePropertyMeta.java
@@ -0,0 +1,136 @@
+// ***************************************************************************************************************************
+// * 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.httppart;
+
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents the metadata gathered from a getter method of a class annotated with {@link Response}.
+ */
+public class ResponsePropertyMeta {
+
+	static ResponsePropertyMeta.Builder create() {
+		return new Builder();
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Instance
+	//-----------------------------------------------------------------------------------------------------------------
+
+	private final String partName;
+	private final Method getter;
+	private final HttpPartType partType;
+	private final HttpPartSerializer serializer;
+	private final HttpPartSchema schema;
+
+	ResponsePropertyMeta(Builder b, HttpPartSerializer serializer) {
+		this.partType = b.partType;
+		this.schema = b.schema.build();
+		this.partName = StringUtils.firstNonEmpty(schema.getName(), b.name);
+		this.getter = b.getter;
+		this.serializer = schema.getSerializer() == null ? serializer : ClassUtils.newInstance(HttpPartSerializer.class, schema.getSerializer(), true, b.ps);
+	}
+
+	static class Builder {
+		HttpPartType partType;
+		HttpPartSchemaBuilder schema;
+		String name;
+		Method getter;
+		PropertyStore ps = PropertyStore.DEFAULT;
+
+		Builder name(String value) {
+			name = value;
+			return this;
+		}
+
+		Builder getter(Method value) {
+			getter = value;
+			return this;
+		}
+
+		Builder partType(HttpPartType value) {
+			partType = value;
+			return this;
+		}
+
+		Builder schema(HttpPartSchemaBuilder value) {
+			schema = value;
+			return this;
+		}
+
+		Builder apply(HttpPartSchemaBuilder s) {
+			schema = s;
+			return this;
+		}
+
+		ResponsePropertyMeta build(HttpPartSerializer serializer) {
+			return new ResponsePropertyMeta(this, serializer);
+		}
+	}
+
+	/**
+	 * Returns the HTTP part name for this property (e.g. query parameter name).
+	 *
+	 * @return The HTTP part name, or <jk>null</jk> if it doesn't have a part name.
+	 */
+	public String getPartName() {
+		return partName;
+	}
+
+	/**
+	 * Returns the name of the Java method getter that defines this property.
+	 *
+	 * @return
+	 * 	The name of the Java method getter that defines this property.
+	 * 	<br>Never <jk>null</jk>.
+	 */
+	public Method getGetter() {
+		return getter;
+	}
+
+	/**
+	 * Returns the HTTP part type for this property (e.g. query parameter, header, etc...).
+	 *
+	 * @return
+	 * 	The HTTP part type for this property.
+	 * 	<br>Never <jk>null</jk>.
+	 */
+	public HttpPartType getPartType() {
+		return partType;
+	}
+
+	/**
+	 * Returns the serializer to use for serializing the bean property value.
+	 *
+	 * @param _default The default serializer to use if not defined on the annotation.
+	 * @return The serializer to use for serializing the bean property value.
+	 */
+	public HttpPartSerializer getSerializer(HttpPartSerializer _default) {
+		return serializer == null ? _default : serializer;
+	}
+
+	/**
+	 * Returns the schema information gathered from annotations on the method and return type.
+	 *
+	 * @return
+	 * 	The schema information gathered from annotations on the method and return type.
+	 * 	<br>Never <jk>null</jk>.
+	 */
+	public HttpPartSchema getSchema() {
+		return schema;
+	}
+}
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 4b1a0d5..c7d62de 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
@@ -23,7 +23,6 @@ import java.util.*;
 import javax.servlet.*;
 import javax.servlet.http.*;
 
-import org.apache.juneau.httppart.*;
 import org.apache.juneau.rest.exception.*;
 import org.apache.juneau.rest.helper.*;
 import org.apache.juneau.rest.util.*;
@@ -173,13 +172,11 @@ public class BasicRestCallHandler implements RestCallHandler {
 			}
 
 			if (res.hasOutput()) {
-				Object output = res.getOutput();
+				ResponseObject output = res.getOutput();
 
 				// Do any class-level transforming.
 				for (RestConverter converter : context.getConverters())
-					output = converter.convert(req, output);
-
-				res.setOutput(output);
+					output.setValue(converter.convert(req, output.getValue()));
 
 				// Now serialize the output if there was any.
 				// Some subclasses may write to the OutputStream or Writer directly.
@@ -195,7 +192,7 @@ public class BasicRestCallHandler implements RestCallHandler {
 		} catch (Throwable e) {
 			r1.setAttribute("Exception", e);
 			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
-			handleError(r1, r2, req, e);
+			handleError(r1, r2, e);
 		}
 
 		context.finishCall(r1, r2);
@@ -213,7 +210,7 @@ public class BasicRestCallHandler implements RestCallHandler {
 	 *
 	 * <p>
 	 * The default implementation simply iterates through the response handlers on this resource
-	 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse, Object)} method returns
+	 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse, ResponseObject)} method returns
 	 * <jk>true</jk>.
 	 *
 	 * @param req The HTTP request.
@@ -223,7 +220,7 @@ public class BasicRestCallHandler implements RestCallHandler {
 	 * @throws RestException
 	 */
 	@Override /* RestCallHandler */
-	public void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException, NotImplemented {
+	public void handleResponse(RestRequest req, RestResponse res, ResponseObject output) throws IOException, RestException, NotImplemented {
 		// Loop until we find the correct handler for the POJO.
 		for (ResponseHandler h : context.getResponseHandlers())
 			if (h.handle(req, res, output))
@@ -269,13 +266,10 @@ public class BasicRestCallHandler implements RestCallHandler {
 	 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
 	 */
 	@Override /* RestCallHandler */
-	public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestRequest rreq, Throwable e) throws IOException {
+	public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, Throwable e) throws IOException {
 
-		RestMethodThrown rmt = rreq == null ? null : rreq.getRestMethodThrown(e);
 		int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e);
-		RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, rmt == null ? 500 : rmt.getCode())).setOccurrence(occurrence);
-
-		HttpPartSerializer ps = rmt == null ? null : rmt.getPartSerializer();
+		RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, 500)).setOccurrence(occurrence);
 
 		Throwable t = e2.getRootCause();
 		if (t != null) {
@@ -296,21 +290,13 @@ public class BasicRestCallHandler implements RestCallHandler {
 			}
 
 			try (PrintWriter w2 = w) {
-
-				// Throwable can be handled as an HTTP part.
-				if (rmt != null && ps != null) {
-					w2.append(ps.serialize(rmt.getSchema(), e));
-
-				// It's some other exception.
-				} else {
-					String httpMessage = RestUtils.getHttpResponseText(e2.getStatus());
-					if (httpMessage != null)
-						w2.append("HTTP ").append(String.valueOf(e2.getStatus())).append(": ").append(httpMessage).append("\n\n");
-					if (context != null && context.isRenderResponseStackTraces())
-						e.printStackTrace(w2);
-					else
-						w2.append(e2.getFullStackMessage(true));
-				}
+				String httpMessage = RestUtils.getHttpResponseText(e2.getStatus());
+				if (httpMessage != null)
+					w2.append("HTTP ").append(String.valueOf(e2.getStatus())).append(": ").append(httpMessage).append("\n\n");
+				if (context != null && context.isRenderResponseStackTraces())
+					e.printStackTrace(w2);
+				else
+					w2.append(e2.getFullStackMessage(true));
 			}
 
 		} catch (Exception e1) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseHandler.java
index 39cedbe..2e66ff4 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseHandler.java
@@ -112,5 +112,5 @@ public interface ResponseHandler {
 	 * 	If some other exception occurred.
 	 * 	Can be used to provide an appropriate HTTP response code and message.
 	 */
-	boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, RestException;
+	boolean handle(RestRequest req, RestResponse res, ResponseObject output) throws IOException, RestException;
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseObject.java
similarity index 51%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java
copy to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseObject.java
index 780f441..4b905e2 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponseObject.java
@@ -10,52 +10,78 @@
 // * "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.response;
+package org.apache.juneau.rest;
 
-import java.net.*;
-
-import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
 
 /**
- * Represents an <code>HTTP 301 Moved Permanently</code> response.
- *
- * <p>
- * This and all future requests should be directed to the given URI.
+ * A simple pairing of a response object and metadata on how to serialize that response object.
  */
-@Response(code=301, example="'Moved Permanently'")
-public class MovedPermanently {
-
-	/** Reusable instance. */
-	public static final MovedPermanently INSTANCE = new MovedPermanently();
+public class ResponseObject {
 
-	private final URI location;
+	private ResponseMeta meta;
+	private Object value;
 
 	/**
 	 * Constructor.
+	 *
+	 * @param meta Metadata about the specified value.
+	 * @param value The POJO that makes up the response.
 	 */
-	public MovedPermanently() {
-		this(null);
+	public ResponseObject(ResponseMeta meta, Object value) {
+		this.meta = meta;
+		this.value = value;
 	}
 
 	/**
-	 * Constructor.
+	 * Returns the metadata about this response.
+	 *
+	 * @return
+	 * 	The metadata about this response.
+	 * 	<jk>Never <jk>null</jk>.
+	 */
+	public ResponseMeta getMeta() {
+		return meta;
+	}
+
+	/**
+	 * Returns the POJO that makes up this response.
 	 *
-	 * @param location <code>Location</code> header value.
+	 * @return
+	 * 	The POJO that makes up this response.
+	 * 	<jk>Never <jk>null</jk>.
 	 */
-	public MovedPermanently(URI location) {
-		this.location = location;
+	public Object getValue() {
+		return value;
 	}
 
-	@Override /* Object */
-	public String toString() {
-		return "Moved Permanently";
+	/**
+	 * Returns <jk>true</jk> if this response object is of the specified type.
+	 *
+	 * @param c The type to check against.
+	 * @return <jk>true</jk> if this response object is of the specified type.
+	 */
+	public boolean isType(Class<?> c) {
+		return c.isInstance(value);
 	}
 
 	/**
-	 * @return <code>Location</code> header value.
+	 * Returns this value cast to the specified class.
+	 *
+	 * @param c The class to cast to.
+	 * @return This value cast to the specified class.
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> T getValue(Class<T> c) {
+		return (T)value;
+	}
+
+	/**
+	 * Sets the POJO value for this response.
+	 *
+	 * @param value The POJO value to set.
 	 */
-	@Header(name="Location", description="")
-	public URI getLocation() {
-		return location;
+	public void setValue(Object value) {
+		this.value = value;
 	}
-}
\ No newline at end of file
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponsePart.java
similarity index 51%
copy from juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java
copy to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponsePart.java
index c94c23b..31b5b89 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ResponsePart.java
@@ -10,53 +10,58 @@
 // * "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.response;
+package org.apache.juneau.rest;
 
-import java.net.*;
-
-import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.httppart.*;
+import org.apache.juneau.serializer.*;
 
 /**
- * Represents an <code>HTTP 308 Permanent Redirect</code> response.
- *
- * <p>
- * The request and all future requests should be repeated using another URI. 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change.
- * So, for example, submitting a form to a permanently redirected resource may continue smoothly.
+ * Represents part of an HTTP response such as an HTTP response header.
  */
-@Response(code=308, example="'Permanent Redirect'")
-public class PermanentRedirect {
-
-	/** Reusable instance. */
-	public static final PermanentRedirect INSTANCE = new PermanentRedirect();
-
-	private final URI location;
+public class ResponsePart {
+	private final String name;
+	private final Object part;
+	private final HttpPartType partType;
+	private final HttpPartSchema schema;
+	private final HttpPartSerializer serializer;
+	private final SerializerSessionArgs args;
 
 	/**
 	 * Constructor.
+	 *
+	 * @param name The HTTP part name (e.g. the response header name).
+	 * @param partType The HTTP part type.
+	 * @param schema Schema information about the part.
+	 * @param serializer The part serializer to use to serialize the part.
+	 * @param part The part POJO being serialized.
+	 * @param args Session arguments to pass to the serializer.
 	 */
-	public PermanentRedirect() {
-		this(null);
+	public ResponsePart(String name, HttpPartType partType, HttpPartSchema schema, HttpPartSerializer serializer, Object part, SerializerSessionArgs args) {
+		this.name = name;
+		this.partType = partType;
+		this.schema = schema;
+		this.serializer = serializer;
+		this.part = part;
+		this.args = args;
 	}
 
 	/**
-	 * Constructor.
+	 * Returns the name of the part.
 	 *
-	 * @param location <code>Location</code> header value.
+	 * @return The name of the part.
 	 */
-	public PermanentRedirect(URI location) {
-		this.location = location;
-	}
-
-	@Override /* Object */
-	public String toString() {
-		return "Permanent Redirect";
+	public String getName() {
+		return name;
 	}
 
 	/**
-	 * @return <code>Location</code> header value.
+	 * Returns the value of the part converted to a string.
+	 *
+	 * @return The value of the part converted to a string.
+	 * @throws SchemaValidationException
+	 * @throws SerializeException
 	 */
-	@Header(name="Location", description="")
-	public URI getLocation() {
-		return location;
+	public String getValue() throws SchemaValidationException, SerializeException {
+		return serializer.createSession(args).serialize(partType, schema, part);
 	}
-}
\ No newline at end of file
+}
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 41f5f18..2eb3f10 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
@@ -78,7 +78,7 @@ public interface RestCallHandler {
 	 * @throws IOException
 	 * @throws RestException
 	 */
-	public void handleResponse(RestRequest req, RestResponse res, Object output) throws IOException, RestException ;
+	public void handleResponse(RestRequest req, RestResponse res, ResponseObject output) throws IOException, RestException ;
 
 	/**
 	 * Handle the case where a matching method was not found.
@@ -95,13 +95,10 @@ public interface RestCallHandler {
 	 *
 	 * @param req The servlet request.
 	 * @param res The servlet response.
-	 * @param rreq
-	 * 	The REST request.
-	 * 	<br>This may be <jk>null</jk> if an error occurred before this was initialized.
 	 * @param e The exception that occurred.
 	 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
 	 */
-	public void handleError(HttpServletRequest req, HttpServletResponse res, RestRequest rreq, Throwable e) throws IOException;
+	public void handleError(HttpServletRequest req, HttpServletResponse res, Throwable e) throws IOException;
 
 	/**
 	 * Returns the session objects for the specified request.
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 5cb30c2..8e3307c 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
@@ -834,7 +834,7 @@ public final class RestContext extends BeanContext {
 	 * <p>
 	 * Enables the following:
 	 * <ul>
-	 * 	<li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)} is called.
+	 * 	<li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)} is called.
 	 * </ul>
 	 */
 	public static final String REST_debug = PREFIX + "debug.b";
@@ -2030,7 +2030,7 @@ public final class RestContext extends BeanContext {
 	 * 		<ul>
 	 * 			<li class='jm'>{@link RestContext#isRenderResponseStackTraces() RestContext.isRenderResponseStackTraces()}
 	 * 		</ul>
-	 * 		That method is used by {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)}.
+	 * 		That method is used by {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)}.
 	 * </ul>
 	 */
 	public static final String REST_renderResponseStackTraces = PREFIX + "renderResponseStackTraces.b";
@@ -2666,7 +2666,7 @@ public final class RestContext extends BeanContext {
 	 * Affects the following methods:
 	 * <ul>
 	 * 	<li class='jm'>{@link RestContext#getStackTraceOccurrence(Throwable) RestContext.getStackTraceOccurrance(Throwable)}
-	 * 	<li class='jm'>{@link RestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)}
+	 * 	<li class='jm'>{@link RestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)}
 	 * 	<li class='jm'>{@link RestException#getOccurrence()} - Returns the number of times this exception occurred.
 	 * </ul>
 	 *
@@ -2853,6 +2853,8 @@ public final class RestContext extends BeanContext {
 	private final ClasspathResourceManager staticResourceManager;
 	private final ConcurrentHashMap<Integer,AtomicInteger> stackTraceHashes = new ConcurrentHashMap<>();
 
+	private final Map<Class<?>,ResponseMeta> responseMetas = new ConcurrentHashMap<>();
+
 	/**
 	 * Constructor.
 	 *
@@ -3050,7 +3052,7 @@ public final class RestContext extends BeanContext {
 									if (rc != SC_OK)
 										return rc;
 
-									final Object o = res.getOutput();
+									final ResponseObject ro = res.getOutput();
 
 									if ("GET".equals(req.getMethod())) {
 										res.setOutput(getMethodInfo(remoteableMethods.values()));
@@ -3073,7 +3075,7 @@ public final class RestContext extends BeanContext {
 														args = p.parseArgs(in, m.getGenericParameterTypes());
 													}
 												}
-												Object output = m.invoke(o, args);
+												Object output = m.invoke(ro.getValue(), args);
 												res.setOutput(output);
 												return SC_OK;
 											} catch (Exception e) {
@@ -4355,7 +4357,7 @@ public final class RestContext extends BeanContext {
 
 			} else if (hasAnnotation(Response.class, method, i)) {
 				s = HttpPartSchema.create(Response.class, method, i);
-				rp[i] = new RestParamDefaults.ResponseObject(method, s, t, ps);
+				rp[i] = new RestParamDefaults.ResponseParamObject(method, i, s, t, ps);
 			} else if (hasAnnotation(ResponseHeader.class, method, i)) {
 				s = HttpPartSchema.create(ResponseHeader.class, method, i);
 				rp[i] = new RestParamDefaults.ResponseHeaderObject(method, s, t, ps);
@@ -4547,4 +4549,21 @@ public final class RestContext extends BeanContext {
 	public BeanSessionArgs createDefaultSessionArgs() {
 		throw new NoSuchMethodError();
 	}
+
+	ResponseMeta getResponseMetaForObject(Object o) {
+		if (o == null)
+			return null;
+		return getResponseMeta(o.getClass());
+	}
+
+	ResponseMeta getResponseMeta(Class<?> c) {
+		ResponseMeta rm = responseMetas.get(c);
+		if (rm == ResponseMeta.NULL)
+			return null;
+		if (rm != null)
+			return rm;
+		rm = ResponseMeta.create(c, getPropertyStore());
+		responseMetas.put(c, rm == null ? ResponseMeta.NULL : rm);
+		return rm;
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
index 619d14b..12eec28 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
@@ -519,18 +519,40 @@ public class RestJavaMethod implements Comparable<RestJavaMethod>  {
 				if (! guard.guard(req, res))
 					return SC_OK;
 
-			Object output = method.invoke(context.getResource(), args);
-			if (! method.getReturnType().equals(Void.TYPE))
-				if (output != null || ! res.getOutputStreamCalled())
-					res.setOutput(output);
+			Object output;
+			try {
+				output = method.invoke(context.getResource(), args);
+				if (res.getStatus() == 0)
+					res.setStatus(200);
+				RestMethodReturn rmr = req.getRestMethodReturn();
+				if (! method.getReturnType().equals(Void.TYPE)) {
+					if (output != null || ! res.getOutputStreamCalled()) {
+						ResponseMeta rm = rmr.getResponseMeta();
+						if (rm == null)
+							rm = context.getResponseMetaForObject(output);
+						res.setOutput(new ResponseObject(rm, output));
+					}
+				}
+			} catch (InvocationTargetException e) {
+				Throwable e2 = e.getTargetException();		// Get the throwable thrown from the doX() method.
+				if (res.getStatus() == 0)
+					res.setStatus(500);
+				RestMethodThrown rmt = req.getRestMethodThrown(e2);
+				ResponseMeta rm = rmt == null ? null : rmt.getResponseMeta();
+				if (rm == null)
+					rm = context.getResponseMetaForObject(e2);
+				if (rm != null)
+					res.setOutput(new ResponseObject(rm, e2));
+				else
+					throw e;
+			}
 
 			context.postCall(req, res);
 
 			if (res.hasOutput()) {
-				output = res.getOutput();
+				ResponseObject ro = res.getOutput();
 				for (RestConverter converter : converters)
-					output = converter.convert(req, output);
-				res.setOutput(output);
+					ro.setValue(converter.convert(req, ro.getValue()));
 			}
 		} catch (IllegalArgumentException e) {
 			throw new BadRequest(e,
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java
index c8f75ee..29d1876 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java
@@ -19,7 +19,6 @@ import java.lang.reflect.*;
 import org.apache.juneau.*;
 import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.internal.*;
 
 /**
  * Contains metadata about the return type on a REST Java method.
@@ -29,21 +28,18 @@ public class RestMethodReturn {
 	private final Type type;
 	private final int code;
 	private final ObjectMap api;
-	private final HttpPartSchema schema;
-	private final HttpPartSerializer partSerializer;
+	private final ResponseMeta responseMeta;
 
 	RestMethodReturn(Method m, HttpPartSerializer partSerializer, PropertyStore ps) {
+		this.responseMeta = ResponseMeta.create(m, ps);
+
 		HttpPartSchema s = HttpPartSchema.DEFAULT;
 		if (hasAnnotation(Response.class, m))
 			s = HttpPartSchema.create(Response.class, m);
 
-		this.schema = s;
 		this.type = m.getGenericReturnType();
 		this.api = HttpPartSchema.getApiCodeMap(s, 200).unmodifiable();
 		this.code = s.getCode(200);
-
-		boolean usePS = (s.isUsePartSerializer() || s.getSerializer() != null);
-		this.partSerializer = usePS ? ObjectUtils.firstNonNull(ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps), partSerializer) : null;
 	}
 
 	/**
@@ -74,22 +70,13 @@ public class RestMethodReturn {
 	}
 
 	/**
-	 * Returns the schema for the method return type.
-	 *
-	 * @return The schema for the method return type.  Never <jk>null</jk>.
-	 */
-	public HttpPartSchema getSchema() {
-		return schema;
-	}
-
-	/**
-	 * Returns the part serializer for the method return type.
+	 * Returns metadata about the response object.
 	 *
 	 * @return
-	 * 	The part serializer for the method return type.
-	 * 	<br><jk>null</jk> if {@link Response#usePartSerializer()} is <jk>false</jk>.
+	 * 	Metadata about the response object.
+	 * 	<br>Can be <jk>null</jk> if no {@link Response} annotation is associated with the return.
 	 */
-	public HttpPartSerializer getPartSerializer() {
-		return partSerializer;
+	public ResponseMeta getResponseMeta() {
+		return responseMeta;
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java
index a4176f5..ada4e7f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java
@@ -17,7 +17,6 @@ import static org.apache.juneau.internal.ReflectionUtils.*;
 import org.apache.juneau.*;
 import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.internal.*;
 
 /**
  * Contains metadata about a throwable on a REST Java method.
@@ -27,21 +26,18 @@ public class RestMethodThrown {
 	final Class<?> type;
 	final int code;
 	final ObjectMap api;
-	private final HttpPartSchema schema;
-	final HttpPartSerializer partSerializer;
+	private final ResponseMeta responseMeta;
 
 	RestMethodThrown(Class<?> type, HttpPartSerializer partSerializer, PropertyStore ps) {
+		this.responseMeta = ResponseMeta.create(type, ps);
+
 		HttpPartSchema s = HttpPartSchema.DEFAULT;
 		if (hasAnnotation(Response.class, type))
 			s = HttpPartSchema.create(Response.class, type);
 
-		this.schema = s;
 		this.type = type;
 		this.api = HttpPartSchema.getApiCodeMap(s, 500).unmodifiable();
 		this.code = s.getCode(500);
-
-		boolean usePS = (s.isUsePartSerializer() || s.getSerializer() != null);
-		this.partSerializer = usePS ? ObjectUtils.firstNonNull(ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps), partSerializer) : null;
 	}
 
 	/**
@@ -72,22 +68,13 @@ public class RestMethodThrown {
 	}
 
 	/**
-	 * Returns the schema for the method return type.
-	 *
-	 * @return The schema for the method return type.  Never <jk>null</jk>.
-	 */
-	public HttpPartSchema getSchema() {
-		return schema;
-	}
-
-	/**
-	 * Returns the part serializer for the method return type.
+	 * Returns metadata about the thrown object.
 	 *
 	 * @return
-	 * 	The part serializer for the method return type.
-	 * 	<br><jk>null</jk> if {@link Response#usePartSerializer()} is <jk>false</jk>.
+	 * 	Metadata about the thrown object.
+	 * 	<br>Can be <jk>null</jk> if no {@link Response} annotation is associated with the thrown object.
 	 */
-	public HttpPartSerializer getPartSerializer() {
-		return partSerializer;
+	public ResponseMeta getResponseMeta() {
+		return responseMeta;
 	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
index a45b31c..80328f0 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
@@ -642,7 +642,7 @@ class RestParamDefaults {
 				@Override
 				public void onSet(Object newValue) {
 					try {
-						res.setHeader(name, partSerializer.createSession(req.getSerializerSessionArgs()).serialize(HttpPartType.HEADER, schema, newValue));
+						res.setHeader(new ResponsePart(name, HttpPartType.HEADER, schema, partSerializer, newValue, req.getSerializerSessionArgs()));
 					} catch (SerializeException | SchemaValidationException e) {
 						throw new RuntimeException(e);
 					}
@@ -660,17 +660,13 @@ class RestParamDefaults {
 		}
 	}
 
-	static final class ResponseObject extends RestMethodParam {
-		final HttpPartSerializer partSerializer;
-		final boolean usePartSerializer;
-		final HttpPartSchema schema;
+	static final class ResponseParamObject extends RestMethodParam {
 		private String _default;
+		final ResponseMeta meta;
 
-		protected ResponseObject(Method m, HttpPartSchema s, Type t, PropertyStore ps) {
+		protected ResponseParamObject(Method m, int i, HttpPartSchema s, Type t, PropertyStore ps) {
 			super(RESPONSE, m, s.getName(), t, HttpPartSchema.getApiCodeMap(s, 200));
-			this.usePartSerializer = s.isUsePartSerializer() || s.getSerializer() != null;
-			this.partSerializer = usePartSerializer ? ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps) : null;
-			this.schema = s;
+			this.meta = ResponseMeta.create(m, i, ps);
 			this._default = s.getDefault();
 
 			if (getTypeClass() == null)
@@ -686,16 +682,7 @@ class RestParamDefaults {
 			v.listener(new ValueListener() {
 				@Override
 				public void onSet(Object newValue) {
-					try {
-						if (usePartSerializer) {
-							HttpPartSerializer ps = partSerializer == null ? req.getPartSerializer() : partSerializer;
-							if (ps != null)
-								newValue = new StringReader(ps.serialize(HttpPartType.BODY, schema, newValue));
-						}
-						res.setOutput(newValue);
-					} catch (SchemaValidationException | SerializeException e) {
-						throw new RuntimeException(e);
-					}
+					res.setOutput(new ResponseObject(meta, newValue));
 				}
 			});
 			if (_default != null) {
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 5507f15..9992ee7 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
@@ -1528,13 +1528,23 @@ public final class RestRequest extends HttpServletRequestWrapper {
 		return sb.toString();
 	}
 
-	SerializerSessionArgs getSerializerSessionArgs() {
+	/**
+	 * Returns the session arguments to pass to serializers.
+	 *
+	 * @return The session arguments to pass to serializers.
+	 */
+	public SerializerSessionArgs getSerializerSessionArgs() {
 		if (serializerSessionArgs == null)
 			serializerSessionArgs = new SerializerSessionArgs(getProperties(), getJavaMethod(), getLocale(), getHeaders().getTimeZone(), null, isDebug() ? true : null, getUriContext(), isPlainText() ? true : null);
 		return serializerSessionArgs;
 	}
 
-	ParserSessionArgs getParserSessionArgs() {
+	/**
+	 * Returns the session arguments to pass to parsers.
+	 *
+	 * @return The session arguments to pass to parsers.
+	 */
+	public ParserSessionArgs getParserSessionArgs() {
 		if (parserSessionArgs == null)
 			parserSessionArgs = new ParserSessionArgs(getProperties(), getJavaMethod(), getLocale(), getHeaders().getTimeZone(), null, isDebug() ? true : null, getUriContext());
 		return parserSessionArgs;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
index 269e8b0..8d9d24b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -58,7 +58,7 @@ public final class RestResponse extends HttpServletResponseWrapper {
 
 	private final RestRequest request;
 	private RestJavaMethod restJavaMethod;
-	private Object output;                       // The POJO being sent to the output.
+	private ResponseObject output;                       // The POJO being sent to the output.
 	private boolean isNullOutput;                // The output is null (as opposed to not being set at all)
 	private RequestProperties properties;                // Response properties
 	private ServletOutputStream sos;
@@ -189,11 +189,17 @@ public final class RestResponse extends HttpServletResponseWrapper {
 	 * @return This object (for method chaining).
 	 */
 	public RestResponse setOutput(Object output) {
-		this.output = output;
+		this.output = new ResponseObject(null, output);
 		this.isNullOutput = output == null;
 		return this;
 	}
 
+	RestResponse setOutput(ResponseObject output) {
+		this.output = output;
+		this.isNullOutput = output.getValue() == null;
+		return this;
+	}
+
 	/**
 	 * Returns a programmatic interface for setting properties for the HTML doc view.
 	 *
@@ -309,7 +315,7 @@ public final class RestResponse extends HttpServletResponseWrapper {
 	 * @return This object (for method chaining).
 	 */
 	public RestResponse setOutputs(Object...output) {
-		this.output = output;
+		this.output = new ResponseObject(null, output);
 		return this;
 	}
 
@@ -318,7 +324,7 @@ public final class RestResponse extends HttpServletResponseWrapper {
 	 *
 	 * @return The output object.
 	 */
-	public Object getOutput() {
+	public ResponseObject getOutput() {
 		return output;
 	}
 
@@ -489,15 +495,6 @@ public final class RestResponse extends HttpServletResponseWrapper {
 		super.sendRedirect(uri);
 	}
 
-	/**
-	 * Returns the HTTP-part serializer associated with this response.
-	 *
-	 * @return The HTTP-part serializer associated with this response.
-	 */
-	public HttpPartSerializer getPartSerializer() {
-		return restJavaMethod.partSerializer;
-	}
-
 	@Override /* ServletResponse */
 	public void setHeader(String name, String value) {
 		// Jetty doesn't set the content type correctly if set through this method.
@@ -508,6 +505,16 @@ public final class RestResponse extends HttpServletResponseWrapper {
 			super.setHeader(name, value);
 	}
 
+	/**
+	 * Same as {@link #setHeader(String, String)} but header is defined as a response part
+	 *
+	 * @param h Header to set.
+	 * @throws SchemaValidationException
+	 * @throws SerializeException
+	 */
+	public void setHeader(ResponsePart h) throws SchemaValidationException, SerializeException {
+		setHeader(h.getName(), h.getValue());
+	}
 
 	@Override /* ServletResponse */
 	public void flushBuffer() throws IOException {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
index daf3fea..881738f 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
@@ -952,7 +952,7 @@ public @interface RestResource {
 	 * <p>
 	 * Enables the following:
 	 * <ul>
-	 * 	<li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)} is called.
+	 * 	<li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)} is called.
 	 * </ul>
 	 *
 	 * <h5 class='section'>Notes:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
index 9821f3d..425b60a 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.rest.reshandlers;
 
 import java.io.*;
+import java.net.*;
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -46,13 +47,45 @@ public class DefaultHandler implements ResponseHandler {
 
 	@SuppressWarnings("resource")
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, InternalServerError, NotAcceptable {
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, InternalServerError, NotAcceptable {
 		SerializerGroup g = res.getSerializers();
 		String accept = req.getHeaders().getString("Accept", "");
 		SerializerMatch sm = g.getSerializerMatch(accept);
 
-		RestMethodReturn rmr = req.getRestMethodReturn();
-		res.setStatus(rmr.getCode());
+		Object o = ro.getValue();
+
+		ResponseMeta rm = ro.getMeta();
+
+		if (rm != null) {
+			if (rm.getCode() != 0)
+				res.setStatus(rm.getCode());
+
+			for (ResponsePropertyMeta hm : rm.getHeaderMetas()) {
+				try {
+					Object headerValue = hm.getGetter().invoke(o);
+					if (headerValue instanceof URI)
+						headerValue = req.getUriResolver().resolve(headerValue);
+					res.setHeader(new ResponsePart(hm.getPartName(), HttpPartType.HEADER, hm.getSchema(), hm.getSerializer(req.getPartSerializer()), headerValue, req.getSerializerSessionArgs()));
+				} catch (Exception e) {
+					throw new InternalServerError(e, "Could not set header ''{0}''", hm.getPartName());
+				}
+			}
+
+			if (rm.isUsePartSerializer()) {
+				res.setContentType("text/plain");
+				HttpPartSerializer ps = rm.getPartSerializer(req.getPartSerializer());
+				if (ps != null) {
+					try (FinishablePrintWriter w = res.getNegotiatedWriter()) {
+						w.append(ps.serialize(HttpPartType.BODY, rm.getSchema(), o));
+						w.flush();
+						w.finish();
+					} catch (SchemaValidationException | SerializeException e) {
+						throw new InternalServerError(e);
+					}
+					return true;
+				}
+			}
+		}
 
 		if (sm != null) {
 			Serializer s = sm.getSerializer();
@@ -82,19 +115,19 @@ public class DefaultHandler implements ResponseHandler {
 					if (req.isPlainText()) {
 						FinishablePrintWriter w = res.getNegotiatedWriter();
 						ByteArrayOutputStream baos = new ByteArrayOutputStream();
-						session.serialize(output, baos);
+						session.serialize(o, baos);
 						w.write(StringUtils.toSpacedHex(baos.toByteArray()));
 						w.flush();
 						w.finish();
 					} else {
 						FinishableServletOutputStream os = res.getNegotiatedOutputStream();
-						session.serialize(output, os);
+						session.serialize(o, os);
 						os.flush();
 						os.finish();
 					}
 				} else {
 					FinishablePrintWriter w = res.getNegotiatedWriter();
-					session.serialize(output, w);
+					session.serialize(o, w);
 					w.flush();
 					w.finish();
 				}
@@ -105,25 +138,12 @@ public class DefaultHandler implements ResponseHandler {
 
 		}
 
-		HttpPartSerializer ps = rmr.getPartSerializer();
-		if (ps != null) {
-			try {
-				FinishablePrintWriter w = res.getNegotiatedWriter();
-				w.append(ps.serialize(rmr.getSchema(), output));
-				w.flush();
-				w.finish();
-			} catch (SchemaValidationException | SerializeException e) {
-				throw new InternalServerError(e);
-			}
-			return true;
-		}
-
 		// Non-existent Accept or plain/text can just be serialized as-is.
 		if ("".equals(accept) || "plain/text".equals(accept)) {
 			FinishablePrintWriter w = res.getNegotiatedWriter();
-			ClassMeta<?> cm = req.getBeanSession().getClassMetaForObject(output);
+			ClassMeta<?> cm = req.getBeanSession().getClassMetaForObject(o);
 			if (cm != null)
-				w.append(cm.toString(output));
+				w.append(cm.toString(o));
 			w.flush();
 			w.finish();
 			return true;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/InputStreamHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/InputStreamHandler.java
index 4fe68be..8f2948e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/InputStreamHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/InputStreamHandler.java
@@ -35,11 +35,11 @@ import org.apache.juneau.utils.*;
 public final class InputStreamHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, NotAcceptable, RestException {
-		if (output instanceof InputStream) {
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, NotAcceptable, RestException {
+		if (ro.isType(InputStream.class)) {
 			res.setHeader("Content-Type", res.getContentType());
 			try (OutputStream os = res.getNegotiatedOutputStream()) {
-				IOPipe.create(output, os).run();
+				IOPipe.create(ro.getValue(InputStream.class), os).run();
 			}
 			return true;
 		}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ReaderHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ReaderHandler.java
index 027f6fa..c89c8c1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ReaderHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ReaderHandler.java
@@ -32,10 +32,10 @@ import org.apache.juneau.utils.*;
 public final class ReaderHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, NotAcceptable, RestException {
-		if (output instanceof Reader) {
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, NotAcceptable, RestException {
+		if (ro.isType(Reader.class)) {
 			try (Writer w = res.getNegotiatedWriter()) {
-				IOPipe.create(output, w).run();
+				IOPipe.create(ro.getValue(Reader.class), w).run();
 			}
 			return true;
 		}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/RedirectHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/RedirectHandler.java
index 354b4a3..798b9be 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/RedirectHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/RedirectHandler.java
@@ -28,9 +28,9 @@ import org.apache.juneau.rest.helper.*;
 public final class RedirectHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, RestException {
-		if (output instanceof Redirect) {
-			Redirect r = (Redirect)output;
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, RestException {
+		if (ro.isType(Redirect.class)) {
+			Redirect r = ro.getValue(Redirect.class);
 			String uri = req.getUriResolver().resolve(r.getURI());
 			int rc = r.getHttpResponseCode();
 			if (rc != 0)
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/StreamableHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/StreamableHandler.java
index d8d1486..5752ada 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/StreamableHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/StreamableHandler.java
@@ -37,10 +37,10 @@ import org.apache.juneau.rest.helper.*;
 public final class StreamableHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, RestException {
-		if (output instanceof Streamable) {
-			if (output instanceof StreamResource) {
-				StreamResource r = (StreamResource)output;
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, RestException {
+		if (ro.isType(Streamable.class)) {
+			if (ro.isType(StreamResource.class)) {
+				StreamResource r = ro.getValue(StreamResource.class);
 				MediaType mediaType = r.getMediaType();
 				if (mediaType != null)
 					res.setContentType(mediaType.toString());
@@ -48,7 +48,7 @@ public final class StreamableHandler implements ResponseHandler {
 					res.setHeader(h.getKey(), asString(h.getValue()));
 			}
 			try (OutputStream os = res.getOutputStream()) {
-				((Streamable)output).streamTo(os);
+				ro.getValue(Streamable.class).streamTo(os);
 			}
 			return true;
 		}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/WritableHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/WritableHandler.java
index 61299e6..ddcf849 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/WritableHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/WritableHandler.java
@@ -37,10 +37,10 @@ import org.apache.juneau.rest.helper.*;
 public final class WritableHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, NotAcceptable, RestException {
-		if (output instanceof Writable) {
-			if (output instanceof ReaderResource) {
-				ReaderResource r = (ReaderResource)output;
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, NotAcceptable, RestException {
+		if (ro.isType(Writable.class)) {
+			if (ro.isType(ReaderResource.class)) {
+				ReaderResource r = ro.getValue(ReaderResource.class);
 				MediaType mediaType = r.getMediaType();
 				if (mediaType != null)
 					res.setContentType(mediaType.toString());
@@ -48,7 +48,7 @@ public final class WritableHandler implements ResponseHandler {
 					res.setHeader(h.getKey(), asString(h.getValue()));
 			}
 			try (Writer w = res.getNegotiatedWriter()) {
-				((Writable)output).writeTo(w);
+				ro.getValue(Writable.class).writeTo(w);
 			}
 			return true;
 		}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ZipFileListResponseHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ZipFileListResponseHandler.java
index c548d05..5deda07 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ZipFileListResponseHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/ZipFileListResponseHandler.java
@@ -44,9 +44,9 @@ import org.apache.juneau.utils.ZipFileList.*;
 public class ZipFileListResponseHandler implements ResponseHandler {
 
 	@Override /* ResponseHandler */
-	public boolean handle(RestRequest req, RestResponse res, Object output) throws IOException, RestException {
-		if (output.getClass() == ZipFileList.class) {
-			ZipFileList m = (ZipFileList)output;
+	public boolean handle(RestRequest req, RestResponse res, ResponseObject ro) throws IOException, RestException {
+		if (ro.isType(ZipFileList.class)) {
+			ZipFileList m = ro.getValue(ZipFileList.class);
 			res.setContentType("application/zip");
 			res.setHeader("Content-Disposition", "attachment;filename=" + m.fileName); //$NON-NLS-2$
 			try (OutputStream os = res.getOutputStream()) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java
index 780f441..06d4e50 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/MovedPermanently.java
@@ -34,7 +34,7 @@ public class MovedPermanently {
 	 * Constructor.
 	 */
 	public MovedPermanently() {
-		this(null);
+		this((URI)null);
 	}
 
 	/**
@@ -46,6 +46,15 @@ public class MovedPermanently {
 		this.location = location;
 	}
 
+	/**
+	 * Constructor.
+	 *
+	 * @param location <code>Location</code> header value.
+	 */
+	public MovedPermanently(String location) {
+		this.location = URI.create(location);
+	}
+
 	@Override /* Object */
 	public String toString() {
 		return "Moved Permanently";
@@ -54,7 +63,7 @@ public class MovedPermanently {
 	/**
 	 * @return <code>Location</code> header value.
 	 */
-	@Header(name="Location", description="")
+	@Header(name="Location")
 	public URI getLocation() {
 		return location;
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java
index c94c23b..a4fb513 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/PermanentRedirect.java
@@ -35,7 +35,16 @@ public class PermanentRedirect {
 	 * Constructor.
 	 */
 	public PermanentRedirect() {
-		this(null);
+		this((URI)null);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param location <code>Location</code> header value.
+	 */
+	public PermanentRedirect(String location) {
+		this.location = URI.create(location);
 	}
 
 	/**
@@ -55,7 +64,7 @@ public class PermanentRedirect {
 	/**
 	 * @return <code>Location</code> header value.
 	 */
-	@Header(name="Location", description="")
+	@Header(name="Location")
 	public URI getLocation() {
 		return location;
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/SeeOther.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/SeeOther.java
index b2f9056..8b7848b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/SeeOther.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/SeeOther.java
@@ -35,7 +35,16 @@ public class SeeOther {
 	 * Constructor.
 	 */
 	public SeeOther() {
-		this(null);
+		this((URI)null);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param location <code>Location</code> header value.
+	 */
+	public SeeOther(String location) {
+		this.location = URI.create(location);
 	}
 
 	/**
@@ -55,7 +64,7 @@ public class SeeOther {
 	/**
 	 * @return <code>Location</code> header value.
 	 */
-	@Header(name="Location", description="")
+	@Header(name="Location")
 	public URI getLocation() {
 		return location;
 	}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/TemporaryRedirect.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/TemporaryRedirect.java
index f24197f..a46121c 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/TemporaryRedirect.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/TemporaryRedirect.java
@@ -36,7 +36,16 @@ public class TemporaryRedirect {
 	 * Constructor.
 	 */
 	public TemporaryRedirect() {
-		this(null);
+		this((URI)null);
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param location <code>Location</code> header value.
+	 */
+	public TemporaryRedirect(String location) {
+		this.location = URI.create(location);
 	}
 
 	/**
@@ -57,7 +66,7 @@ public class TemporaryRedirect {
 	/**
 	 * @return <code>Location</code> header value.
 	 */
-	@Header(name="Location", description="")
+	@Header(name="Location")
 	public URI getLocation() {
 		return location;
 	}
diff --git a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java
index 224f3e7..02e623f 100644
--- a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java
@@ -246,7 +246,7 @@ public class ResponseAnnotationTest {
 
 	@Test
 	public void b06_dontUseOnThrown() throws Exception {
-		b.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("HTTP 500: Internal Server Error");
+		b.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("foo");
 	}
 
 	@Test
@@ -364,7 +364,7 @@ public class ResponseAnnotationTest {
 
 	@Test
 	public void c06_dontUseOnThrown() throws Exception {
-		c.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("HTTP 500: Internal Server Error");
+		c.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("foo");
 	}
 
 	@Test
diff --git a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/response/BasicTest.java b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/response/BasicTest.java
index f301eaa..cbe5d74 100644
--- a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/response/BasicTest.java
+++ b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/response/BasicTest.java
@@ -21,6 +21,10 @@ import org.junit.runners.*;
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class BasicTest {
 
+	//-----------------------------------------------------------------------------------------------------------------
+	// Basic sanity tests
+	//-----------------------------------------------------------------------------------------------------------------
+
 	@RestResource
 	public static class A {
 		@RestMethod public Accepted accepted() { return new Accepted(); }
@@ -137,4 +141,35 @@ public class BasicTest {
 	public void a22_useProxy() throws Exception {
 		a.get("/useProxy").execute().assertStatus(305).assertBody("Use Proxy");
 	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Statuses with URIs.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@RestResource
+	public static class B {
+		@RestMethod public MovedPermanently movedPermanently() { return new MovedPermanently("servlet:/foo"); }
+		@RestMethod public PermanentRedirect permanentRedirect() { return new PermanentRedirect("servlet:/foo"); }
+		@RestMethod public SeeOther seeOther() { return new SeeOther("servlet:/foo"); }
+		@RestMethod public TemporaryRedirect temporaryRedirect() { return new TemporaryRedirect("servlet:/foo"); }
+	}
+
+	static MockRest b = MockRest.create(B.class);
+
+	@Test
+	public void b01_movedPermanently() throws Exception {
+		b.get("/movedPermanently").execute().assertStatus(301).assertBody("Moved Permanently").assertHeader("Location", "/foo");
+	}
+	@Test
+	public void b02_permanentRedirect() throws Exception {
+		b.get("/permanentRedirect").execute().assertStatus(308).assertBody("Permanent Redirect").assertHeader("Location", "/foo");
+	}
+	@Test
+	public void b03_seeOther() throws Exception {
+		b.get("/seeOther").execute().assertStatus(303).assertBody("See Other").assertHeader("Location", "/foo");
+	}
+	@Test
+	public void b04_temporaryRedirect() throws Exception {
+		b.get("/temporaryRedirect").execute().assertStatus(307).assertBody("Temporary Redirect").assertHeader("Location", "/foo");
+	}
 }