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/09/30 20:45:18 UTC

[juneau] branch master updated: Support for end-to-end REST Java interfaces.

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 8abb14f  Support for end-to-end REST Java interfaces.
8abb14f is described below

commit 8abb14f95f55f1b66dae5b3093d297ae5af9c5b4
Author: JamesBognar <ja...@apache.org>
AuthorDate: Sun Sep 30 16:44:58 2018 -0400

    Support for end-to-end REST Java interfaces.
---
 .../java/org/apache/juneau/internal/IOUtils.java   | 317 ++++++++++------
 .../main/java/org/apache/juneau/utils/IOPipe.java  |  69 +++-
 .../org/apache/juneau/rest/client/RestCall.java    |  17 +
 .../org/apache/juneau/rest/client/RestClient.java  |   2 +-
 .../rest/client/remote/RemoteMethodReturn.java     |   7 +-
 .../rest/client/remote/EndToEndInterfaceTest.java  | 405 +++++++++++++++++++++
 .../java/org/apache/juneau/rest/RestContext.java   |   6 +-
 .../org/apache/juneau/rest/RestJavaMethod.java     |   2 +-
 .../java/org/apache/juneau/rest/RestRequest.java   |  52 ++-
 .../apache/juneau/rest/helper/ReaderResource.java  | 211 +++++++++--
 .../juneau/rest/helper/ReaderResourceBuilder.java  | 127 -------
 .../apache/juneau/rest/helper/StreamResource.java  | 208 +++++++++--
 .../juneau/rest/helper/StreamResourceBuilder.java  | 115 ------
 .../org/apache/juneau/rest/response/Found.java     |  12 +
 .../juneau/rest/response/MovedPermanently.java     |  12 +
 .../juneau/rest/response/PermanentRedirect.java    |  12 +
 .../org/apache/juneau/rest/response/SeeOther.java  |  12 +
 .../juneau/rest/response/TemporaryRedirect.java    |  12 +
 .../rest/annotation/AnnotationInheritanceTest.java |  70 +++-
 19 files changed, 1189 insertions(+), 479 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/IOUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/IOUtils.java
index efea505..074baa2 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/IOUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/IOUtils.java
@@ -41,21 +41,6 @@ public final class IOUtils {
 	}
 
 	/**
-	 * Reads the contents of a file into a string.
-	 *
-	 * @param in The file to read using default character encoding.
-	 * @return The contents of the reader as a string, or <jk>null</jk> if file does not exist.
-	 * @throws IOException If a problem occurred trying to read from the reader.
-	 */
-	public static String read(File in) throws IOException {
-		if (in == null || ! in.exists())
-			return null;
-		try (Reader r = FileReaderBuilder.create(in).build()) {
-			return read(r, 0, 1024);
-		}
-	}
-
-	/**
 	 * Reads the specified object to a <code>String</code>.
 	 *
 	 * <p>
@@ -68,53 +53,54 @@ public final class IOUtils {
 	 * 	<li><code><jk>byte</jk>[]</code>
 	 * </ul>
 	 *
-	 * @param o The object to read.
+	 * @param in The object to read.
 	 * @return The object serialized to a string, or <jk>null</jk> if it wasn't a supported type.
 	 * @throws IOException
 	 */
-	public static String read(Object o) throws IOException {
-		if (o instanceof CharSequence)
-			return o.toString();
-		if (o instanceof File)
-			return read((File)o);
-		if (o instanceof Reader)
-			return read((Reader)o);
-		if (o instanceof InputStream)
-			return read((InputStream)o);
-		if (o instanceof byte[])
-			return read(new ByteArrayInputStream((byte[])o));
-		return null;
+	public static String read(Object in) throws IOException {
+		if (in == null)
+			return null;
+		if (in instanceof CharSequence)
+			return in.toString();
+		if (in instanceof File)
+			return read((File)in);
+		if (in instanceof Reader)
+			return read((Reader)in);
+		if (in instanceof InputStream)
+			return read((InputStream)in);
+		if (in instanceof byte[])
+			return read(new ByteArrayInputStream((byte[])in));
+		throw new IOException("Cannot convert object of type '"+in.getClass().getName()+"' to a String.");
 	}
 
 	/**
-	 * Writes the contents of the specified <code>Reader</code> to the specified file.
+	 * Same as {@link #read(Object)} but appends all the input into a single String.
 	 *
-	 * @param out The file to write the output to.
-	 * @param in The reader to pipe from.
-	 * @return The number of characters written to the file.
+	 * @param in The objects to read.
+	 * @return The objects serialized to a string, never <jk>null</jk>.
 	 * @throws IOException
 	 */
-	public static int write(File out, Reader in) throws IOException {
-		assertFieldNotNull(out, "out");
-		assertFieldNotNull(in, "in");
-		try (Writer w = FileWriterBuilder.create(out).build()) {
-			return IOPipe.create(in, w).run();
-		}
+	public static String read(Object...in) throws IOException {
+		if (in.length == 1)
+			return read(in[0]);
+		StringWriter sw = new StringWriter();
+		for (Object o : in)
+			sw.write(emptyIfNull(read(o)));
+		return sw.toString();
 	}
 
 	/**
-	 * Writes the contents of the specified <code>InputStream</code> to the specified file.
+	 * Reads the contents of a file into a string.
 	 *
-	 * @param out The file to write the output to.
-	 * @param in The input stream to pipe from.
-	 * @return The number of characters written to the file.
-	 * @throws IOException
+	 * @param in The file to read using default character encoding.
+	 * @return The contents of the reader as a string, or <jk>null</jk> if file does not exist.
+	 * @throws IOException If a problem occurred trying to read from the reader.
 	 */
-	public static int write(File out, InputStream in) throws IOException {
-		assertFieldNotNull(out, "out");
-		assertFieldNotNull(in, "in");
-		try (OutputStream os = new FileOutputStream(out)) {
-			return IOPipe.create(in, os).run();
+	public static String read(File in) throws IOException {
+		if (in == null || ! in.exists())
+			return null;
+		try (Reader r = FileReaderBuilder.create(in).build()) {
+			return read(r, 0, 1024);
 		}
 	}
 
@@ -157,48 +143,6 @@ public final class IOUtils {
 	}
 
 	/**
-	 * Read the specified input stream into a byte array and closes the stream.
-	 *
-	 * @param in The input stream.
-	 * @param bufferSize The expected size of the buffer.
-	 * @return The contents of the stream as a byte array.
-	 * @throws IOException Thrown by underlying stream.
-	 */
-	public static byte[] readBytes(InputStream in, int bufferSize) throws IOException {
-		if (in == null)
-			return null;
-		ByteArrayOutputStream buff = new ByteArrayOutputStream(bufferSize);
-		int nRead;
-		byte[] b = new byte[Math.min(bufferSize, 8192)];
-
-		try {
-			while ((nRead = in.read(b, 0, b.length)) != -1)
-				buff.write(b, 0, nRead);
-			buff.flush();
-
-			return buff.toByteArray();
-		} finally {
-			in.close();
-		}
-	}
-
-	/**
-	 * Reads a raw stream of bytes from the specified file.
-	 *
-	 * @param f The file to read.
-	 * @return A byte array containing the contents of the file.
-	 * @throws IOException
-	 */
-	public static byte[] readBytes(File f) throws IOException {
-		if (f == null || ! (f.exists() && f.canRead()))
-			return null;
-
-		try (FileInputStream fis = new FileInputStream(f)) {
-			return readBytes(fis, (int)f.length());
-		}
-	}
-
-	/**
 	 * Reads the specified input into a {@link String} until the end of the input is reached.
 	 *
 	 * <p>
@@ -217,6 +161,8 @@ public final class IOUtils {
 	public static String read(Reader in, int length, int bufferSize) throws IOException {
 		if (in == null)
 			return null;
+		if (bufferSize == 0)
+			bufferSize = 1024;
 		length = (length <= 0 ? bufferSize : length);
 		StringBuilder sb = new StringBuilder(length); // Assume they're ASCII characters.
 		try {
@@ -231,55 +177,194 @@ public final class IOUtils {
 	}
 
 	/**
-	 * Pipes the contents of the specified reader into the writer.
+	 * Read the specified object into a byte array.
 	 *
-	 * <p>
-	 * The reader is closed, the writer is not.
+	 * @param in
+	 * 	The object to read into a byte array.
+	 * 	<br>Can be any of the following types:
+	 * 	<ul>
+	 * 		<li><code><jk>byte</jk>[]</code>
+	 * 		<li>{@link InputStream}
+	 * 		<li>{@link Reader}
+	 * 		<li>{@link CharSequence}
+	 * 		<li>{@link File}
+	 * 	</ul>
+	 * @param buffSize
+	 * 	The buffer size to use.
+	 * @return The contents of the stream as a byte array.
+	 * @throws IOException Thrown by underlying stream or if object is not a supported type.
+	 */
+	public static byte[] readBytes(Object in, int buffSize) throws IOException {
+		if (in == null)
+			return new byte[0];
+		if (in instanceof byte[])
+			return (byte[])in;
+		if (in instanceof CharSequence)
+			return in.toString().getBytes(UTF8);
+		if (in instanceof InputStream)
+			return readBytes((InputStream)in, buffSize);
+		if (in instanceof Reader)
+			return read((Reader)in, 0, buffSize).getBytes(UTF8);
+		if (in instanceof File)
+			return readBytes((File)in, buffSize);
+		throw new IOException("Cannot convert object of type '"+in.getClass().getName()+"' to a byte array.");
+	}
+
+	/**
+	 * Read the specified input stream into a byte array.
 	 *
 	 * @param in
-	 * 	The reader to pipe from.
-	 * @param out
-	 * 	The writer to pipe to.
+	 * 	The stream to read into a byte array.
+	 * @return The contents of the stream as a byte array.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static byte[] readBytes(InputStream in) throws IOException {
+		return readBytes(in, 1024);
+	}
+
+	/**
+	 * Read the specified input stream into a byte array.
+	 *
+	 * @param in
+	 * 	The stream to read into a byte array.
+	 * @param buffSize
+	 * 	The buffer size to use.
+	 * @return The contents of the stream as a byte array.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static byte[] readBytes(InputStream in, int buffSize) throws IOException {
+		if (buffSize == 0)
+			buffSize = 1024;
+        try (final ByteArrayOutputStream buff = new ByteArrayOutputStream(buffSize)) {
+			int nRead;
+			byte[] b = new byte[buffSize];
+			while ((nRead = in.read(b, 0, b.length)) != -1)
+				buff.write(b, 0, nRead);
+			buff.flush();
+			return buff.toByteArray();
+        }
+	}
+
+	/**
+	 * Read the specified file into a byte array.
+	 *
+	 * @param in
+	 * 	The file to read into a byte array.
+	 * @return The contents of the file as a byte array.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static byte[] readBytes(File in) throws IOException {
+		return readBytes(in, 1024);
+	}
+
+	/**
+	 * Read the specified file into a byte array.
+	 *
+	 * @param in
+	 * 	The file to read into a byte array.
+	 * @param buffSize
+	 * 	The buffer size to use.
+	 * @return The contents of the file as a byte array.
+	 * @throws IOException Thrown by underlying stream.
+	 */
+	public static byte[] readBytes(File in, int buffSize) throws IOException {
+		if (buffSize == 0)
+			buffSize = 1024;
+		if (! (in.exists() && in.canRead()))
+			return new byte[0];
+		buffSize = Math.min((int)in.length(), buffSize);
+		try (FileInputStream fis = new FileInputStream(in)) {
+			return readBytes(fis, buffSize);
+		}
+	}
+
+	/**
+	 * Shortcut for calling <code>readBytes(in, 1024);</code>
+	 *
+	 * @param in
+	 * 	The object to read into a byte array.
+	 * 	<br>Can be any of the following types:
+	 * 	<ul>
+	 * 		<li><code><jk>byte</jk>[]</code>
+	 * 		<li>{@link InputStream}
+	 * 		<li>{@link Reader}
+	 * 		<li>{@link CharSequence}
+	 * 		<li>{@link File}
+	 * 	</ul>
+	 * @return The contents of the stream as a byte array.
+	 * @throws IOException Thrown by underlying stream or if object is not a supported type.
+	 */
+	public static byte[] readBytes(Object in) throws IOException {
+		return readBytes(in, 1024);
+	}
+
+	/**
+	 * Same as {@link #readBytes(Object)} but appends all the input into a single byte array.
+	 *
+	 * @param in The objects to read.
+	 * @return The objects serialized to a byte array, never <jk>null</jk>.
+	 * @throws IOException
+	 */
+	public static byte[] readBytes(Object...in) throws IOException {
+		if (in.length == 1)
+			return readBytes(in[0]);
+        try (final ByteArrayOutputStream buff = new ByteArrayOutputStream(1024)) {
+			for (Object o : in) {
+				byte[] bo = readBytes(o);
+				if (bo != null)
+					buff.write(bo);
+			}
+			buff.flush();
+			return buff.toByteArray();
+        }
+	}
+
+	/**
+	 * Writes the contents of the specified <code>Reader</code> to the specified file.
+	 *
+	 * @param out The file to write the output to.
+	 * @param in The reader to pipe from.
+	 * @return The number of characters written to the file.
 	 * @throws IOException
 	 */
-	public static void pipe(Reader in, Writer out) throws IOException {
+	public static int write(File out, Reader in) throws IOException {
 		assertFieldNotNull(out, "out");
 		assertFieldNotNull(in, "in");
-		IOPipe.create(in, out).run();
+		try (Writer w = FileWriterBuilder.create(out).build()) {
+			return IOPipe.create(in, w).run();
+		}
 	}
 
 	/**
-	 * Pipes the contents of the specified object into the writer.
-	 *
-	 * <p>
-	 * The reader is closed, the writer is not.
+	 * Writes the contents of the specified <code>InputStream</code> to the specified file.
 	 *
-	 * @param in
-	 * 	The input to pipe from.
-	 * 	Can be any of the types defined by {@link #toReader(Object)}.
-	 * @param out
-	 * 	The writer to pipe to.
+	 * @param out The file to write the output to.
+	 * @param in The input stream to pipe from.
+	 * @return The number of characters written to the file.
 	 * @throws IOException
 	 */
-	public static void pipe(Object in, Writer out) throws IOException {
-		pipe(toReader(in), out);
+	public static int write(File out, InputStream in) throws IOException {
+		assertFieldNotNull(out, "out");
+		assertFieldNotNull(in, "in");
+		try (OutputStream os = new FileOutputStream(out)) {
+			return IOPipe.create(in, os).run();
+		}
 	}
 
 	/**
-	 * Pipes the contents of the specified streams.
+	 * Pipes the contents of the specified object into the writer.
 	 *
 	 * <p>
-	 * The input stream is closed, the output stream is not.
+	 * The reader is closed, the writer is not.
 	 *
 	 * @param in
-	 * 	The reader to pipe from.
+	 * 	The input to pipe from.
+	 * 	Can be any of the types defined by {@link #toReader(Object)}.
 	 * @param out
 	 * 	The writer to pipe to.
 	 * @throws IOException
 	 */
-	public static void pipe(InputStream in, OutputStream out) throws IOException {
-		assertFieldNotNull(out, "out");
-		assertFieldNotNull(in, "in");
+	public static void pipe(Object in, Writer out) throws IOException {
 		IOPipe.create(in, out).run();
 	}
 
@@ -297,7 +382,7 @@ public final class IOUtils {
 	 * @throws IOException
 	 */
 	public static void pipe(Object in, OutputStream out) throws IOException {
-		pipe(toInputStream(in), out);
+		IOPipe.create(in, out).run();
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/IOPipe.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/IOPipe.java
index 8990c4e..8bd7428 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/IOPipe.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/IOPipe.java
@@ -47,12 +47,10 @@ public class IOPipe {
 		assertFieldNotNull(input, "input");
 		assertFieldNotNull(output, "output");
 
-		if (input instanceof CharSequence)
-			this.input = new StringReader(input.toString());
-		else if (input instanceof InputStream || input instanceof Reader)
+		if (input instanceof InputStream || input instanceof Reader || input instanceof File || input instanceof byte[] || input instanceof CharSequence || input == null)
 			this.input = input;
 		else
-			illegalArg("Invalid input class type.  Must be one of the following:  InputStream, Reader, CharSequence");
+			illegalArg("Invalid input class type.  Must be one of the following:  InputStream, Reader, CharSequence, byte[], File");
 
 		if (output instanceof OutputStream || output instanceof Writer)
 			this.output = output;
@@ -165,22 +163,41 @@ public class IOPipe {
 		int c = 0;
 
 		try {
-			if (input instanceof InputStream && output instanceof OutputStream && lineProcessor == null) {
-				InputStream in = (InputStream)input;
+			if (input == null)
+				return 0;
+
+			if ((input instanceof InputStream || input instanceof byte[]) && output instanceof OutputStream && lineProcessor == null) {
 				OutputStream out = (OutputStream)output;
-				byte[] b = new byte[buffSize];
-				int i;
-				while ((i = in.read(b)) > 0) {
-					c += i;
-					out.write(b, 0, i);
+				if (input instanceof InputStream) {
+					InputStream in = (InputStream)input;
+					byte[] b = new byte[buffSize];
+					int i;
+					while ((i = in.read(b)) > 0) {
+						c += i;
+						out.write(b, 0, i);
+					}
+				} else {
+					byte[] b = (byte[])input;
+					out.write(b);
+					c = b.length;
 				}
 				out.flush();
 			} else {
-				Reader in = (input instanceof Reader ? (Reader)input : new InputStreamReader((InputStream)input, UTF8));
+				@SuppressWarnings("resource")
 				Writer out = (output instanceof Writer ? (Writer)output : new OutputStreamWriter((OutputStream)output, UTF8));
-				output = out;
-				input = in;
+				closeIn |= input instanceof File;
 				if (byLines || lineProcessor != null) {
+					Reader in = null;
+					if (input instanceof Reader)
+						in = (Reader)input;
+					else if (input instanceof InputStream)
+						in = new InputStreamReader((InputStream)input, UTF8);
+					else if (input instanceof File)
+						in = new FileReader((File)input);
+					else if (input instanceof byte[])
+						in = new StringReader(new String((byte[])input, "UTF8"));
+					else if (input instanceof CharSequence)
+						in = new StringReader(input.toString());
 					try (Scanner s = new Scanner(in)) {
 						while (s.hasNextLine()) {
 							String l = s.nextLine();
@@ -195,11 +212,25 @@ public class IOPipe {
 						}
 					}
 				} else {
-					int i;
-					char[] b = new char[buffSize];
-					while ((i = in.read(b)) > 0) {
-						c += i;
-						out.write(b, 0, i);
+					if (input instanceof InputStream)
+						input = new InputStreamReader((InputStream)input, UTF8);
+					else if (input instanceof File)
+						input = new FileReader((File)input);
+					else if (input instanceof byte[])
+						input = new String((byte[])input, UTF8);
+
+					if (input instanceof Reader) {
+						Reader in = (Reader)input;
+						int i;
+						char[] b = new char[buffSize];
+						while ((i = in.read(b)) > 0) {
+							c += i;
+							out.write(b, 0, i);
+						}
+					} else {
+						String s = input.toString();
+						out.write(s);
+						c = s.length();
 					}
 				}
 				out.flush();
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
index d1c53da..ceff58b 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
@@ -16,6 +16,7 @@ import static org.apache.juneau.internal.ClassUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 import static org.apache.juneau.httppart.HttpPartType.*;
+import static org.apache.http.HttpStatus.*;
 
 import java.io.*;
 import java.lang.reflect.*;
@@ -2243,6 +2244,7 @@ public final class RestCall extends BeanSession implements Closeable {
 
 			connect();
 			Header h = response.getFirstHeader("Content-Type");
+			int sc = response.getStatusLine().getStatusCode();
 			String ct = firstNonEmpty(h == null ? null : h.getValue(), "text/plain");
 
 			MediaType mt = MediaType.forString(ct);
@@ -2254,6 +2256,21 @@ public final class RestCall extends BeanSession implements Closeable {
 
 			if (parser != null) {
 				try (Closeable in = parser.isReaderParser() ? getReader() : getInputStream()) {
+
+					// HttpClient automatically ignores the content body for certain HTTP status codes.
+					// So instantiate the object anyway if it has a no-arg constructor.
+					// This allows a remote resource method to return a NoContent object for example.
+					if (in == null && (sc < SC_OK || sc == SC_NO_CONTENT || sc == SC_NOT_MODIFIED || sc == SC_RESET_CONTENT)) {
+						Constructor<T> c = ClassUtils.findNoArgConstructor(type.getInnerClass(), Visibility.PUBLIC);
+						if (c != null) {
+							try {
+								return c.newInstance();
+							} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+								throw new ParseException(e);
+							}
+						}
+					}
+
 					ParserSessionArgs pArgs = new ParserSessionArgs(this.getProperties(), null, response.getLocale(), null, mt, responseBodySchema, false, null);
 					return parser.createSession(pArgs).parse(in, type);
 				}
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index d0817cc..4e2b0f0 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -1121,7 +1121,7 @@ public class RestClient extends BeanContext implements Closeable {
 							} else if (rmr.getReturnValue() == RemoteReturn.BEAN) {
 								return rc.getResponse(rmr.getResponseBeanMeta());
 							} else {
-								Object v = rc.getResponseBody(method.getGenericReturnType());
+								Object v = rc.getResponseBody(rmr.getReturnType());
 								if (v == null && method.getReturnType().isPrimitive())
 									v = ClassUtils.getPrimitiveDefault(method.getReturnType());
 								return v;
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteMethodReturn.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteMethodReturn.java
index 03c2148..eb985a9 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteMethodReturn.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteMethodReturn.java
@@ -36,14 +36,15 @@ public final class RemoteMethodReturn {
 
 	RemoteMethodReturn(Method m) {
 		RemoteMethod rm = m.getAnnotation(RemoteMethod.class);
-		RemoteReturn rv = m.getReturnType() == void.class ? RemoteReturn.NONE : rm == null ? RemoteReturn.BODY : rm.returns();
-		this.returnType = m.getGenericReturnType();
-		if (hasAnnotation(Response.class, m)) {
+		Class<?> rt = m.getReturnType();
+		RemoteReturn rv = rt == void.class ? RemoteReturn.NONE : rm == null ? RemoteReturn.BODY : rm.returns();
+		if (hasAnnotation(Response.class, rt) && rt.isInterface()) {
 			this.meta = ResponseBeanMeta.create(m, PropertyStore.DEFAULT);
 			rv = RemoteReturn.BEAN;
 		} else {
 			this.meta = null;
 		}
+		this.returnType = m.getGenericReturnType();
 		this.returnValue = rv;
 	}
 
diff --git a/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/EndToEndInterfaceTest.java b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/EndToEndInterfaceTest.java
new file mode 100644
index 0000000..d31efcd
--- /dev/null
+++ b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/EndToEndInterfaceTest.java
@@ -0,0 +1,405 @@
+// ***************************************************************************************************************************
+// * 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.rest.client.remote;
+
+import static org.apache.juneau.http.HttpMethodName.*;
+import static org.junit.Assert.*;
+
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.client.*;
+import org.apache.juneau.rest.mock.*;
+import org.apache.juneau.rest.response.*;
+import org.junit.*;
+import org.junit.runners.*;
+
+/**
+ * Tests inheritance of annotations from interfaces.
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@SuppressWarnings("javadoc")
+public class EndToEndInterfaceTest {
+
+	//=================================================================================================================
+	// Simple tests, split annotations.
+	//=================================================================================================================
+
+	@RemoteResource
+	public static interface IA {
+
+		@RemoteMethod(method="PUT", path="/a01")
+		public String a01(@Body String b);
+
+		@RemoteMethod(method="GET", path="/a02")
+		public String a02(@Query("foo") String b);
+
+		@RemoteMethod(method="GET", path="/a03")
+		public String a03(@Header("foo") String b);
+	}
+
+	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
+	public static class A implements IA {
+
+		@Override
+		@RestMethod(name=PUT, path="/a01")
+		public String a01(String b) {
+			return b;
+		}
+
+		@Override
+		@RestMethod(name=GET, path="/a02")
+		public String a02(String b) {
+			return b;
+		}
+
+		@Override
+		@RestMethod(name=GET, path="/a03")
+		public String a03(String b) {
+			return b;
+		}
+	}
+
+	private static MockRest a = MockRest.create(A.class);
+	private static IA ia = RestClient.create().json().mockHttpConnection(a).build().getRemoteResource(IA.class);
+
+	@Test
+	public void a01_splitAnnotations_Body() throws Exception {
+		assertEquals("foo", ia.a01("foo"));
+	}
+	@Test
+	public void a02_splitAnnotations_Query() throws Exception {
+		assertEquals("foo", ia.a02("foo"));
+	}
+	@Test
+	public void a03_splitAnnotations_Header() throws Exception {
+		assertEquals("foo", ia.a03("foo"));
+	}
+
+	//=================================================================================================================
+	// Simple tests, combined annotations.
+	//=================================================================================================================
+
+	@RemoteResource
+	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
+	public static interface IB {
+
+		@RemoteMethod(method="PUT", path="/a01")
+		@RestMethod(name=PUT, path="/a01")
+		public String b01(@Body String b);
+
+		@RemoteMethod(method="GET", path="/a02")
+		@RestMethod(name=GET, path="/a02")
+		public String b02(@Query("foo") String b);
+
+		@RemoteMethod(method="GET", path="/a03")
+		@RestMethod(name=GET, path="/a03")
+		public String b03(@Header("foo") String b);
+	}
+
+	public static class B implements IB {
+
+		@Override
+		public String b01(String b) {
+			return b;
+		}
+
+		@Override
+		public String b02(String b) {
+			return b;
+		}
+
+		@Override
+		public String b03(String b) {
+			return b;
+		}
+	}
+
+	private static MockRest b = MockRest.create(B.class);
+	private static IB ib = RestClient.create().json().mockHttpConnection(b).build().getRemoteResource(IB.class);
+
+	@Test
+	public void b01_combinedAnnotations_Body() throws Exception {
+		assertEquals("foo", ib.b01("foo"));
+	}
+	@Test
+	public void b02_combinedAnnotations_Query() throws Exception {
+		assertEquals("foo", ib.b02("foo"));
+	}
+	@Test
+	public void b03_combinedAnnotations_Header() throws Exception {
+		assertEquals("foo", ib.b03("foo"));
+	}
+
+	//=================================================================================================================
+	// Standard responses
+	//=================================================================================================================
+
+	@RemoteResource
+	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
+	public static interface IC {
+
+		@RemoteMethod
+		@RestMethod
+		public Ok ok();
+
+		@RemoteMethod
+		@RestMethod
+		public Accepted accepted();
+
+		@RemoteMethod
+		@RestMethod
+		public AlreadyReported alreadyReported();
+
+		@RemoteMethod
+		@RestMethod
+		public Continue _continue();
+
+		@RemoteMethod
+		@RestMethod
+		public Created created();
+
+		@RemoteMethod
+		@RestMethod
+		public EarlyHints earlyHints();
+
+		@RemoteMethod
+		@RestMethod
+		public Found found();
+
+		@RemoteMethod
+		@RestMethod
+		public IMUsed iMUsed();
+
+		@RemoteMethod
+		@RestMethod
+		public MovedPermanently movedPermanently();
+
+		@RemoteMethod
+		@RestMethod
+		public MultipleChoices multipleChoices();
+
+		@RemoteMethod
+		@RestMethod
+		public MultiStatus multiStatus();
+
+		@RemoteMethod
+		@RestMethod
+		public NoContent noContent();
+
+		@RemoteMethod
+		@RestMethod
+		public NonAuthoritiveInformation nonAuthoritiveInformation();
+
+		@RemoteMethod
+		@RestMethod
+		public NotModified notModified();
+
+		@RemoteMethod
+		@RestMethod
+		public PartialContent partialContent();
+
+		@RemoteMethod
+		@RestMethod
+		public PermanentRedirect permanentRedirect();
+
+		@RemoteMethod
+		@RestMethod
+		public Processing processing();
+
+		@RemoteMethod
+		@RestMethod
+		public ResetContent resetContent();
+
+		@RemoteMethod
+		@RestMethod
+		public SeeOther seeOther();
+
+		@RemoteMethod
+		@RestMethod
+		public SwitchingProtocols switchingProtocols();
+
+		@RemoteMethod
+		@RestMethod
+		public TemporaryRedirect temporaryRedirect();
+
+		@RemoteMethod
+		@RestMethod
+		public UseProxy useProxy();
+	}
+
+	public static class C implements IC {
+
+		@Override public Ok ok() { return Ok.OK; }
+		@Override public Accepted accepted() { return Accepted.INSTANCE; }
+		@Override public AlreadyReported alreadyReported() { return AlreadyReported.INSTANCE; }
+		@Override public Continue _continue() { return Continue.INSTANCE; }
+		@Override public Created created() { return Created.INSTANCE; }
+		@Override public EarlyHints earlyHints() { return EarlyHints.INSTANCE; }
+		@Override public Found found() { return Found.INSTANCE; }
+		@Override public IMUsed iMUsed() { return IMUsed.INSTANCE; }
+		@Override public MovedPermanently movedPermanently() { return MovedPermanently.INSTANCE; }
+		@Override public MultipleChoices multipleChoices() { return MultipleChoices.INSTANCE; }
+		@Override public MultiStatus multiStatus() { return MultiStatus.INSTANCE; }
+		@Override public NoContent noContent() { return NoContent.INSTANCE; }
+		@Override public NonAuthoritiveInformation nonAuthoritiveInformation() { return NonAuthoritiveInformation.INSTANCE; }
+		@Override public NotModified notModified() { return NotModified.INSTANCE; }
+		@Override public PartialContent partialContent() { return PartialContent.INSTANCE; }
+		@Override public PermanentRedirect permanentRedirect() { return PermanentRedirect.INSTANCE; }
+		@Override public Processing processing() { return Processing.INSTANCE; }
+		@Override public ResetContent resetContent() { return ResetContent.INSTANCE; }
+		@Override public SeeOther seeOther() { return SeeOther.INSTANCE; }
+		@Override public SwitchingProtocols switchingProtocols() { return SwitchingProtocols.INSTANCE; }
+		@Override public TemporaryRedirect temporaryRedirect() { return TemporaryRedirect.INSTANCE; }
+		@Override public UseProxy useProxy() { return UseProxy.INSTANCE; }
+	}
+
+	private static IC ic = RestClient.create().json().disableRedirectHandling().mockHttpConnection(MockRest.create(C.class)).build().getRemoteResource(IC.class);
+
+	@Test
+	public void c01_standardResponses_Ok() throws Exception {
+		assertEquals("OK", ic.ok().toString());
+	}
+	@Test
+	public void c02_standardResponses_Accepted() throws Exception {
+		assertEquals("Accepted", ic.accepted().toString());
+	}
+	@Test
+	public void c03_standardResponses_AlreadyReported() throws Exception {
+		assertEquals("Already Reported", ic.alreadyReported().toString());
+	}
+	@Test
+	public void c04_standardResponses_Continue() throws Exception {
+		// HttpClient goes into loop if status code is less than 200.
+		//assertEquals("Continue", ic._continue().toString());
+	}
+	@Test
+	public void c05_standardResponses_Created() throws Exception {
+		assertEquals("Created", ic.created().toString());
+	}
+	@Test
+	public void c06_standardResponses_EarlyHints() throws Exception {
+		// HttpClient goes into loop if status code is less than 200.
+		//assertEquals("Early Hints", ic.earlyHints().toString());
+	}
+	@Test
+	public void c07_standardResponses_Found() throws Exception {
+		assertEquals("Found", ic.found().toString());
+	}
+	@Test
+	public void c08_standardResponses_IMUsed() throws Exception {
+		assertEquals("IM Used", ic.iMUsed().toString());
+	}
+	@Test
+	public void c09_standardResponses_MovedPermanently() throws Exception {
+		assertEquals("Moved Permanently", ic.movedPermanently().toString());
+	}
+	@Test
+	public void c10_standardResponses_MultipleChoices() throws Exception {
+		assertEquals("Multiple Choices", ic.multipleChoices().toString());
+	}
+	@Test
+	public void c11_standardResponses_MultiStatus() throws Exception {
+		assertEquals("Multi-Status", ic.multiStatus().toString());
+	}
+	@Test
+	public void c12_standardResponses_NoContent() throws Exception {
+		assertEquals("No Content", ic.noContent().toString());
+	}
+	@Test
+	public void c13_standardResponses_NonAuthoritiveInformation() throws Exception {
+		assertEquals("Non-Authoritative Information", ic.nonAuthoritiveInformation().toString());
+	}
+	@Test
+	public void c14_standardResponses_NotModified() throws Exception {
+		assertEquals("Not Modified", ic.notModified().toString());
+	}
+	@Test
+	public void c15_standardResponses_PartialContent() throws Exception {
+		assertEquals("Partial Content", ic.partialContent().toString());
+	}
+	@Test
+	public void c16_standardResponses_PermanentRedirect() throws Exception {
+		assertEquals("Permanent Redirect", ic.permanentRedirect().toString());
+	}
+	@Test
+	public void c17_standardResponses_Processing() throws Exception {
+		// HttpClient goes into loop if status code is less than 200.
+		//assertEquals("Processing", ic.processing().toString());
+	}
+	@Test
+	public void c18_standardResponses_ResetContent() throws Exception {
+		assertEquals("Reset Content", ic.resetContent().toString());
+	}
+	@Test
+	public void c19_standardResponses_SeeOther() throws Exception {
+		assertEquals("See Other", ic.seeOther().toString());
+	}
+	@Test
+	public void c20_standardResponses_SwitchingProtocols() throws Exception {
+		// HttpClient goes into loop if status code is less than 200.
+		//assertEquals("Switching Protocols", ic.switchingProtocols().toString());
+	}
+	@Test
+	public void c21_standardResponses_TemporaryRedirect() throws Exception {
+		assertEquals("Temporary Redirect", ic.temporaryRedirect().toString());
+	}
+	@Test
+	public void c22_standardResponses_UseProxy() throws Exception {
+		assertEquals("Use Proxy", ic.useProxy().toString());
+	}
+
+	//=================================================================================================================
+	// Helper responses
+	//=================================================================================================================
+
+//	@RemoteResource
+//	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
+//	public static interface ID {
+//
+//		@RemoteMethod
+//		@RestMethod
+//		public BeanDescription beanDescription();
+//
+//		@RemoteMethod
+//		@RestMethod
+//		public ChildResourceDescriptions beanDescription();
+
+		//		BeanDescription.java
+//		ChildResourceDescriptions.java
+//		ResourceDescription.java
+//		ResourceDescriptions.java
+//		SeeOtherRoot.java
+		// ReaderResource
+		// StreamResource
+//	}
+//
+//	public static class D implements ID {
+//
+//	}
+//
+//	private static ID id = RestClient.create().json().disableRedirectHandling().mockHttpConnection(MockRest.create(D.class)).build().getRemoteResource(ID.class);
+//
+//	@Test
+//	public void d01_helperResponses_Ok() throws Exception {
+//		assertEquals("OK", ic.ok().toString());
+//	}
+
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// TODO
+	//-----------------------------------------------------------------------------------------------------------------
+	// Object return type.
+	// Thrown objects.
+
+}
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 a482968..90eca8d 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
@@ -518,7 +518,7 @@ public final class RestContext extends BeanContext {
 	 * 	<ul>
 	 * 		<li class='jm'>{@link RestRequest#getClasspathReaderResource(String) getClasspathReaderResource(String)}
 	 * 		<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean) getClasspathReaderResource(String,boolean)}
-	 * 		<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean,MediaType) getClasspathReaderResource(String,boolean,MediaType)}
+	 * 		<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean,MediaType,boolean) getClasspathReaderResource(String,boolean,MediaType,boolean)}
 	 * 	</ul>
 	 * </ul>
 	 *
@@ -1470,7 +1470,7 @@ public final class RestContext extends BeanContext {
 	 * Used for specifying the content type on file resources retrieved through the following methods:
 	 * <ul>
 	 * 	<li class='jm'>{@link RestContext#resolveStaticFile(String) RestContext.resolveStaticFile(String)}
-	 * 	<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean,MediaType)}
+	 * 	<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean,MediaType,boolean)}
 	 * 	<li class='jm'>{@link RestRequest#getClasspathReaderResource(String,boolean)}
 	 * 	<li class='jm'>{@link RestRequest#getClasspathReaderResource(String)}
 	 * </ul>
@@ -3573,7 +3573,7 @@ public final class RestContext extends BeanContext {
 								String name = (i == -1 ? p2 : p2.substring(i+1));
 								String mediaType = mimetypesFileTypeMap.getContentType(name);
 								Map<String,Object> responseHeaders = sfm.responseHeaders != null ? sfm.responseHeaders : staticFileResponseHeaders;
-								sr = new StreamResource(MediaType.forString(mediaType), responseHeaders, is);
+								sr = new StreamResource(MediaType.forString(mediaType), responseHeaders, true, is);
 								break;
 							}
 						}
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 c496a84..e156c6b 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
@@ -141,7 +141,7 @@ public class RestJavaMethod implements Comparable<RestJavaMethod>  {
 
 			try {
 
-				RestMethod m = method.getAnnotation(RestMethod.class);
+				RestMethod m = getAnnotation(RestMethod.class, method);
 				if (m == null)
 					throw new RestServletException("@RestMethod annotation not found on method ''{0}''", sig);
 
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 da5b25a..5713fe8 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
@@ -1338,21 +1338,24 @@ public final class RestRequest extends HttpServletRequestWrapper {
 	 * 	resolved by the variable resolver returned by {@link #getVarResolverSession()}.
 	 * 	<br>See {@link RestContext#getVarResolver()} for the list of supported variables.
 	 * @param mediaType The value to set as the <js>"Content-Type"</js> header for this object.
+	 * @param cached If <jk>true</jk>, the resource will be read into a byte array for fast serialization.
 	 * @return A new reader resource, or <jk>null</jk> if resource could not be found.
 	 * @throws IOException
 	 */
-	public ReaderResource getClasspathReaderResource(String name, boolean resolveVars, MediaType mediaType) throws IOException {
+	public ReaderResource getClasspathReaderResource(String name, boolean resolveVars, MediaType mediaType, boolean cached) throws IOException {
 		String s = context.getClasspathResourceAsString(name, getLocale());
 		if (s == null)
 			return null;
-		ReaderResourceBuilder b = new ReaderResourceBuilder().mediaType(mediaType).contents(s);
+		ReaderResource.Builder b = ReaderResource.create().mediaType(mediaType).contents(s);
 		if (resolveVars)
 			b.varResolver(getVarResolverSession());
+		if (cached)
+			b.cached();
 		return b.build();
 	}
 
 	/**
-	 * Same as {@link #getClasspathReaderResource(String, boolean, MediaType)} except uses the resource mime-type map
+	 * Same as {@link #getClasspathReaderResource(String, boolean, MediaType, boolean)} except uses the resource mime-type map
 	 * constructed using {@link RestContextBuilder#mimeTypes(String...)} to determine the media type.
 	 *
 	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
@@ -1364,7 +1367,7 @@ public final class RestRequest extends HttpServletRequestWrapper {
 	 * @throws IOException
 	 */
 	public ReaderResource getClasspathReaderResource(String name, boolean resolveVars) throws IOException {
-		return getClasspathReaderResource(name, resolveVars, MediaType.forString(context.getMediaTypeForName(name)));
+		return getClasspathReaderResource(name, resolveVars, MediaType.forString(context.getMediaTypeForName(name)), false);
 	}
 
 	/**
@@ -1375,7 +1378,46 @@ public final class RestRequest extends HttpServletRequestWrapper {
 	 * @throws IOException
 	 */
 	public ReaderResource getClasspathReaderResource(String name) throws IOException {
-		return getClasspathReaderResource(name, false, MediaType.forString(context.getMediaTypeForName(name)));
+		return getClasspathReaderResource(name, false, MediaType.forString(context.getMediaTypeForName(name)), false);
+	}
+
+	/**
+	 * Returns an instance of a {@link StreamResource} that represents the contents of a resource binary file from the
+	 * classpath.
+	 *
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='jf'>{@link org.apache.juneau.rest.RestContext#REST_classpathResourceFinder}
+	 * 	<li class='jm'>{@link org.apache.juneau.rest.RestRequest#getClasspathStreamResource(String)}
+	 * </ul>
+	 *
+	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
+	 * @param mediaType The value to set as the <js>"Content-Type"</js> header for this object.
+	 * @param cached If <jk>true</jk>, the resource will be read into a byte array for fast serialization.
+	 * @return A new stream resource, or <jk>null</jk> if resource could not be found.
+	 * @throws IOException
+	 */
+	@SuppressWarnings("resource")
+	public StreamResource getClasspathStreamResource(String name, MediaType mediaType, boolean cached) throws IOException {
+		InputStream is = context.getClasspathResource(name, getLocale());
+		if (is == null)
+			return null;
+		StreamResource.Builder b = StreamResource.create().mediaType(mediaType).contents(is);
+		if (cached)
+			b.cached();
+		return b.build();
+	}
+
+	/**
+	 * Same as {@link #getClasspathStreamResource(String, MediaType, boolean)} except uses the resource mime-type map
+	 * constructed using {@link RestContextBuilder#mimeTypes(String...)} to determine the media type.
+	 *
+	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
+	 * @return A new stream resource, or <jk>null</jk> if resource could not be found.
+	 * @throws IOException
+	 */
+	public StreamResource getClasspathStreamResource(String name) throws IOException {
+		return getClasspathStreamResource(name, MediaType.forString(context.getMediaTypeForName(name)), false);
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResource.java
index 07c7d2a..a157950 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResource.java
@@ -35,7 +35,7 @@ import org.apache.juneau.svl.*;
  * <br>The contents of the request passed into the constructor are immediately converted to read-only strings.
  *
  * <p>
- * Instances of this class can be built using {@link ReaderResourceBuilder}.
+ * Instances of this class can be built using {@link Builder}.
  *
  * <h5 class='section'>See Also:</h5>
  * <ul>
@@ -46,17 +46,12 @@ import org.apache.juneau.svl.*;
 public class ReaderResource implements Writable {
 
 	private final MediaType mediaType;
-	private final String[] contents;
+	private final Object[] contents;
 	private final VarResolverSession varSession;
 	private final Map<String,Object> headers;
 
-	/**
-	 * Creates a new instance of a {@link ReaderResourceBuilder}
-	 *
-	 * @return A new instance of a {@link ReaderResourceBuilder}
-	 */
-	public static ReaderResourceBuilder create() {
-		return new ReaderResourceBuilder();
+	ReaderResource(Builder b) throws IOException {
+		this(b.mediaType, b.headers, b.varResolver, b.cached, b.contents.toArray());
 	}
 
 	/**
@@ -65,6 +60,9 @@ public class ReaderResource implements Writable {
 	 * @param mediaType The resource media type.
 	 * @param headers The HTTP response headers for this streamed resource.
 	 * @param varSession Optional variable resolver for resolving variables in the string.
+	 * @param cached
+	 * 	Identifies if this resource is cached in memory.
+	 * 	<br>If <jk>true</jk>, the contents will be loaded into a String for fast retrieval.
 	 * @param contents
 	 * 	The resource contents.
 	 * 	<br>If multiple contents are specified, the results will be concatenated.
@@ -77,30 +75,153 @@ public class ReaderResource implements Writable {
 	 * 	</ul>
 	 * @throws IOException
 	 */
-	public ReaderResource(MediaType mediaType, Map<String,Object> headers, VarResolverSession varSession, Object...contents) throws IOException {
+	public ReaderResource(MediaType mediaType, Map<String,Object> headers, VarResolverSession varSession, boolean cached, Object...contents) throws IOException {
 		this.mediaType = mediaType;
 		this.varSession = varSession;
-
 		this.headers = immutableMap(headers);
+		this.contents = cached ? new Object[]{read(contents)} : contents;
+	}
 
-		this.contents = new String[contents.length];
-		for (int i = 0; i < contents.length; i++) {
-			Object c = contents[i];
-			if (c == null)
-				this.contents[i] = "";
-			else if (c instanceof InputStream)
-				this.contents[i] = read((InputStream)c);
-			else if (c instanceof File)
-				this.contents[i] = read((File)c);
-			else if (c instanceof Reader)
-				this.contents[i] = read((Reader)c);
-			else if (c instanceof CharSequence)
-				this.contents[i] = ((CharSequence)c).toString();
-			else
-				throw new IOException("Invalid class type passed to ReaderResource: " + c.getClass().getName());
+	//-----------------------------------------------------------------------------------------------------------------
+	// Builder
+	//-----------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Creates a new instance of a {@link Builder} for this class.
+	 *
+	 * @return A new instance of a {@link Builder}.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	/**
+	 * Builder class for constructing {@link ReaderResource} objects.
+	 *
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='link'>{@doc juneau-rest-server.RestMethod.ReaderResource}
+	 * </ul>
+	 */
+	public static class Builder {
+
+		ArrayList<Object> contents = new ArrayList<>();
+		MediaType mediaType;
+		VarResolverSession varResolver;
+		Map<String,Object> headers = new LinkedHashMap<>();
+		boolean cached;
+
+		/**
+		 * Specifies the resource media type string.
+		 *
+		 * @param mediaType The resource media type string.
+		 * @return This object (for method chaining).
+		 */
+		public Builder mediaType(String mediaType) {
+			this.mediaType = MediaType.forString(mediaType);
+			return this;
+		}
+
+		/**
+		 * Specifies the resource media type string.
+		 *
+		 * @param mediaType The resource media type string.
+		 * @return This object (for method chaining).
+		 */
+		public Builder mediaType(MediaType mediaType) {
+			this.mediaType = mediaType;
+			return this;
+		}
+
+		/**
+		 * Specifies the contents for this resource.
+		 *
+		 * <p>
+		 * This method can be called multiple times to add more content.
+		 *
+		 * @param contents
+		 * 	The resource contents.
+		 * 	<br>If multiple contents are specified, the results will be concatenated.
+		 * 	<br>Contents can be any of the following:
+		 * 	<ul>
+		 * 		<li><code>InputStream</code>
+		 * 		<li><code>Reader</code> - Converted to UTF-8 bytes.
+		 * 		<li><code>File</code>
+		 * 		<li><code>CharSequence</code> - Converted to UTF-8 bytes.
+		 * 	</ul>
+		 * @return This object (for method chaining).
+		 */
+		public Builder contents(Object...contents) {
+			this.contents.addAll(Arrays.asList(contents));
+			return this;
+		}
+
+		/**
+		 * Specifies an HTTP response header value.
+		 *
+		 * @param name The HTTP header name.
+		 * @param value
+		 * 	The HTTP header value.
+		 * 	<br>Will be converted to a <code>String</code> using {@link Object#toString()}.
+		 * @return This object (for method chaining).
+		 */
+		public Builder header(String name, Object value) {
+			this.headers.put(name, value);
+			return this;
+		}
+
+		/**
+		 * Specifies HTTP response header values.
+		 *
+		 * @param headers
+		 * 	The HTTP headers.
+		 * 	<br>Values will be converted to <code>Strings</code> using {@link Object#toString()}.
+		 * @return This object (for method chaining).
+		 */
+		public Builder headers(Map<String,Object> headers) {
+			this.headers.putAll(headers);
+			return this;
+		}
+
+		/**
+		 * Specifies the variable resolver to use for this resource.
+		 *
+		 * @param varResolver The variable resolver.
+		 * @return This object (for method chaining).
+		 */
+		public Builder varResolver(VarResolverSession varResolver) {
+			this.varResolver = varResolver;
+			return this;
+		}
+
+		/**
+		 * Specifies that this resource is intended to be cached.
+		 *
+		 * <p>
+		 * This will trigger the contents to be loaded into a String for fast serializing.
+		 *
+		 * @return This object (for method chaining).
+		 */
+		public Builder cached() {
+			this.cached = true;
+			return this;
+		}
+
+		/**
+		 * Create a new {@link ReaderResource} using values in this builder.
+		 *
+		 * @return A new immutable {@link ReaderResource} object.
+		 * @throws IOException
+		 */
+		public ReaderResource build() throws IOException {
+			return new ReaderResource(this);
 		}
 	}
 
+	//-----------------------------------------------------------------------------------------------------------------
+	// Properties
+	//-----------------------------------------------------------------------------------------------------------------
+
 	/**
 	 * Get the HTTP response headers.
 	 *
@@ -117,11 +238,13 @@ public class ReaderResource implements Writable {
 	@ResponseBody
 	@Override /* Writeable */
 	public Writer writeTo(Writer w) throws IOException {
-		for (String s : contents) {
-			if (varSession != null)
-				varSession.resolveTo(s, w);
-			else
-				w.write(s);
+		for (Object o : contents) {
+			if (o != null) {
+				if (varSession == null)
+					pipe(o, w);
+				else
+					varSession.resolveTo(read(o), w);
+			}
 		}
 		return w;
 	}
@@ -134,15 +257,13 @@ public class ReaderResource implements Writable {
 
 	@Override /* Object */
 	public String toString() {
-		if (contents.length == 1 && varSession == null)
-			return contents[0];
-		StringWriter sw = new StringWriter();
-		for (String s : contents) {
-			if (varSession != null)
-				return varSession.resolve(s);
-			sw.write(s);
+		try {
+			if (contents.length == 1 && varSession == null)
+				return read(contents[0]);
+			return writeTo(new StringWriter()).toString();
+		} catch (IOException e) {
+			throw new RuntimeException(e);
 		}
-		return sw.toString();
 	}
 
 	/**
@@ -162,4 +283,16 @@ public class ReaderResource implements Writable {
 			s = s.replaceAll("(?s)\\/\\*(.*?)\\*\\/\\s*", "");
 		return s;
 	}
+
+	/**
+	 * Returns the contents of this resource.
+	 *
+	 * @return The contents of this resource.
+	 */
+	public Reader getContents() {
+		if (contents.length == 1 && varSession == null && contents[0] instanceof Reader) {
+			return (Reader)contents[0];
+		}
+		return new StringReader(toString());
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResourceBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResourceBuilder.java
deleted file mode 100644
index 3004a6d..0000000
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/ReaderResourceBuilder.java
+++ /dev/null
@@ -1,127 +0,0 @@
-// ***************************************************************************************************************************
-// * 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.rest.helper;
-
-import java.io.*;
-import java.util.*;
-
-import org.apache.juneau.http.*;
-import org.apache.juneau.svl.*;
-
-/**
- * Builder class for constructing {@link ReaderResource} objects.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.RestMethod.ReaderResource}
- * </ul>
- */
-public final class ReaderResourceBuilder {
-	ArrayList<Object> contents = new ArrayList<>();
-	MediaType mediaType;
-	VarResolverSession varResolver;
-	Map<String,Object> headers = new LinkedHashMap<>();
-
-	/**
-	 * Specifies the resource media type string.
-	 *
-	 * @param mediaType The resource media type string.
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder mediaType(String mediaType) {
-		this.mediaType = MediaType.forString(mediaType);
-		return this;
-	}
-
-	/**
-	 * Specifies the resource media type string.
-	 *
-	 * @param mediaType The resource media type string.
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder mediaType(MediaType mediaType) {
-		this.mediaType = mediaType;
-		return this;
-	}
-
-	/**
-	 * Specifies the contents for this resource.
-	 *
-	 * <p>
-	 * This method can be called multiple times to add more content.
-	 *
-	 * @param contents
-	 * 	The resource contents.
-	 * 	<br>If multiple contents are specified, the results will be concatenated.
-	 * 	<br>Contents can be any of the following:
-	 * 	<ul>
-	 * 		<li><code>InputStream</code>
-	 * 		<li><code>Reader</code> - Converted to UTF-8 bytes.
-	 * 		<li><code>File</code>
-	 * 		<li><code>CharSequence</code> - Converted to UTF-8 bytes.
-	 * 	</ul>
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder contents(Object...contents) {
-		this.contents.addAll(Arrays.asList(contents));
-		return this;
-	}
-
-	/**
-	 * Specifies an HTTP response header value.
-	 *
-	 * @param name The HTTP header name.
-	 * @param value
-	 * 	The HTTP header value.
-	 * 	<br>Will be converted to a <code>String</code> using {@link Object#toString()}.
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder header(String name, Object value) {
-		this.headers.put(name, value);
-		return this;
-	}
-
-	/**
-	 * Specifies HTTP response header values.
-	 *
-	 * @param headers
-	 * 	The HTTP headers.
-	 * 	<br>Values will be converted to <code>Strings</code> using {@link Object#toString()}.
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder headers(Map<String,Object> headers) {
-		this.headers.putAll(headers);
-		return this;
-	}
-
-	/**
-	 * Specifies the variable resolver to use for this resource.
-	 *
-	 * @param varResolver The variable resolver.
-	 * @return This object (for method chaining).
-	 */
-	public ReaderResourceBuilder varResolver(VarResolverSession varResolver) {
-		this.varResolver = varResolver;
-		return this;
-	}
-
-	/**
-	 * Create a new {@link ReaderResource} using values in this builder.
-	 *
-	 * @return A new immutable {@link ReaderResource} object.
-	 * @throws IOException
-	 */
-	public ReaderResource build() throws IOException {
-		return new ReaderResource(mediaType, headers, varResolver, contents.toArray());
-	}
-}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResource.java
index 8dc6097..feecd8e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResource.java
@@ -16,6 +16,7 @@ import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 
 import java.io.*;
+import java.nio.*;
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -33,7 +34,7 @@ import org.apache.juneau.http.annotation.*;
  * <br>The contents of the request passed into the constructor are immediately converted to read-only byte arrays.
  *
  * <p>
- * Instances of this class can be built using {@link StreamResourceBuilder}.
+ * Instances of this class can be built using {@link Builder}.
  *
  * <h5 class='section'>See Also:</h5>
  * <ul>
@@ -44,16 +45,11 @@ import org.apache.juneau.http.annotation.*;
 public class StreamResource implements Streamable {
 
 	private final MediaType mediaType;
-	private final byte[][] contents;
+	private final Object[] contents;
 	private final Map<String,Object> headers;
 
-	/**
-	 * Creates a new instance of a {@link StreamResourceBuilder}
-	 *
-	 * @return A new instance of a {@link StreamResourceBuilder}
-	 */
-	public static StreamResourceBuilder create() {
-		return new StreamResourceBuilder();
+	StreamResource(Builder b) throws IOException {
+		this(b.mediaType, b.headers, b.cached, b.contents.toArray());
 	}
 
 	/**
@@ -61,6 +57,9 @@ public class StreamResource implements Streamable {
 	 *
 	 * @param mediaType The resource media type.
 	 * @param headers The HTTP response headers for this streamed resource.
+	 * @param cached
+	 * 	Identifies if this stream resource is cached in memory.
+	 * 	<br>If <jk>true</jk>, the contents will be loaded into a byte array for fast retrieval.
 	 * @param contents
 	 * 	The resource contents.
 	 * 	<br>If multiple contents are specified, the results will be concatenated.
@@ -74,28 +73,133 @@ public class StreamResource implements Streamable {
 	 * 	</ul>
 	 * @throws IOException
 	 */
-	public StreamResource(MediaType mediaType, Map<String,Object> headers, Object...contents) throws IOException {
+	public StreamResource(MediaType mediaType, Map<String,Object> headers, boolean cached, Object...contents) throws IOException {
 		this.mediaType = mediaType;
-
 		this.headers = immutableMap(headers);
+		this.contents = cached ? new Object[]{readBytes(contents)} : contents;
+	}
 
-		this.contents = new byte[contents.length][];
-		for (int i = 0; i < contents.length; i++) {
-			Object c = contents[i];
-			if (c == null)
-				this.contents[i] = new byte[0];
-			else if (c instanceof byte[])
-				this.contents[i] = (byte[])c;
-			else if (c instanceof InputStream)
-				this.contents[i] = readBytes((InputStream)c, 1024);
-			else if (c instanceof File)
-				this.contents[i] = readBytes((File)c);
-			else if (c instanceof Reader)
-				this.contents[i] = read((Reader)c).getBytes(UTF8);
-			else if (c instanceof CharSequence)
-				this.contents[i] = ((CharSequence)c).toString().getBytes(UTF8);
-			else
-				throw new IOException("Invalid class type passed to StreamResource: " + c.getClass().getName());
+	//-----------------------------------------------------------------------------------------------------------------
+	// Builder
+	//-----------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Creates a new instance of a {@link Builder} for this class.
+	 *
+	 * @return A new instance of a {@link Builder}.
+	 */
+	public static Builder create() {
+		return new Builder();
+	}
+
+	/**
+	 * Builder class for constructing {@link StreamResource} objects.
+	 *
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
+	 * </ul>
+	 */
+	public static class Builder {
+		ArrayList<Object> contents = new ArrayList<>();
+		MediaType mediaType;
+		Map<String,Object> headers = new LinkedHashMap<>();
+		boolean cached;
+
+		/**
+		 * Specifies the resource media type string.
+		 *
+		 * @param mediaType The resource media type string.
+		 * @return This object (for method chaining).
+		 */
+		public Builder mediaType(String mediaType) {
+			this.mediaType = MediaType.forString(mediaType);
+			return this;
+		}
+
+		/**
+		 * Specifies the resource media type string.
+		 *
+		 * @param mediaType The resource media type string.
+		 * @return This object (for method chaining).
+		 */
+		public Builder mediaType(MediaType mediaType) {
+			this.mediaType = mediaType;
+			return this;
+		}
+
+		/**
+		 * Specifies the contents for this resource.
+		 *
+		 * <p>
+		 * This method can be called multiple times to add more content.
+		 *
+		 * @param contents
+		 * 	The resource contents.
+		 * 	<br>If multiple contents are specified, the results will be concatenated.
+		 * 	<br>Contents can be any of the following:
+		 * 	<ul>
+		 * 		<li><code><jk>byte</jk>[]</code>
+		 * 		<li><code>InputStream</code>
+		 * 		<li><code>Reader</code> - Converted to UTF-8 bytes.
+		 * 		<li><code>File</code>
+		 * 		<li><code>CharSequence</code> - Converted to UTF-8 bytes.
+		 * 	</ul>
+		 * @return This object (for method chaining).
+		 */
+		public Builder contents(Object...contents) {
+			this.contents.addAll(Arrays.asList(contents));
+			return this;
+		}
+
+		/**
+		 * Specifies an HTTP response header value.
+		 *
+		 * @param name The HTTP header name.
+		 * @param value
+		 * 	The HTTP header value.
+		 * 	<br>Will be converted to a <code>String</code> using {@link Object#toString()}.
+		 * @return This object (for method chaining).
+		 */
+		public Builder header(String name, Object value) {
+			this.headers.put(name, value);
+			return this;
+		}
+
+		/**
+		 * Specifies HTTP response header values.
+		 *
+		 * @param headers
+		 * 	The HTTP headers.
+		 * 	<br>Values will be converted to <code>Strings</code> using {@link Object#toString()}.
+		 * @return This object (for method chaining).
+		 */
+		public Builder headers(Map<String,Object> headers) {
+			this.headers.putAll(headers);
+			return this;
+		}
+
+		/**
+		 * Specifies that this resource is intended to be cached.
+		 *
+		 * <p>
+		 * This will trigger the contents to be loaded into a byte array for fast serializing.
+		 *
+		 * @return This object (for method chaining).
+		 */
+		public Builder cached() {
+			this.cached = true;
+			return this;
+		}
+
+		/**
+		 * Create a new {@link StreamResource} using values in this builder.
+		 *
+		 * @return A new immutable {@link StreamResource} object.
+		 * @throws IOException
+		 */
+		public StreamResource build() throws IOException {
+			return new StreamResource(this);
 		}
 	}
 
@@ -115,8 +219,8 @@ public class StreamResource implements Streamable {
 	@ResponseBody
 	@Override /* Streamable */
 	public void streamTo(OutputStream os) throws IOException {
-		for (byte[] b : contents)
-			os.write(b);
+		for (Object c : contents)
+			pipe(c, os);
 		os.flush();
 	}
 
@@ -125,4 +229,48 @@ public class StreamResource implements Streamable {
 	public MediaType getMediaType() {
 		return mediaType;
 	}
+
+	/**
+	 * Returns the contents of this stream resource.
+	 *
+	 * @return The contents of this stream resource.
+	 * @throws IOException
+	 */
+	public InputStream getContents() throws IOException {
+		if (contents.length == 1) {
+			Object c = contents[0];
+			if (c != null) {
+				if (c instanceof byte[])
+					return new ByteArrayInputStream((byte[])c);
+				else if (c instanceof InputStream)
+					return (InputStream)c;
+				else if (c instanceof File)
+					return new FileInputStream((File)c);
+				else if (c instanceof CharSequence)
+					return new ByteArrayInputStream((((CharSequence)c).toString().getBytes(UTF8)));
+			}
+		}
+		byte[][] bc = new byte[contents.length][];
+		int c = 0;
+		for (int i = 0; i < contents.length; i++) {
+			Object o = contents[i];
+			if (o == null)
+				bc[i] = new byte[0];
+			else if (o instanceof byte[])
+				bc[i] = (byte[])o;
+			else if (o instanceof InputStream)
+				bc[i] = readBytes((InputStream)o, 1024);
+			else if (o instanceof Reader)
+				bc[i] = read((Reader)o).getBytes(UTF8);
+			else if (o instanceof File)
+				bc[i] = readBytes((File)o);
+			else if (o instanceof CharSequence)
+				bc[i] = ((CharSequence)o).toString().getBytes(UTF8);
+			c += bc[i].length;
+		}
+		ByteBuffer bb = ByteBuffer.allocate(c);
+		for (byte[] b : bc)
+			bb.put(b);
+		return new ByteArrayInputStream(bb.array());
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResourceBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResourceBuilder.java
deleted file mode 100644
index b9fdcc2..0000000
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/helper/StreamResourceBuilder.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// ***************************************************************************************************************************
-// * 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.rest.helper;
-
-import java.io.*;
-import java.util.*;
-
-import org.apache.juneau.http.*;
-
-/**
- * Builder class for constructing {@link StreamResource} objects.
- *
- * <h5 class='section'>See Also:</h5>
- * <ul>
- * 	<li class='link'>{@doc juneau-rest-server.RestMethod.StreamResource}
- * </ul>
- */
-public final class StreamResourceBuilder {
-	ArrayList<Object> contents = new ArrayList<>();
-	MediaType mediaType;
-	Map<String,Object> headers = new LinkedHashMap<>();
-
-	/**
-	 * Specifies the resource media type string.
-	 *
-	 * @param mediaType The resource media type string.
-	 * @return This object (for method chaining).
-	 */
-	public StreamResourceBuilder mediaType(String mediaType) {
-		this.mediaType = MediaType.forString(mediaType);
-		return this;
-	}
-
-	/**
-	 * Specifies the resource media type string.
-	 *
-	 * @param mediaType The resource media type string.
-	 * @return This object (for method chaining).
-	 */
-	public StreamResourceBuilder mediaType(MediaType mediaType) {
-		this.mediaType = mediaType;
-		return this;
-	}
-
-	/**
-	 * Specifies the contents for this resource.
-	 *
-	 * <p>
-	 * This method can be called multiple times to add more content.
-	 *
-	 * @param contents
-	 * 	The resource contents.
-	 * 	<br>If multiple contents are specified, the results will be concatenated.
-	 * 	<br>Contents can be any of the following:
-	 * 	<ul>
-	 * 		<li><code><jk>byte</jk>[]</code>
-	 * 		<li><code>InputStream</code>
-	 * 		<li><code>Reader</code> - Converted to UTF-8 bytes.
-	 * 		<li><code>File</code>
-	 * 		<li><code>CharSequence</code> - Converted to UTF-8 bytes.
-	 * 	</ul>
-	 * @return This object (for method chaining).
-	 */
-	public StreamResourceBuilder contents(Object...contents) {
-		this.contents.addAll(Arrays.asList(contents));
-		return this;
-	}
-
-	/**
-	 * Specifies an HTTP response header value.
-	 *
-	 * @param name The HTTP header name.
-	 * @param value
-	 * 	The HTTP header value.
-	 * 	<br>Will be converted to a <code>String</code> using {@link Object#toString()}.
-	 * @return This object (for method chaining).
-	 */
-	public StreamResourceBuilder header(String name, Object value) {
-		this.headers.put(name, value);
-		return this;
-	}
-
-	/**
-	 * Specifies HTTP response header values.
-	 *
-	 * @param headers
-	 * 	The HTTP headers.
-	 * 	<br>Values will be converted to <code>Strings</code> using {@link Object#toString()}.
-	 * @return This object (for method chaining).
-	 */
-	public StreamResourceBuilder headers(Map<String,Object> headers) {
-		this.headers.putAll(headers);
-		return this;
-	}
-
-	/**
-	 * Create a new {@link StreamResource} using values in this builder.
-	 *
-	 * @return A new immutable {@link StreamResource} object.
-	 * @throws IOException
-	 */
-	public StreamResource build() throws IOException {
-		return new StreamResource(mediaType, headers, contents.toArray());
-	}
-}
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/Found.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/Found.java
index 7631ea0..d2ce146 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/Found.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/Found.java
@@ -52,6 +52,18 @@ public class Found extends HttpResponse {
 	}
 
 	/**
+	 * Constructor with no redirect.
+	 * <p>
+	 * Used for end-to-end interfaces.
+	 *
+	 * @param message Message to send as the response.
+	 */
+	public Found(String message) {
+		super(message);
+		this.location = null;
+	}
+
+	/**
 	 * Constructor using custom message.
 	 * @param message Message to send as the response.
 	 * @param location <code>Location</code> header value.
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 c14889a..968e00a 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
@@ -48,6 +48,18 @@ public class MovedPermanently extends HttpResponse {
 	}
 
 	/**
+	 * Constructor with no redirect.
+	 * <p>
+	 * Used for end-to-end interfaces.
+	 *
+	 * @param message Message to send as the response.
+	 */
+	public MovedPermanently(String message) {
+		super(message);
+		this.location = null;
+	}
+
+	/**
 	 * Constructor using custom message.
 	 * @param message Message to send as the response.
 	 * @param location <code>Location</code> header value.
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 5eec0a3..cfc5ff4 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
@@ -49,6 +49,18 @@ public class PermanentRedirect extends HttpResponse {
 	}
 
 	/**
+	 * Constructor with no redirect.
+	 * <p>
+	 * Used for end-to-end interfaces.
+	 *
+	 * @param message Message to send as the response.
+	 */
+	public PermanentRedirect(String message) {
+		super(message);
+		this.location = null;
+	}
+
+	/**
 	 * Constructor using custom message.
 	 * @param message Message to send as the response.
 	 * @param location <code>Location</code> header value.
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 f01ab5c..7cdc508 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
@@ -51,6 +51,18 @@ public class SeeOther extends HttpResponse {
 	}
 
 	/**
+	 * Constructor with no redirect.
+	 * <p>
+	 * Used for end-to-end interfaces.
+	 *
+	 * @param message Message to send as the response.
+	 */
+	public SeeOther(String message) {
+		super(message);
+		this.location = null;
+	}
+
+	/**
 	 * Constructor using custom message.
 	 *
 	 * @param message Message to send as the response.
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 0ef69e7..221bf77 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
@@ -50,6 +50,18 @@ public class TemporaryRedirect extends HttpResponse {
 	}
 
 	/**
+	 * Constructor with no redirect.
+	 * <p>
+	 * Used for end-to-end interfaces.
+	 *
+	 * @param message Message to send as the response.
+	 */
+	public TemporaryRedirect(String message) {
+		super(message);
+		this.location = null;
+	}
+
+	/**
 	 * Constructor using custom message.
 	 * @param message Message to send as the response.
 	 * @param location <code>Location</code> header value.
diff --git a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/AnnotationInheritanceTest.java b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/AnnotationInheritanceTest.java
index 3f53930..706f8ef 100644
--- a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/AnnotationInheritanceTest.java
+++ b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/AnnotationInheritanceTest.java
@@ -12,6 +12,11 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.annotation;
 
+import static org.apache.juneau.http.HttpMethodName.*;
+
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.mock.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -19,30 +24,55 @@ import org.junit.runners.*;
  * Tests inheritance of annotations from interfaces.
  */
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@SuppressWarnings("javadoc")
 public class AnnotationInheritanceTest {
 
 	//=================================================================================================================
 	// @Body on parameter
 	//=================================================================================================================
 
-//	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
-//	public static interface IA {
-//		@RestMethod(name=PUT, path="/String")
-//		public String a01(@Body String b);
-//	}
-//
-//	public static class A implements IA {
-//
-//		@Override
-//		public String a01(String b) {
-//			return b;
-//		}
-//	}
-//
-//	private static MockRest a = MockRest.create(A.class);
-//
-//	@Test
-//	public void a01a_onParameter_String() throws Exception {
-//	//	a.put("/String", "'foo'").json().execute().assertBody("'foo'");
-//	}
+	@RestResource(serializers=SimpleJsonSerializer.class, parsers=JsonParser.class, defaultAccept="text/json")
+	public static interface IA {
+		@RestMethod(name=PUT, path="/a01")
+		public String a01(@Body String b);
+
+		@RestMethod(name=GET, path="/a02")
+		public String a02(@Query("foo") String b);
+
+		@RestMethod(name=GET, path="/a03")
+		public String a03(@Header("foo") String b);
+	}
+
+	public static class A implements IA {
+
+		@Override
+		public String a01(String b) {
+			return b;
+		}
+
+		@Override
+		public String a02(String b) {
+			return b;
+		}
+
+		@Override
+		public String a03(String b) {
+			return b;
+		}
+	}
+
+	private static MockRest a = MockRest.create(A.class);
+
+	@Test
+	public void a01_inherited_Body() throws Exception {
+		a.put("/a01", "'foo'").json().execute().assertBody("'foo'");
+	}
+	@Test
+	public void a02_inherited_Query() throws Exception {
+		a.get("/a02").query("foo", "bar").json().execute().assertBody("'bar'");
+	}
+	@Test
+	public void a03_inherited_Header() throws Exception {
+		a.get("/a03").header("foo", "bar").json().execute().assertBody("'bar'");
+	}
 }