juneau-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jamesbog...@apache.org
Subject [33/48] incubator-juneau git commit: Rename server and client projects.
Date Sat, 21 Jan 2017 23:30:59 GMT
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/91a388d0/juneau-rest/src/main/java/org/apache/juneau/rest/RestRequest.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestRequest.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestRequest.java
new file mode 100644
index 0000000..8be3753
--- /dev/null
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -0,0 +1,1870 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import static java.util.Collections.*;
+import static java.util.logging.Level.*;
+import static javax.servlet.http.HttpServletResponse.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.net.*;
+import java.text.*;
+import java.util.*;
+import java.util.logging.*;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.dto.swagger.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.encoders.Encoder;
+import org.apache.juneau.ini.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.parser.ParseException;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.utils.*;
+
+/**
+ * Represents an HTTP request for a REST resource.
+ * <p>
+ * 	Equivalent to {@link HttpServletRequest} except with some additional convenience methods.
+ * </p>
+ * <p>
+ * 	For reference, given the URL <js>"http://localhost:9080/contextRoot/servletPath/foo?bar=baz#qux"</js>, the
+ * 	following methods return the following values....
+ * </p>
+ * <table class='styled'>
+ * 	<tr><th>Method</th><th>Value</th></tr>
+ * 	<tr><td>{@code getContextPath()}</td><td>{@code /contextRoot}</td></tr>
+ * 	<tr><td>{@code getPathInfo()}</td><td>{@code /foo}</td></tr>
+ * 	<tr><td>{@code getPathTranslated()}</td><td>{@code path-to-deployed-war-on-filesystem/foo}</td></tr>
+ * 	<tr><td>{@code getQueryString()}</td><td>{@code bar=baz}</td></tr>
+ * 	<tr><td>{@code getRequestURI()}</td><td>{@code /contextRoot/servletPath/foo}</td></tr>
+ * 	<tr><td>{@code getRequestURL()}</td><td>{@code http://localhost:9080/contextRoot/servletPath/foo}</td></tr>
+ * 	<tr><td>{@code getServletPath()}</td><td>{@code /servletPath}</td></tr>
+ * </table>
+ * <p>
+ * 	Refer to <a class='doclink' href='package-summary.html#TOC'>REST Servlet API</a> for information about using this class.
+ * </p>
+ */
+@SuppressWarnings("unchecked")
+public final class RestRequest extends HttpServletRequestWrapper {
+
+	private final RestServlet servlet;
+	private String method, pathRemainder, body;
+	Method javaMethod;
+	private ObjectMap properties;
+	private SerializerGroup serializerGroup;
+	private ParserGroup parserGroup;
+	private Encoder encoder;
+	private int contentLength;
+	private final boolean debug;
+	private UrlEncodingParser urlEncodingParser;   // The parser used to parse URL attributes and parameters (beanContext also used to parse headers)
+	private BeanSession beanSession;
+	private VarResolverSession varSession;
+	private Map<String,String[]> queryParams;
+	private Map<String,String> defaultServletHeaders, defaultMethodHeaders, overriddenHeaders, overriddenQueryParams, overriddenFormDataParams, pathParameters;
+	private boolean isPost;
+	private String servletURI, relativeServletURI;
+	private String charset, defaultCharset;
+	private ObjectMap headers;
+	private ConfigFile cf;
+	private Swagger swagger, fileSwagger;
+
+	/**
+	 * Constructor.
+	 */
+	RestRequest(RestServlet servlet, HttpServletRequest req) throws ServletException {
+		super(req);
+
+		try {
+			this.servlet = servlet;
+			isPost = req.getMethod().equalsIgnoreCase("POST");
+
+			// If this is a POST, we want to parse the query parameters ourselves to prevent
+			// the servlet code from processing the HTTP body as URL-Encoded parameters.
+			if (isPost)
+				queryParams = servlet.getUrlEncodingParser().parseIntoSimpleMap(getQueryString());
+			else {
+				queryParams = req.getParameterMap();
+			}
+
+			// Get the HTTP method.
+			// Can be overridden through a "method" GET attribute.
+			method = super.getMethod();
+
+			String m = getQueryParameter("method");
+			if (! StringUtils.isEmpty(m) && (servlet.context.allowMethodParams.contains(m) || servlet.context.allowMethodParams.contains("*")))
+				method = m;
+
+			if (servlet.context.allowBodyParam)
+				body = getQueryParameter("body");
+
+			defaultServletHeaders = servlet.getDefaultRequestHeaders();
+
+			debug = "true".equals(getQueryParameter("debug", "false"));
+
+			if (debug) {
+				servlet.log(Level.INFO, toString());
+			}
+
+		} catch (RestException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new ServletException(e);
+		}
+	}
+
+	/*
+	 * Called from RestServlet after a match has been made but before the guard or method invocation.
+	 */
+	@SuppressWarnings("hiding")
+	final void init(Method javaMethod, String pathRemainder, ObjectMap properties, Map<String,String> mDefaultRequestHeaders, String defaultCharset, SerializerGroup mSerializers, ParserGroup mParsers, UrlEncodingParser mUrlEncodingParser) {
+		this.javaMethod = javaMethod;
+		this.pathRemainder = pathRemainder;
+		this.properties = properties;
+		this.defaultMethodHeaders = mDefaultRequestHeaders;
+		this.serializerGroup = mSerializers;
+		this.parserGroup = mParsers;
+		this.urlEncodingParser = mUrlEncodingParser;
+		this.beanSession = urlEncodingParser.getBeanContext().createSession();
+		this.defaultCharset = defaultCharset;
+	}
+
+	/**
+	 * Returns a string of the form <js>"HTTP method-name full-url"</js>
+	 *
+	 * @return A description of the request.
+	 */
+	public String getDescription() {
+		String qs = getQueryString();
+		return "HTTP " + getMethod() + " " + getRequestURI() + (qs == null ? "" : "?" + qs);
+	}
+
+	//--------------------------------------------------------------------------------
+	// Properties
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Servlet calls this method to initialize the properties.
+	 */
+	RestRequest setProperties(ObjectMap properties) {
+		this.properties = properties;
+		return this;
+	}
+
+	/**
+	 * Retrieve the properties active for this request.
+	 * <p>
+	 * 	These properties can be modified by the request.
+	 *
+	 * @return The properties active for this request.
+	 */
+	public ObjectMap getProperties() {
+		return this.properties;
+	}
+
+	//--------------------------------------------------------------------------------
+	// Headers
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Sets a request header value.
+	 *
+	 * @param name The header name.
+	 * @param value The header value.
+	 */
+	public void setHeader(String name, String value) {
+		if (overriddenHeaders == null)
+			overriddenHeaders = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+		overriddenHeaders.put(name, value);
+	}
+
+
+	/**
+	 * Returns the specified header value, or <jk>null</jk> if the header doesn't exist.
+	 * <p>
+	 * 	If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks
+	 * 	for {@code &HeaderName=x} in the URL query string.
+	 */
+	@Override /* ServletRequest */
+	public String getHeader(String name) {
+		return getHeader(name, (String)null);
+	}
+
+	/**
+	 * Returns the specified header value, or a default value if the header doesn't exist.
+	 * <p>
+	 * 	If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks
+	 * 	for {@code &HeaderName=x} in the URL query string.
+	 *
+	 * @param name The HTTP header name.
+	 * @param def The default value to return if the header value isn't found.
+	 * @return The header value, or the default value if the header isn't present.
+	 */
+	public String getHeader(String name, String def) {
+		String h = getOverriddenHeader(name);
+		if (h != null)
+			return h;
+		h = super.getHeader(name);
+		if (h != null && ! h.isEmpty())
+			return h;
+		if (defaultMethodHeaders != null) {
+			h = defaultMethodHeaders.get(name);
+			if (h != null)
+				return h;
+		}
+		h = defaultServletHeaders.get(name);
+		if (h != null)
+			return h;
+		return def;
+	}
+
+	/**
+	 * Returns the specified header value converted to a POJO.
+	 * <p>
+	 * 	The type can be any POJO type convertable from a <code>String</code> (See <a class='doclink' href='package-summary.html#PojosConvertableFromString'>POJOs Convertable From Strings</a>).
+	 *
+	 * @param name The HTTP header name.
+	 * @param c The class type to convert the header value to.
+	 * @param def The default value if the header was not specified or is <jk>null</jk>.
+	 * @param <T> The class type to convert the header value to.
+	 * @return The parameter value converted to the specified class type.
+	 */
+	public <T> T getHeader(String name, Class<T> c, T def) {
+		String h = getHeader(name);
+		if (h == null)
+			return def;
+		return beanSession.convertToType(h, c);
+	}
+
+	/**
+	 * Returns the specified header value converted to a POJO.
+	 * <p>
+	 * 	The type can be any POJO type convertable from a <code>String</code> (See <a class='doclink' href='package-summary.html#PojosConvertableFromString'>POJOs Convertable From Strings</a>).
+	 *
+	 * @param name The HTTP header name.
+	 * @param c The class type to convert the header value to.
+	 * @param <T> The class type to convert the header value to.
+	 * @return The parameter value converted to the specified class type.
+	 */
+	public <T> T getHeader(String name, Class<T> c) {
+		String h = getHeader(name);
+		return beanSession.convertToType(h, c);
+	}
+
+	/**
+	 * Same as {@link #getHeader(String, Class)} except works on parameterized
+	 * types such as those returned by {@link Method#getGenericParameterTypes()}
+	 *
+	 * @param name The HTTP header name.
+	 * @param c The class type to convert the header value to.
+	 * @param <T> The class type to convert the header value to.
+	 * @return The parameter value converted to the specified class type.
+	 */
+	public <T> T getHeader(String name, Type c) {
+		String h = getHeader(name);
+		return (T)beanSession.convertToType(null, h, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Returns all the request headers as an {@link ObjectMap}.
+	 * <p>
+	 * 	Altering entries in this map does not alter headers in the underlying request.
+	 *
+	 * @return The request headers.  Never <jk>null</jk>.
+	 */
+	public ObjectMap getHeaders() {
+		if (headers == null) {
+			headers = new ObjectMap();
+			for (Enumeration<String> e = getHeaderNames(); e.hasMoreElements();) {
+				String key = e.nextElement();
+				headers.put(key, getHeader(key));
+			}
+		}
+		return headers;
+	}
+
+	@Override /* ServletRequest */
+	public Enumeration<String> getHeaders(String name) {
+		String h = getOverriddenHeader(name);
+		if (h != null)
+			return enumeration(singleton(h));
+		return super.getHeaders(name);
+	}
+
+	/**
+	 * Returns the <code>Content-Type</code> header value on the request, stripped
+	 * 	of any parameters such as <js>";charset=X"</js>.
+	 * <p>
+	 * 	Example: <js>"text/json"</js>.
+	 * <p>
+	 * 	If the content type is not specified, and the content is specified via a
+	 * 	<code>&body</code> query parameter, the content type is assumed to be
+	 * 	<js>"text/uon"</js>.  Otherwise, the content type is assumed to be <js>"text/json"</js>.
+	 *
+	 * @return The <code>Content-Type</code> media-type header value on the request.
+	 */
+	public String getMediaType() {
+		String cm = getHeader("Content-Type");
+		if (cm == null) {
+			if (body != null)
+				return "text/uon";
+			return "text/json";
+		}
+		int j = cm.indexOf(';');
+		if (j != -1)
+			cm = cm.substring(0, j);
+		return cm;
+	}
+
+	/**
+	 * Returns the <code>Time-Zone</code> header value on the request if there is one.
+	 * <p>
+	 * 	Example: <js>"GMT"</js>.
+	 *
+	 * @return The <code>Time-Zone</code> header value on the request, or <jk>null</jk> if not present.
+	 */
+	public TimeZone getTimeZone() {
+		String tz = getHeader("Time-Zone");
+		if (tz != null)
+			return TimeZone.getTimeZone(tz);
+		return null;
+	}
+
+	/**
+	 * Returns the media types that are valid for <code>Content-Type</code> headers on the request.
+	 *
+	 * @return The set of media types registered in the parser group of this request.
+	 */
+	public List<String> getSupportedMediaTypes() {
+		return parserGroup.getSupportedMediaTypes();
+	}
+
+	/**
+	 * Sets the charset to expect on the request body.
+	 */
+	@Override /* ServletRequest */
+	public void setCharacterEncoding(String charset) {
+		this.charset = charset;
+	}
+
+	/**
+	 * Returns the charset specified on the <code>Content-Type</code> header, or
+	 * <js>"UTF-8"</js> if not specified.
+	 */
+	@Override /* ServletRequest */
+	public String getCharacterEncoding() {
+		if (charset == null) {
+			// Determine charset
+			// NOTE:  Don't use super.getCharacterEncoding() because the spec is implemented inconsistently.
+			// Jetty returns the default charset instead of null if the character is not specified on the request.
+			String h = getHeader("Content-Type");
+			if (h != null) {
+				int i = h.indexOf(";charset=");
+				if (i > 0)
+					charset = h.substring(i+9).trim();
+			}
+			if (charset == null)
+				charset = defaultCharset;
+			if (! RestServlet.availableCharsets.containsKey(charset))
+				throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported charset in header ''Content-Type'': ''{0}''", h);
+		}
+		return charset;
+	}
+
+	@Override /* ServletRequest */
+	public Locale getLocale() {
+		String h = getOverriddenHeader("Accept-Language");
+		if (h != null) {
+			MediaRange[] mr = MediaRange.parse(h);
+			if (mr.length > 0)
+				return toLocale(mr[0].getType());
+		}
+		return super.getLocale();
+	}
+
+	@Override /* ServletRequest */
+	public Enumeration<Locale> getLocales() {
+		String h = getOverriddenHeader("Accept-Language");
+		if (h != null) {
+			MediaRange[] mr = MediaRange.parse(h);
+			if (mr.length > 0) {
+				List<Locale> l = new ArrayList<Locale>(mr.length);
+				for (MediaRange r : mr)
+					l.add(toLocale(r.getType()));
+				return enumeration(l);
+			}
+		}
+		return super.getLocales();
+	}
+
+	//--------------------------------------------------------------------------------
+	// Query parameters
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Sets a request query parameter value.
+	 *
+	 * @param name The parameter name.
+	 * @param value The parameter value.
+	 */
+	public void setQueryParameter(String name, Object value) {
+		if (overriddenQueryParams == null)
+			overriddenQueryParams = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+		overriddenQueryParams.put(name, value == null ? null : value.toString());
+	}
+
+	/**
+	 * Returns a query parameter value.
+	 * <p>
+	 * 	Same as {@link #getParameter(String)} except only looks in the URL string,
+	 * 	not parameters from URL-Encoded FORM posts.
+	 * <p>
+	 * This method can be used to retrieve a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The URL parameter name.
+	 * @return The parameter value, or <jk>null</jk> if parameter not specified or has no value (e.g. <js>"&foo"</js>.
+	 */
+	public String getQueryParameter(String name) {
+		String s = null;
+		if (overriddenQueryParams != null)
+			s = overriddenQueryParams.get(name);
+		if (s != null)
+			return s;
+		String[] v = queryParams.get(name);
+		if (v == null || v.length == 0)
+			return null;
+		if (v.length == 1 && v[0] != null && v[0].isEmpty()) {
+			// Fix for behavior difference between Tomcat and WAS.
+			// getParameter("foo") on "&foo" in Tomcat returns "".
+			// getParameter("foo") on "&foo" in WAS returns null.
+			if (queryParams.containsKey(name))
+				return null;
+		}
+		return v[0];
+	}
+
+	/**
+	 * Same as {@link #getQueryParameter(String)} but returns the specified default
+	 * 	value if the query parameter was not specified.
+	 *
+	 * @param name The URL parameter name.
+	 * @param def The default value.
+	 * @return The parameter value, or the default value if parameter not specified or has no value (e.g. <js>"&foo"</js>.
+	 */
+	public String getQueryParameter(String name, String def) {
+		String s = getQueryParameter(name);
+		return s == null ? def : s;
+	}
+
+	/**
+	 * Returns the specified query parameter value converted to a POJO.
+	 * <p>
+	 * This method can be used to retrieve a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameter(String name, Class<T> c, T def) throws ParseException {
+		return getQueryParameter(name, beanSession.getClassMeta(c), def);
+	}
+
+	/**
+	 * Returns the specified query parameter value converted to a POJO.
+	 * <p>
+	 * This method can be used to retrieve a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameter(String name, ClassMeta<T> cm, T def) throws ParseException {
+		String val = getQueryParameter(name);
+		if (val == null)
+			return def;
+		return parseParameter(val, cm);
+	}
+
+	/**
+	 * Returns the specified query parameter value converted to a POJO.
+	 * <p>
+	 * This method can be used to retrieve a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameter(String name, Class<T> c) throws ParseException {
+		return getQueryParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getQueryParameter(String, Class)} except for use on multi-part parameters
+	 * 	(e.g. <js>"&key=1&key=2&key=3"</js> instead of <js>"&key=(1,2,3)"</js>).
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The query parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The query parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameters(String name, Class<T> c) throws ParseException {
+		return getQueryParameters(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getQueryParameter(String, Class)} except works on parameterized
+	 * types such as those returned by {@link Method#getGenericParameterTypes()}
+	 *
+	 * @param name The query parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The query parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameter(String name, Type c) throws ParseException {
+		return (T)getQueryParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getQueryParameter(String, Type)} except for use on multi-part parameters
+	 * 	(e.g. <js>"&key=1&key=2&key=3"</js> instead of <js>"&key=(1,2,3)"</js>).
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The query parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The query parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameters(String name, Type c) throws ParseException {
+		return (T)getQueryParameters(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Returns the specified query parameter value converted to a POJO.
+	 * <p>
+	 * 	This method can be used to retrieve a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getQueryParameter(String name, ClassMeta<T> cm) throws ParseException {
+
+		String val = getQueryParameter(name);
+
+		if (cm.isPrimitive() && (val == null || val.isEmpty()))
+			return cm.getPrimitiveDefault();
+		return parseParameter(val, cm);
+	}
+
+	/**
+	 * Same as {@link #getQueryParameter(String, ClassMeta)} except for use on multi-part parameters
+	 * 	(e.g. <js>"&key=1&key=2&key=3"</js> instead of <js>"&key=(1,2,3)"</js>).
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	@SuppressWarnings("rawtypes")
+	public <T> T getQueryParameters(String name, ClassMeta<T> cm) throws ParseException {
+		String[] p = getQueryParameters(name);
+		if (p == null)
+			return null;
+		if (cm.isArray()) {
+			List c = new ArrayList();
+			for (int i = 0; i < p.length; i++)
+				c.add(parseParameter(p[i], cm.getElementType()));
+			return (T)ArrayUtils.toArray(c, cm.getElementType().getInnerClass());
+		} else if (cm.isCollection()) {
+			try {
+				Collection c = (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new ObjectList());
+				for (int i = 0; i < p.length; i++)
+					c.add(parseParameter(p[i], cm.getElementType()));
+				return (T)c;
+			} catch (ParseException e) {
+				throw e;
+			} catch (Exception e) {
+				// Typically an instantiation exception.
+				throw new ParseException(e);
+			}
+		}
+		throw new ParseException("Invalid call to getQueryParameters(String, ClassMeta).  Class type must be a Collection or array.");
+	}
+
+	/**
+	 * Returns the list of all query parameters with the specified name.
+	 * <p>
+	 * 	Same as {@link #getParameterValues(String)} except only looks in the URL string,
+	 * 	not parameters from URL-Encoded FORM posts.
+	 * <p>
+	 * 	This method can be used to retrieve parameters without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name
+	 * @return the list of query parameters, or <jk>null</jk> if the parameter does not exist.
+	 */
+	public String[] getQueryParameters(String name) {
+		return queryParams.get(name);
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the query parameters on this request contains the specified entry.
+	 * <p>
+	 * 	Note that this returns <jk>true</jk> even if the value is set to null (e.g. <js>"?key"</js>).
+	 * <p>
+	 * 	This method can be used to check the existence of a parameter without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 *
+	 * @param name The URL parameter name.
+	 * @return <jk>true</jk> if the URL parameters on this request contains the specified entry.
+	 */
+	public boolean hasQueryParameter(String name) {
+		return queryParams.containsKey(name);
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the request contains any of the specified query parameters.
+	 *
+	 * @param params The list of parameters to check for.
+	 * @return <jk>true</jk> if the request contains any of the specified query parameters.
+	 */
+	public boolean hasAnyQueryParameters(String...params) {
+		for (String p : params)
+			if (hasQueryParameter(p))
+				return true;
+		return false;
+	}
+
+	/**
+	 * Equivalent to {@link #getParameterMap()}, but only looks for query parameters in the URL, not form posts.
+	 * <p>
+	 * 	This method can be used to retrieve query parameters without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 * <p>
+	 * 	This object is modifiable.
+	 *
+	 * @return The query parameters as a modifiable map.
+	 */
+	public Map<String,String[]> getQueryParameterMap() {
+		return queryParams;
+	}
+
+	/**
+	 * Equivalent to {@link #getParameterNames()}, but only looks for query parameters in the URL, not form posts.
+	 * <p>
+	 * 	This method can be used to retrieve query parameters without triggering the underlying
+	 * 	servlet API to load and parse the request body.
+	 * <p>
+	 * 	This object is modifiable.
+	 *
+	 * @return An iterator of query parameter names.
+	 */
+	public Iterator<String> getQueryParameterNames() {
+		return queryParams.keySet().iterator();
+	}
+
+	//--------------------------------------------------------------------------------
+	// Form data parameters
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Sets a request form data parameter value.
+	 *
+	 * @param name The parameter name.
+	 * @param value The parameter value.
+	 */
+	public void setFormDataParameter(String name, Object value) {
+		if (overriddenFormDataParams == null)
+			overriddenFormDataParams = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+		overriddenFormDataParams.put(name, value == null ? null : value.toString());
+	}
+
+	/**
+	 * Returns a form data parameter value.
+	 * <p>
+	 * 	Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * <p>
+	 * 	<i>Note:</i> Calling this method on URL-Encoded FORM posts causes the body content to be loaded and parsed by
+	 * 	the underlying servlet API.
+	 * <p>
+	 * 	<i>Note:</i> This method returns the raw unparsed value, and differs from calling <code>getFormDataParameter(name, String.<jk>class</js>)</code>
+	 * 	which will convert the value from UON notation:
+	 * <ul>
+	 * 	<li><js>"\u0000"</js> =&gt; <jk>null</jk>
+	 * 	<li><js>"$s(foo)"</js> =&gt; <js>"foo"</js>
+	 * 	<li><js>"(foo)"</js> =&gt; <js>"foo"</js>
+	 * </ul>
+	 *
+	 * @param name The form data parameter name.
+	 * @return The parameter value, or <jk>null</jk> if parameter does not exist.
+	 */
+	public String getFormDataParameter(String name) {
+		String s = null;
+		if (overriddenFormDataParams != null)
+			s = overriddenFormDataParams.get(name);
+		if (s != null)
+			return s;
+
+		return super.getParameter(name);
+	}
+
+	/**
+	 * Same as {@link #getFormDataParameter(String)} except returns a default value if <jk>null</jk> or empty.
+	 *
+	 * @param name The form data parameter name.
+	 * @param def The default value.
+	 * @return The parameter value, or the default value if <jk>null</jk> or empty.
+	 */
+	public String getFormDataParameter(String name, String def) {
+		String val = getParameter(name);
+		if (val == null || val.isEmpty())
+			return def;
+		return val;
+	}
+
+	/**
+	 * Returns the specified form data parameter value converted to a POJO using the
+	 * 	{@link UrlEncodingParser} registered with this servlet.
+	 * <p>
+	 * 	<i>Note:</i> Calling this method on URL-Encoded FORM posts causes the body content to be loaded and parsed by
+	 * 	the underlying servlet API.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameter(String name, Class<T> c, T def) throws ParseException {
+		return getFormDataParameter(name, beanSession.getClassMeta(c), def);
+	}
+
+	/**
+	 * Returns the specified form data parameter value converted to a POJO using the
+	 * 	{@link UrlEncodingParser} registered with this servlet.
+	 * <p>
+	 * 	<i>Note:</i> Calling this method on URL-Encoded FORM posts causes the body content to be loaded and parsed by
+	 * 	the underlying servlet API.
+	 * <p>
+	 * 	Unlike {@link #getFormDataParameter(String, Class, Object)}, this method can be used to parse parameters
+	 * 	of complex types involving JCF classes.
+	 * <p class='bcode'>
+	 * 	ClassMeta&ltMap&lt;String,Integer&gt;&gt; cm = request.getBeanContext().getMapClassMeta(TreeMap.<jk>class</jk>, String.<jk>class</jk>, Integer.<jk>class</jk>);
+	 * 	Map&lt;String,Integer&gt; m = request.getFormDataParameter(<js>"myParameter"</js>, cm, <jk>new</jk> TreeMap&lt;String,Integer&gt;());
+	 * </p>
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameter(String name, ClassMeta<T> cm, T def) throws ParseException {
+		String val = getParameter(name);
+		if (val == null)
+			return def;
+		return parseParameter(val, cm);
+	}
+
+	/**
+	 * Returns the specified form data parameter value converted to a POJO using the
+	 * 	{@link UrlEncodingParser} registered with this servlet.
+	 * <p>
+	 * <i>Note:</i> Calling this method on URL-Encoded FORM posts causes the body content to be loaded and parsed by
+	 * 	the underlying servlet API.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameter(String name, Class<T> c) throws ParseException {
+		return getFormDataParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getFormDataParameter(String, Class)} except for use on multi-part parameters
+	 * 	(e.g. <js>"key=1&key=2&key=3"</js> instead of <js>"key=(1,2,3)"</js>)
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameters(String name, Class<T> c) throws ParseException {
+		return getFormDataParameters(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getFormDataParameter(String, Class)} except works on parameterized
+	 * types such as those returned by {@link Method#getGenericParameterTypes()}
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameter(String name, Type c) throws ParseException {
+		return (T)getFormDataParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getFormDataParameter(String, Class)} except for use on multi-part parameters
+	 * 	(e.g. <js>"key=1&key=2&key=3"</js> instead of <js>"key=(1,2,3)"</js>)
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The parameter name.
+	 * @param c The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameters(String name, Type c) throws ParseException {
+		return (T)getFormDataParameters(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Returns the specified form data parameter value converted to a POJO using the
+	 * 	{@link UrlEncodingParser} registered with this servlet.
+	 * <p>
+	 * 	<i>Note:</i> Calling this method on URL-Encoded FORM posts causes the body content to be loaded and parsed by
+	 * 	the underlying servlet API.
+	 * <p>
+	 * 	Unlike {@link #getFormDataParameter(String, Class)}, this method can be used to parse parameters
+	 * 	of complex types involving JCF classes.
+	 * <p class='bcode'>
+	 * 	ClassMeta&lt;Map&lt;String,Integer&gt;&gt; cm = request.getBeanContext().getMapClassMeta(TreeMap.<jk>class</jk>, String.<jk>class</jk>, Integer.<jk>class</jk>);
+	 * 	Map&lt;String,Integer&gt; m = request.getFormDataParameter(<js>"myParameter"</js>, cm);
+	 * </p>
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getFormDataParameter(String name, ClassMeta<T> cm) throws ParseException {
+
+		String val = getParameter(name);
+
+		if (cm.isPrimitive() && (val == null || val.isEmpty()))
+			return cm.getPrimitiveDefault();
+
+		return parseParameter(val, cm);
+	}
+
+	/**
+	 * Same as {@link #getFormDataParameter(String, ClassMeta)} except for use on multi-part parameters
+	 * 	(e.g. <js>"key=1&key=2&key=3"</js> instead of <js>"key=(1,2,3)"</js>)
+	 * <p>
+	 * 	This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * @param name The parameter name.
+	 * @param cm The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	@SuppressWarnings("rawtypes")
+	public <T> T getFormDataParameters(String name, ClassMeta<T> cm) throws ParseException {
+		String[] p = getParameterValues(name);
+		if (p == null)
+			return null;
+		if (cm.isArray()) {
+			List c = new ArrayList();
+			for (int i = 0; i < p.length; i++)
+				c.add(parseParameter(p[i], cm.getElementType()));
+			return (T)ArrayUtils.toArray(c, cm.getElementType().getInnerClass());
+		} else if (cm.isCollection()) {
+			try {
+				Collection c = (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new ObjectList());
+				for (int i = 0; i < p.length; i++)
+					c.add(parseParameter(p[i], cm.getElementType()));
+				return (T)c;
+			} catch (ParseException e) {
+				throw e;
+			} catch (Exception e) {
+				// Typically an instantiation exception.
+				throw new ParseException(e);
+			}
+		}
+		throw new ParseException("Invalid call to getParameters(String, ClassMeta).  Class type must be a Collection or array.");
+	}
+
+	/**
+	 * Returns <jk>true</jk> if the form data parameters on this request contains the specified entry.
+	 * <p>
+	 * 	Note that this returns <jk>true</jk> even if the value is set to null (e.g. <js>"?key"</js>).
+	 *
+	 * @param name The URL parameter name.
+	 * @return <jk>true</jk> if the URL parameters on this request contains the specified entry.
+	 */
+	public boolean hasFormDataParameter(String name) {
+		return getParameterMap().containsKey(name);
+	}
+
+	//--------------------------------------------------------------------------------
+	// Path parameters
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Sets a path parameter value.
+	 * <p>
+	 * 	A path parameter is a variable in the path pattern such as <js>"/{foo}"</js>
+	 *
+	 * @param name The parameter name.
+	 * @param value The parameter value.
+	 */
+	public void setPathParameter(String name, String value) {
+		if (pathParameters == null)
+			pathParameters = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+		pathParameters.put(name, value == null ? null : value.toString());
+	}
+
+	/**
+	 * Returns a path parameter value.
+	 * <p>
+	 * 	A path parameter is a variable in the path pattern such as <js>"/{foo}"</js>
+	 *
+	 * @param name The parameter name.
+	 * @return The paramter value, or <jk>null</jk> if path parameter not specified.
+	 */
+	public String getPathParameter(String name) {
+		return (pathParameters == null ? null : pathParameters.get(name));
+	}
+
+	/**
+	 * Returns the specified path parameter converted to a POJO.
+	 * <p>
+	 * 	The type can be any POJO type convertable from a <code>String</code> (See <a class='doclink' href='package-summary.html#PojosConvertableFromString'>POJOs Convertable From Strings</a>).
+	 *
+	 * @param name The attribute name.
+	 * @param c The class type to convert the attribute value to.
+	 * @param <T> The class type to convert the attribute value to.
+	 * @return The attribute value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getPathParameter(String name, Class<T> c) throws ParseException {
+		return getPathParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Same as {@link #getPathParameter(String, Class)} except works on parameterized
+	 * types such as those returned by {@link Method#getGenericParameterTypes()}
+	 *
+	 * @param name The attribute name.
+	 * @param c The class type to convert the attribute value to.
+	 * @param <T> The class type to convert the attribute value to.
+	 * @return The attribute value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getPathParameter(String name, Type c) throws ParseException {
+		return (T)getPathParameter(name, beanSession.getClassMeta(c));
+	}
+
+	/**
+	 * Returns the specified path parameter converted to a POJO.
+	 * <p>
+	 * 	The type can be any POJO type convertable from a <code>String</code> (See <a class='doclink' href='package-summary.html#PojosConvertableFromString'>POJOs Convertable From Strings</a>).
+	 *
+	 * @param name The attribute name.
+	 * @param cm The class type to convert the attribute value to.
+	 * @param <T> The class type to convert the attribute value to.
+	 * @return The attribute value converted to the specified class type.
+	 * @throws ParseException
+	 */
+	public <T> T getPathParameter(String name, ClassMeta<T> cm) throws ParseException {
+		Object attr = getPathParameter(name);
+		T t = null;
+		if (attr != null)
+			t = urlEncodingParser.parseParameter(attr.toString(), cm);
+		if (t == null && cm.isPrimitive())
+			return cm.getPrimitiveDefault();
+		return t;
+	}
+
+	//--------------------------------------------------------------------------------
+	// Body methods
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Same as {@link #getBody(ClassMeta)}, except a shortcut for passing in regular {@link Class} objects
+	 * 	instead of having to look up {@link ClassMeta} objects.
+	 *
+	 * @param type The class type to instantiate.
+	 * @param <T> The class type to instantiate.
+	 * @return The input parsed to a POJO.
+	 * @throws IOException If a problem occurred trying to read from the reader.
+	 * @throws ParseException If the input contains a syntax error or is malformed for the requested {@code Accept} header or is not valid for the specified type.
+	 */
+	public <T> T getBody(Class<T> type) throws IOException, ParseException {
+		return getBody(beanSession.getClassMeta(type));
+	}
+
+	/**
+	 * Same as {@link #getBody(Class)} except works on parameterized
+	 * types such as those returned by {@link Method#getGenericParameterTypes()}
+	 *
+	 * @param type The class type to instantiate.
+	 * @param <T> The class type to instantiate.
+	 * @return The input parsed to a POJO.
+	 */
+	public <T> T getBody(Type type) {
+		return (T)getBody(beanSession.getClassMeta(type));
+	}
+
+	/**
+	 * Reads the input from the HTTP request as JSON, XML, or HTML and converts the input to a POJO.
+	 * <p>
+	 * 	If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks
+	 * 	for {@code &body=xxx} in the URL query string.
+	 * <p>
+	 * 	If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined automatically based on the
+	 * 	following input:
+	 * 	<table class='styled'>
+	 * 		<tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr>
+	 * 		<tr>
+	 * 			<td>object</td>
+	 * 			<td><js>"{...}"</js></td>
+	 * 			<td><code><xt>&lt;object&gt;</xt>...<xt>&lt;/object&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'object'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</xt></code></td>
+	 * 			<td>{@link ObjectMap}</td>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<td>array</td>
+	 * 			<td><js>"[...]"</js></td>
+	 * 			<td><code><xt>&lt;array&gt;</xt>...<xt>&lt;/array&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'array'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</xt></code></td>
+	 * 			<td>{@link ObjectList}</td>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<td>string</td>
+	 * 			<td><js>"'...'"</js></td>
+	 * 			<td><code><xt>&lt;string&gt;</xt>...<xt>&lt;/string&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'string'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</xt></code></td>
+	 * 			<td>{@link String}</td>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<td>number</td>
+	 * 			<td><code>123</code></td>
+	 * 			<td><code><xt>&lt;number&gt;</xt>123<xt>&lt;/number&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'number'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</xt></code></td>
+	 * 			<td>{@link Number}</td>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<td>boolean</td>
+	 * 			<td><jk>true</jk></td>
+	 * 			<td><code><xt>&lt;boolean&gt;</xt>true<xt>&lt;/boolean&gt;</xt></code><br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>&gt;</xt>...<xt>&lt;/x&gt;</xt></code></td>
+	 * 			<td>{@link Boolean}</td>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<td>null</td>
+	 * 			<td><jk>null</jk> or blank</td>
+	 * 			<td><code><xt>&lt;null/&gt;</xt></code> or blank<br><code><xt>&lt;x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/&gt;</xt></code></td>
+	 * 			<td><jk>null</jk></td>
+	 * 		</tr>
+	 * 	</table>
+	 * <p>
+	 * 	Refer to <a href='../../../../overview-summary.html#Core.PojoCategories' class='doclink'>POJO Categories</a> for a complete definition of supported POJOs.
+	 *
+	 * @param type The class type to instantiate.
+	 * @param <T> The class type to instantiate.
+	 * @return The input parsed to a POJO.
+	 * @throws RestException If a problem occurred trying to read the input.
+	 */
+	public <T> T getBody(ClassMeta<T> type) throws RestException {
+
+		try {
+			if (type.isReader())
+				return (T)getReader();
+
+			if (type.isInputStream())
+				return (T)getInputStream();
+
+			String mediaType = getMediaType();
+			TimeZone timeZone = getTimeZone();
+			Locale locale = getLocale();
+			Parser p = getParser();
+
+			if (p != null) {
+				try {
+					properties.append("mediaType", mediaType).append("characterEncoding", getCharacterEncoding());
+					if (! p.isReaderParser()) {
+						InputStreamParser p2 = (InputStreamParser)p;
+						ParserSession session = p2.createSession(getInputStream(), properties, getJavaMethod(), getServlet(), locale, timeZone);
+						return p2.parse(session, type);
+					}
+					ReaderParser p2 = (ReaderParser)p;
+					ParserSession session = p2.createSession(getUnbufferedReader(), properties, getJavaMethod(), getServlet(), locale, timeZone);
+					return p2.parse(session, type);
+				} catch (ParseException e) {
+					throw new RestException(SC_BAD_REQUEST,
+						"Could not convert request body content to class type ''{0}'' using parser ''{1}''.",
+						type, p.getClass().getName()
+					).initCause(e);
+				}
+			}
+
+			throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE,
+				"Unsupported media-type in request header ''Content-Type'': ''{0}''\n\tSupported media-types: {1}",
+				getHeader("Content-Type"), parserGroup.getSupportedMediaTypes()
+			);
+
+		} catch (IOException e) {
+			throw new RestException(SC_INTERNAL_SERVER_ERROR,
+				"I/O exception occurred while attempting to handle request ''{0}''.",
+				getDescription()
+			).initCause(e);
+		}
+	}
+
+	/**
+	 * Returns the HTTP body content as a plain string.
+	 * <p>
+	 * 	If {@code allowHeaderParams} init parameter is true, then first looks
+	 * 	for {@code &body=xxx} in the URL query string.
+	 *
+	 * @return The incoming input from the connection as a plain string.
+	 * @throws IOException If a problem occurred trying to read from the reader.
+	 */
+	public String getBodyAsString() throws IOException {
+		if (body != null)
+			return body;
+		body = IOUtils.read(getReader()).toString();
+		return body;
+	}
+
+	/**
+	 * Returns the HTTP body content as a {@link Reader}.
+	 * <p>
+	 * 	If {@code allowHeaderParams} init parameter is true, then first looks
+	 * 	for {@code &body=xxx} in the URL query string.
+	 * <p>
+	 * 	Automatically handles GZipped input streams.
+	 */
+	@Override /* ServletRequest */
+	public BufferedReader getReader() throws IOException {
+		Reader r = getUnbufferedReader();
+		if (r instanceof BufferedReader)
+			return (BufferedReader)r;
+		int len = getContentLength();
+		int buffSize = len <= 0 ? 8192 : Math.max(len, 8192);
+		return new BufferedReader(r, buffSize);
+	}
+
+	/**
+	 * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
+	 *
+	 * @return An unbuffered reader.
+	 * @throws IOException
+	 */
+	protected Reader getUnbufferedReader() throws IOException {
+		if (body != null)
+			return new CharSequenceReader(body);
+		return new InputStreamReader(getInputStream(), getCharacterEncoding());
+	}
+
+	/**
+	 * Returns the HTTP body content as an {@link InputStream}.
+	 * <p>
+	 * 	Automatically handles GZipped input streams.
+	 *
+	 * @return The negotiated input stream.
+	 * @throws IOException If any error occurred while trying to get the input stream or wrap it
+	 * 	in the GZIP wrapper.
+	 */
+	@Override /* ServletRequest */
+	public ServletInputStream getInputStream() throws IOException {
+
+		Encoder enc = getEncoder();
+
+		ServletInputStream is = super.getInputStream();
+		if (enc != null) {
+			final InputStream is2 = enc.getInputStream(is);
+			return new ServletInputStream() {
+				@Override /* InputStream */
+				public final int read() throws IOException {
+					return is2.read();
+				}
+				@Override /* InputStream */
+				public final void close() throws IOException {
+					is2.close();
+				}
+			};
+		}
+		return is;
+	}
+
+	//--------------------------------------------------------------------------------
+	// URI-related methods
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Same as {@link HttpServletRequest#getPathInfo()} except returns the path undecoded.
+	 *
+	 * @return The undecoded portion of the URL after the resource URL path pattern match.
+	 */
+	public String getPathInfoUndecoded() {
+		return RestUtils.getPathInfoUndecoded(this);
+	}
+
+	/**
+	 * Returns the value {@link #getPathInfo()} split on the <js>'/'</js> character.
+	 * <p>
+	 * 	If path info is <jk>null</jk>, returns an empty list.
+	 * <p>
+	 * 	URL-encoded characters in segments are automatically decoded by this method.
+	 *
+	 * @return The decoded segments, or an empty list if path info is <jk>null</jk>.
+	 */
+	public String[] getPathInfoParts() {
+		String s = getPathInfoUndecoded();
+		if (s == null || s.isEmpty() || s.equals("/"))
+			return new String[0];
+		s = s.substring(1);
+		if (s.endsWith("/"))
+			s = s.substring(0, s.length()-1);
+		boolean needsDecode = (s.indexOf('%') != -1 || s.indexOf('+') != -1);
+		String[] l = s.split("/", Integer.MAX_VALUE);
+		try {
+			if (needsDecode)
+				for (int i = 0; i < l.length; i++)
+					l[i] = URLDecoder.decode(l[i], "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			e.printStackTrace();  // Won't happen.
+		}
+		return l;
+	}
+
+	/**
+	 * Returns a resolved URL.
+	 * <p>
+	 * <ul class='spaced-list'>
+	 * 	<li>Fully-qualified absolute URLs (e.g. <js>"http://..."</js>, <js>"https://"</js>) are simply converted to a URL.
+	 * 	<li>Absolute URLs (e.g. <js>"/foo/..."</js>) are interpreted as relative to the server hostname.
+	 * 	<li>Relative URLs (e.g. <js>"foo/..."</js>) are interpreted as relative to this servlet path.
+	 * </ul>
+	 *
+	 * @param path The URL path to resolve.
+	 * @return The resolved URL.
+	 * @throws MalformedURLException If path is not a valid URL component.
+	 */
+	public URL getURL(String path) throws MalformedURLException {
+		if (path.startsWith("http://") || path.startsWith("https://"))
+			return new URL(path);
+		if (StringUtils.startsWith(path, '/'))
+			return new URL(getScheme(), getLocalName(), getLocalPort(), path);
+		return new URL(getScheme(), getLocalName(), getLocalPort(), getContextPath() + getServletPath() + (StringUtils.isEmpty(path) ? "" : ('/' + path)));
+	}
+
+	/**
+	 * Returns the URI of the parent of this servlet.
+	 *
+	 * @return The URI of the parent of this servlet.
+	 */
+	public String getServletParentURI() {
+		String s = getServletURI();
+		return s.substring(0, s.lastIndexOf('/'));
+	}
+
+	/**
+	 * Returns the decoded remainder of the URL following any path pattern matches.
+	 * <p>
+	 * 	The behavior of path remainder is shown below given the path pattern "/foo/*":
+	 * <p>
+	 * 	<table class='styled'>
+	 * 		<tr>
+	 * 			<th>URL</th>
+	 * 			<th>Path Remainder</th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo</code></th>
+	 * 			<th><jk>null</jk></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/</code></th>
+	 * 			<th><js>""</js></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo//</code></th>
+	 * 			<th><js>"/"</js></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo///</code></th>
+	 * 			<th><js>"//"</js></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/a/b</code></th>
+	 * 			<th><js>"a/b"</js></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo//a/b/</code></th>
+	 * 			<th><js>"/a/b/"</js></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/a%2Fb</code></th>
+	 * 			<th><js>"a/b"</js></th>
+	 * 		</tr>
+	 * 	</table>
+	 *
+	 * <h6 class='topic'>Example:</h6>
+	 * <p class='bcode'>
+	 * 	<jc>// REST method</jc>
+	 * 	<ja>@RestMethod</ja>(name=<js>"GET"</js>,path=<js>"/foo/{bar}/*"</js>)
+	 * 	<jk>public</jk> doGetById(RestServlet res, RestResponse res, <jk>int</jk> bar) {
+	 * 		System.<jsm>err</jsm>.println(res.getRemainder());
+	 * 	}
+	 *
+	 * 	<jc>// Prints "path/remainder"</jc>
+	 * 	<jk>new</jk> RestCall(servletPath + <js>"/foo/123/path/remainder"</js>).connect();
+	 * </p>
+	 *
+	 * @return The path remainder string.
+	 */
+	public String getPathRemainder() {
+		return RestUtils.decode(pathRemainder);
+	}
+
+	/**
+	 * Same as {@link #getPathRemainder()} but doesn't decode characters.
+	 *
+	 * @return The undecoded path remainder.
+	 */
+	public String getPathRemainderUndecoded() {
+		return pathRemainder;
+	}
+
+	/**
+	 * Returns the URI of the parent resource.
+	 * <p>
+	 * 	Trailing slashes in the path are ignored by this method.
+	 * <p>
+	 * 	The behavior is shown below:
+	 * 	<table class='styled'>
+	 * 		<tr>
+	 * 			<th>getRequestURI</th>
+	 * 			<th>getRequestParentURI</th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/bar</code></th>
+	 * 			<th><code>/foo</code></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/bar?baz=bing</code></th>
+	 * 			<th><code>/foo</code></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/bar/</code></th>
+	 * 			<th><code>/foo</code></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo/bar//</code></th>
+	 * 			<th><code>/foo</code></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo//bar//</code></th>
+	 * 			<th><code>/foo/</code></th>
+	 * 		</tr>
+	 * 		<tr>
+	 * 			<th><code>/foo</code></th>
+	 * 			<th>/</th>
+	 * 		</tr>
+	 * 	</table>
+	 *
+	 * @return The request parent URI.
+	 */
+	public String getRequestParentURI() {
+		String uri = getRequestURI();
+		while (StringUtils.endsWith(uri, '/'))
+			uri = uri.substring(0, uri.length()-1);
+		int i = uri.lastIndexOf('/');
+		if (i <= 0)
+			return "/";
+		return uri.substring(0, i);
+	}
+
+	/**
+	 * Same as {@link #getRequestURI()} but trims trailing slashes from the result.
+	 *
+	 * @return The trimmed request URI.
+	 */
+	public String getTrimmedRequestURI() {
+		return RestUtils.trimTrailingSlashes(getRequestURI());
+	}
+
+	/**
+	 * Same as {@link #getRequestURL()} but trims trailing slashes from the result.
+	 *
+	 * @return The trimmed request URL.
+	 */
+	public StringBuffer getTrimmedRequestURL() {
+		return RestUtils.trimTrailingSlashes(getRequestURL());
+	}
+
+	/**
+	 * Gets the URI of the servlet (e.g. <js>"https://localhost:9080/contextPath/servletPath"</js>).
+	 *
+	 * @return The servlet URI.
+	 */
+	public String getServletURI() {
+		if (servletURI == null) {
+			// Note that we can't use getPathInfo() to calculate this since it replaces
+			// URL-encoded chars (e.g. %2F) which throws off the length calculation
+			// because getRequestURL() does not replace those chars.
+			servletURI = getServletURIBuilder().toString();
+		}
+		return servletURI;
+	}
+
+	/**
+	 * Gets the path-absolute relative URI of the servlet (e.g. <js>"/contextPath/servletPath"</js>).
+	 *
+	 * @return The relative servlet URI.
+	 */
+	public String getRelativeServletURI() {
+		if (relativeServletURI == null)
+			relativeServletURI = getContextPath() + getServletPath();
+		return relativeServletURI;
+	}
+
+	/**
+	 * Returns a <code>StringBuffer</code> prefilled with the string <code><js>"/[contextPath]/[servletPath]"</js></code>.
+	 *
+	 * @return The servlet URI string builder.
+	 */
+	public StringBuffer getServletURIBuilder() {
+		return RestUtils.trimPathInfo(getRequestURL(), getContextPath(), getServletPath());
+	}
+
+	//--------------------------------------------------------------------------------
+	// Labels
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Returns the localized servlet title.
+	 * <p>
+	 * 	Equivalent to calling {@link RestServlet#getTitle(RestRequest)} with this object.
+	 *
+	 * @return The localized servlet label.
+	 */
+	public String getServletTitle() {
+		return servlet.getTitle(this);
+	}
+
+	/**
+	 * Returns the localized servlet description.
+	 * <p>
+	 * 	Equivalent to calling {@link RestServlet#getDescription(RestRequest)} with this object.
+	 *
+	 * @return The localized servlet description.
+	 */
+	public String getServletDescription() {
+		return servlet.getDescription(this);
+	}
+
+	/**
+	 * Returns the localized method summary.
+	 * <p>
+	 * 	Equivalent to calling {@link RestServlet#getMethodSummary(String, RestRequest)} with this object.
+	 *
+	 * @return The localized method description.
+	 */
+	public String getMethodSummary() {
+		return servlet.getMethodSummary(javaMethod.getName(), this);
+	}
+
+	/**
+	 * Returns the localized method description.
+	 * <p>
+	 * 	Equivalent to calling {@link RestServlet#getMethodDescription(String, RestRequest)} with this object.
+	 *
+	 * @return The localized method description.
+	 */
+	public String getMethodDescription() {
+		return servlet.getMethodDescription(javaMethod.getName(), this);
+	}
+
+	//--------------------------------------------------------------------------------
+	// Other methods
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Returns the serializers associated with this request.
+	 *
+	 * @return The serializers associated with this request.
+	 */
+	public SerializerGroup getSerializerGroup() {
+		return serializerGroup;
+	}
+
+	/**
+	 * Returns the parsers associated with this request.
+	 *
+	 * @return The parsers associated with this request.
+	 */
+	public ParserGroup getParserGroup() {
+		return parserGroup;
+	}
+
+	/**
+	 * Returns the parser matching the request <code>Accept</code> header.
+	 *
+	 * @return The parser matching the request <code>Accept</code> header, or <jk>null</jk>
+	 * 	if no matching parser was found.
+	 */
+	public Parser getParser() {
+		String mediaType = getMediaType();
+		Parser p = parserGroup.getParser(mediaType);
+
+		// If no patching parser for URL-encoding, use the one defined on the servlet.
+		if (p == null && mediaType.equals("application/x-www-form-urlencoded"))
+			p = urlEncodingParser;
+
+		return p;
+	}
+
+	/**
+	 * Returns the reader parser matching the request <code>Accept</code> header.
+	 *
+	 * @return The reader parser matching the request <code>Accept</code> header, or <jk>null</jk>
+	 * 	if no matching reader parser was found, or the matching parser was an input stream parser.
+	 */
+	public ReaderParser getReaderParser() {
+		Parser p = getParser();
+		if (p.isReaderParser())
+			return (ReaderParser)p;
+		return null;
+	}
+
+	/**
+	 * Returns the method of this request.
+	 * <p>
+	 * 	If <code>allowHeaderParams</code> init parameter is <jk>true</jk>, then first looks
+	 * 	for <code>&method=xxx</code> in the URL query string.
+	 */
+	@Override /* ServletRequest */
+	public String getMethod() {
+		return method;
+	}
+
+
+	@Override /* ServletRequest */
+	public int getContentLength() {
+		return contentLength == 0 ? super.getContentLength() : contentLength;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if <code>&plainText=true</code> was specified as a URL parameter.
+	 * <p>
+	 * 	This indicates that the <code>Content-Type</code> of the output should always be set to <js>"text/plain"</js>
+	 * 	to make it easy to render in a browser.
+	 * <p>
+	 * 	This feature is useful for debugging.
+	 *
+	 * @return <jk>true</jk> if {@code &plainText=true} was specified as a URL parameter
+	 */
+	public boolean isPlainText() {
+		return "true".equals(getQueryParameter("plainText", "false"));
+	}
+
+	/**
+	 * Shortcut method for calling {@link RestServlet#getMessage(Locale, String, Object...)} based
+	 * 	on the request locale.
+	 *
+	 * @param key The message key.
+	 * @param args Optional {@link MessageFormat} variable values in the value.
+	 * @return The localized message.
+	 */
+	public String getMessage(String key, Object...args) {
+		return servlet.getMessage(getLocale(), key, args);
+	}
+
+	/**
+	 * Returns the resource bundle for the request locale.
+	 *
+	 * @return The resource bundle.  Never <jk>null</jk>.
+	 */
+	public MessageBundle getResourceBundle() {
+		return servlet.getMessages(getLocale());
+	}
+
+	/**
+	 * Returns the servlet handling the request.
+	 * <p>
+	 * 	Can be used to access servlet-init parameters or annotations during requests,
+	 * 	such as in calls to {@link RestGuard#guard(RestRequest, RestResponse)}..
+	 *
+	 * @return The servlet handling the request.
+	 */
+	public RestServlet getServlet() {
+		return servlet;
+	}
+
+	/**
+	 * Returns the java method handling the request.
+	 * <p>
+	 * 	Can be used to access the method name or method annotations during requests, such
+	 * 	as in calls to {@link RestGuard#guard(RestRequest, RestResponse)}.
+	 * <p>
+	 * 	Note:  This returns null when evaluating servlet-level guards since the method
+	 * 	has not been resolved at that point of execution.
+	 *
+	 * @return The Java method handling the request, or <code>null</code> if the method
+	 * 	has not yet been resolved.
+	 */
+	public Method getJavaMethod() {
+		return javaMethod;
+	}
+
+	/**
+	 * Returns the {@link BeanSession} associated with this request.
+	 *
+	 * @return The request bean session.
+	 */
+	public BeanSession getBeanSession() {
+		return beanSession;
+	}
+
+	/**
+	 * Returns the variable resolver session for this request using session objects created by {@link RestServlet#getSessionObjects(RestRequest)}.
+	 *
+	 * @return The variable resolver for this request.
+	 */
+	public VarResolverSession getVarResolverSession() {
+		if (varSession == null)
+			varSession = servlet.getVarResolver().createSession(servlet.getSessionObjects(this));
+		return varSession;
+	}
+
+	/**
+	 * Shortcut for calling <code>getVarResolverSession().resolve(input)</code>.
+	 *
+	 * @param input The input string to resolve variables in.
+	 * @return The string with variables resolved, or <jk>null</jk> if input is null.
+	 */
+	public String resolveVars(String input) {
+		return getVarResolverSession().resolve(input);
+	}
+
+	/**
+	 * Returns an instance of a {@link ReaderResource} that represents the contents of a resource text file from the classpath.
+	 *
+	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
+	 * @param resolveVars If <jk>true</jk>, any {@link org.apache.juneau.rest.annotation.Parameter} variables will be resolved by the variable resolver returned
+	 * 	by {@link #getVarResolverSession()}.
+	 * @param contentType The value to set as the <js>"Content-Type"</js> header for this object.
+	 * @return A new reader resource, or <jk>null</jk> if resource could not be found.
+	 * @throws IOException
+	 */
+	public ReaderResource getReaderResource(String name, boolean resolveVars, String contentType) throws IOException {
+		String s = servlet.getResourceAsString(name, getLocale());
+		if (s == null)
+			return null;
+		ReaderResource rr = new ReaderResource(s, contentType);
+		if (resolveVars)
+			rr.setVarSession(getVarResolverSession());
+		return rr;
+	}
+
+	/**
+	 * Same as {@link #getReaderResource(String, boolean, String)} except uses {@link RestServlet#getMimetypesFileTypeMap()}
+	 * to determine the media type.
+	 *
+	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
+	 * @param resolveVars If <jk>true</jk>, any {@link org.apache.juneau.rest.annotation.Parameter} variables will be resolved by the variable resolver returned
+	 * 	by {@link #getVarResolverSession()}.
+	 * @return A new reader resource, or <jk>null</jk> if resource could not be found.
+	 * @throws IOException
+	 */
+	public ReaderResource getReaderResource(String name, boolean resolveVars) throws IOException {
+		return getReaderResource(name, resolveVars, servlet.getMimetypesFileTypeMap().getContentType(name));
+	}
+
+	/**
+	 * Same as {@link #getReaderResource(String, boolean)} with <code>resolveVars == <jk>false</jk></code>
+	 *
+	 * @param name The name of the resource (i.e. the value normally passed to {@link Class#getResourceAsStream(String)}.
+	 * @return A new reader resource, or <jk>null</jk> if resource could not be found.
+	 * @throws IOException
+	 */
+	public ReaderResource getReaderResource(String name) throws IOException {
+		return getReaderResource(name, false, servlet.getMimetypesFileTypeMap().getContentType(name));
+	}
+
+	/**
+	 * Returns the config file associated with the servlet.
+	 *
+	 * @return The config file associated with the servlet, or <jk>null</jk> if servlet does not have a config file associated with it.
+	 */
+	public ConfigFile getConfig() {
+		if (cf == null)
+			cf = servlet.getConfig().getResolving(getVarResolverSession());
+		return cf;
+	}
+
+	/**
+	 * Returns the localized swagger associated with the servlet.
+	 *
+	 * @return The swagger associated with the servlet.  Never <jk>null</jk>.
+	 */
+	public Swagger getSwagger() {
+		if (swagger == null)
+			swagger = servlet.getSwagger(this);
+		return swagger;
+	}
+
+	/**
+	 * Returns the localized Swagger from the file system.
+	 * <p>
+	 * 	Looks for a file called <js>"{ServletClass}_{locale}.json"</js> in the same package
+	 * 	as this servlet and returns it as a parsed {@link Swagger} object.
+	 * <p>
+	 * 	Returned objects are cached for later quick-lookup.
+	 *
+	 * @return The parsed swagger object, or <jk>null</jk> if the swagger file could not be found.
+	 */
+	protected Swagger getSwaggerFromFile() {
+		if (fileSwagger == null)
+			fileSwagger = servlet.getSwaggerFromFile(this.getLocale());
+		if (fileSwagger == null)
+			fileSwagger = Swagger.NULL;
+		return fileSwagger == Swagger.NULL ? null : fileSwagger;
+	}
+
+	@Override /* Object */
+	public String toString() {
+		StringBuilder sb = new StringBuilder("\n").append(getDescription()).append("\n");
+		sb.append("---Headers---\n");
+		for (Enumeration<String> e = getHeaderNames(); e.hasMoreElements();) {
+			String h = e.nextElement();
+			sb.append("\t").append(h).append(": ").append(getHeader(h)).append("\n");
+		}
+		sb.append("---Default Servlet Headers---\n");
+		for (Map.Entry<String,String> e : defaultServletHeaders.entrySet()) {
+			sb.append("\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n");
+		}
+		if (method.equals("PUT") || method.equals("POST")) {
+			sb.append("---Body---\n");
+			try {
+				sb.append(getBodyAsString()).append("\n");
+			} catch (Exception e1) {
+				sb.append(e1.getLocalizedMessage());
+				servlet.log(WARNING, e1, "Error occurred while trying to read debug input.");
+			}
+		}
+		return sb.toString();
+	}
+
+	//--------------------------------------------------------------------------------
+	// Utility methods
+	//--------------------------------------------------------------------------------
+
+	private <T> T parseParameter(String val, ClassMeta<T> c) throws ParseException {
+		if (val == null)
+			return null;
+		// Shortcut - If we're returning a string and the value doesn't start with '$' or '(', then
+		// just return the string since it's a plain value.
+		if (c.getInnerClass() == String.class && val.length() > 0) {
+			char x = val.charAt(0);
+			if (x != '(' && x != '$' && x != '\u0000' && val.indexOf('~') == -1)
+				return (T)val;
+		}
+		return urlEncodingParser.parseParameter(val, c);
+	}
+
+	/*
+	 * Converts an Accept-Language value entry to a Locale.
+	 */
+	private Locale toLocale(String lang) {
+      String country = "";
+      int i = lang.indexOf('-');
+      if (i > -1) {
+          country = lang.substring(i+1).trim();
+          lang = lang.substring(0,i).trim();
+      }
+      return new Locale(lang, country);
+	}
+
+	private Encoder getEncoder() {
+		if (encoder == null) {
+			String ce = getHeader("content-encoding");
+			if (! (ce == null || ce.isEmpty())) {
+				ce = ce.trim();
+				encoder = servlet.getEncoders().getEncoder(ce);
+				if (encoder == null)
+					throw new RestException(SC_UNSUPPORTED_MEDIA_TYPE,
+						"Unsupported encoding in request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}",
+						getHeader("content-encoding"), servlet.getEncoders().getSupportedEncodings()
+					);
+			}
+
+			if (encoder != null)
+				contentLength = -1;
+		}
+		// Note that if this is the identity encoder, we want to return null
+		// so that we don't needlessly wrap the input stream.
+		if (encoder == IdentityEncoder.INSTANCE)
+			return null;
+		return encoder;
+	}
+
+	/*
+	 * Returns header value from URL-parameters or set via setHeader() meant
+	 * to override actual header values on the request.
+	 */
+	private String getOverriddenHeader(String name) {
+		String h = null;
+		if (servlet.context.allowHeaderParams)
+			h = getQueryParameter(name);
+		if (h != null)
+			return h;
+		if (overriddenHeaders != null) {
+			h = overriddenHeaders.get(name);
+			if (h != null)
+				return h;
+		}
+		return h;
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/91a388d0/juneau-rest/src/main/java/org/apache/juneau/rest/RestResponse.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestResponse.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestResponse.java
new file mode 100644
index 0000000..0800071
--- /dev/null
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -0,0 +1,425 @@
+// ***************************************************************************************************************************
+// * 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;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.jena.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.xml.*;
+
+/**
+ * Represents an HTTP response for a REST resource.
+ * <p>
+ * Essentially an extended {@link HttpServletResponse} with some special convenience methods
+ * 	that allow you to easily output POJOs as responses.
+ * </p>
+ * <p>
+ * Since this class extends {@link HttpServletResponse}, developers are free to use these
+ * 	convenience methods, or revert to using lower level methods like any other servlet response.
+ * </p>
+ *
+ * <h6 class='topic'>Example:</h6>
+ * <p class='bcode'>
+ * 	<ja>@RestMethod</ja>(name=<js>"GET"</js>)
+ * 	<jk>public void</jk> doGet(RestRequest req, RestResponse res) {
+ * 		res.setProperty(HtmlSerializerContext.<jsf>HTMLDOC_title</jsf>, <js>"My title"</js>)
+ * 			.setOutput(<js>"Simple string response"</js>);
+ * 	}
+ * </p>
+ * <p>
+ * 	Refer to <a class='doclink' href='package-summary.html#TOC'>REST Servlet API</a> for information about using this class.
+ * </p>
+ */
+public final class RestResponse extends HttpServletResponseWrapper {
+
+	private final RestRequest request;
+	private Object output;                               // The POJO being sent to the output.
+	private boolean isNullOutput;                        // The output is null (as opposed to not being set at all)
+	private ObjectMap properties;                        // Response properties
+	SerializerGroup serializerGroup;
+	UrlEncodingSerializer urlEncodingSerializer;         // The serializer used to convert arguments passed into Redirect objects.
+	private EncoderGroup encoders;
+	private RestServlet servlet;
+	private ServletOutputStream os;
+
+	/**
+	 * Constructor.
+	 */
+	RestResponse(RestServlet servlet, RestRequest req, HttpServletResponse res) {
+		super(res);
+		this.request = req;
+		this.servlet = servlet;
+
+		for (Map.Entry<String,Object> e : servlet.getDefaultResponseHeaders().entrySet())
+			setHeader(e.getKey(), e.getValue().toString());
+
+		try {
+			String passThroughHeaders = req.getHeader("x-response-headers");
+			if (passThroughHeaders != null) {
+				ObjectMap m = servlet.getUrlEncodingParser().parseParameter(passThroughHeaders, ObjectMap.class);
+				for (Map.Entry<String,Object> e : m.entrySet())
+					setHeader(e.getKey(), e.getValue().toString());
+			}
+		} catch (Exception e1) {
+			throw new RestException(SC_BAD_REQUEST, "Invalid format for header 'x-response-headers'.  Must be in URL-encoded format.").initCause(e1);
+		}
+	}
+
+	/*
+	 * Called from RestServlet after a match has been made but before the guard or method invocation.
+	 */
+	@SuppressWarnings("hiding")
+	final void init(ObjectMap properties, String defaultCharset, SerializerGroup mSerializers, UrlEncodingSerializer mUrlEncodingSerializer, EncoderGroup encoders) {
+		this.properties = properties;
+		this.serializerGroup = mSerializers;
+		this.urlEncodingSerializer = mUrlEncodingSerializer;
+		this.encoders = encoders;
+
+		// Find acceptable charset
+		String h = request.getHeader("accept-charset");
+		String charset = null;
+		if (h == null)
+			charset = defaultCharset;
+		else for (MediaRange r : MediaRange.parse(h)) {
+			if (r.getQValue() > 0) {
+				if (r.getType().equals("*"))
+					charset = defaultCharset;
+				else if (RestServlet.availableCharsets.containsKey(r.getType()))
+					charset = r.getType();
+				if (charset != null)
+					break;
+			}
+		}
+
+		if (charset == null)
+			throw new RestException(SC_NOT_ACCEPTABLE, "No supported charsets in header ''Accept-Charset'': ''{0}''", request.getHeader("Accept-Charset"));
+		super.setCharacterEncoding(charset);
+	}
+
+	/**
+	 * Gets the serializer group for the response.
+	 *
+	 * @return The serializer group for the response.
+	 */
+	public SerializerGroup getSerializerGroup() {
+		return serializerGroup;
+	}
+
+	/**
+	 * Returns the media types that are valid for <code>Accept</code> headers on the request.
+	 *
+	 * @return The set of media types registered in the parser group of this request.
+	 */
+	public List<String> getSupportedMediaTypes() {
+		return serializerGroup.getSupportedMediaTypes();
+	}
+
+	/**
+	 * Returns the codings that are valid for <code>Accept-Encoding</code> and <code>Content-Encoding</code> headers on the request.
+	 *
+	 * @return The set of media types registered in the parser group of this request.
+	 * @throws RestServletException
+	 */
+	public List<String> getSupportedEncodings() throws RestServletException {
+		return servlet.getEncoders().getSupportedEncodings();
+	}
+
+	/**
+	 * Sets the HTTP output on the response.
+	 * <p>
+	 * 	Calling this method is functionally equivalent to returning the object in the REST Java method.
+	 * <p>
+	 * 	Can be of any of the following types:
+	 * 	<ul>
+	 * 	  <li> {@link InputStream}
+	 * 	  <li> {@link Reader}
+	 * 	  <li> Any serializable type defined in <a href='../../../../overview-summary.html#Core.PojoCategories'>POJO Categories</a>
+	 * 	</ul>
+	 * <p>
+	 * 	If it's an {@link InputStream} or {@link Reader}, you must also specify the <code>Content-Type</code> using the {@link #setContentType(String)} method.
+	 *
+	 * @param output The output to serialize to the connection.
+	 * @return This object (for method chaining).
+	 */
+	public RestResponse setOutput(Object output) {
+		this.output = output;
+		this.isNullOutput = output == null;
+		return this;
+	}
+
+	/**
+	 * Add a serializer property to send to the serializers to override a default value.
+	 * <p>
+	 * Can be any value specified in the following classes:
+	 * <ul>
+	 * 	<li>{@link SerializerContext}
+	 * 	<li>{@link JsonSerializerContext}
+	 * 	<li>{@link XmlSerializerContext}
+	 * 	<li>{@link RdfSerializerContext}
+	 * </ul>
+	 *
+	 * @param key The setting name.
+	 * @param value The setting value.
+	 * @return This object (for method chaining).
+	 */
+	public RestResponse setProperty(String key, Object value) {
+		properties.put(key, value);
+		return this;
+	}
+
+	/**
+	 * Returns the properties set via {@link #setProperty(String, Object)}.
+	 *
+	 * @return A map of all the property values set.
+	 */
+	public ObjectMap getProperties() {
+		return properties;
+	}
+
+	/**
+	 * Shortcut method that allows you to use varargs to simplify setting array output.
+	 *
+	 * <h6 class='topic'>Example:</h6>
+	 * <p class='bcode'>
+	 * 	<jc>// Instead of...</jc>
+	 * 	response.setOutput(<jk>new</jk> Object[]{x,y,z});
+	 *
+	 * 	<jc>// ...call this...</jc>
+	 * 	response.setOutput(x,y,z);
+	 * </p>
+	 *
+	 * @param output The output to serialize to the connection.
+	 * @return This object (for method chaining).
+	 */
+	public RestResponse setOutputs(Object...output) {
+		this.output = output;
+		return this;
+	}
+
+	/**
+	 * Returns the output that was set by calling {@link #setOutput(Object)}.
+	 *
+	 * @return The output object.
+	 */
+	public Object getOutput() {
+		return output;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if this response has any output associated with it.
+	 *
+	 * @return <jk>true</jk> if {@code setInput()} has been called.
+	 */
+	public boolean hasOutput() {
+		return output != null || isNullOutput;
+	}
+
+	/**
+	 * Sets the output to a plain-text message regardless of the content type.
+	 *
+	 * @param text The output text to send.
+	 * @return This object (for method chaining).
+	 * @throws IOException If a problem occurred trying to write to the writer.
+	 */
+	public RestResponse sendPlainText(String text) throws IOException {
+		setContentType("text/plain");
+		getNegotiatedWriter().write(text);
+		return this;
+	}
+
+	/**
+	 * Equivalent to {@link HttpServletResponse#getOutputStream()}, except
+	 * 	wraps the output stream if an {@link Encoder} was found that matched
+	 * 	the <code>Accept-Encoding</code> header.
+	 *
+	 * @return A negotiated output stream.
+	 * @throws IOException
+	 */
+	public ServletOutputStream getNegotiatedOutputStream() throws IOException {
+		if (os == null) {
+			Encoder encoder = null;
+
+			String ae = request.getHeader("Accept-Encoding");
+			if (! (ae == null || ae.isEmpty())) {
+				String match = encoders != null ? encoders.findMatch(ae) : null;
+				if (match == null) {
+					// Identity should always match unless "identity;q=0" or "*;q=0" is specified.
+					if (ae.matches(".*(identity|\\*)\\s*;\\s*q\\s*=\\s*(0(?!\\.)|0\\.0).*")) {
+						throw new RestException(SC_NOT_ACCEPTABLE,
+							"Unsupported encoding in request header ''Accept-Encoding'': ''{0}''\n\tSupported codings: {1}",
+							ae, encoders.getSupportedEncodings()
+						);
+					}
+				} else {
+					encoder = encoders.getEncoder(match);
+
+					// Some clients don't recognize identity as an encoding, so don't set it.
+					if (! match.equals("identity"))
+					setHeader("content-encoding", match);
+				}
+			}
+			os = getOutputStream();
+			if (encoder != null) {
+				final OutputStream os2 = encoder.getOutputStream(os);
+				os = new ServletOutputStream(){
+					@Override /* OutputStream */
+					public final void write(byte[] b, int off, int len) throws IOException {
+						os2.write(b, off, len);
+					}
+					@Override /* OutputStream */
+					public final void write(int b) throws IOException {
+						os2.write(b);
+					}
+					@Override /* OutputStream */
+					public final void flush() throws IOException {
+						os2.flush();
+					}
+					@Override /* OutputStream */
+					public final void close() throws IOException {
+						os2.close();
+					}
+				};
+			}
+		}
+		return os;
+	}
+
+	@Override /* ServletResponse */
+	public ServletOutputStream getOutputStream() throws IOException {
+		if (os == null)
+			os = super.getOutputStream();
+		return os;
+	}
+
+	/**
+	 * Returns <jk>true</jk> if {@link #getOutputStream()} has been called.
+	 *
+	 * @return <jk>true</jk> if {@link #getOutputStream()} has been called.
+	 */
+	public boolean getOutputStreamCalled() {
+		return os != null;
+	}
+
+	/**
+	 * Returns the writer to the response body.
+	 * This methods bypasses any specified encoders and returns a regular unbuffered writer.
+	 * Use the {@link #getNegotiatedWriter()} method if you want to use the matched encoder (if any).
+	 */
+	@Override /* ServletResponse */
+	public PrintWriter getWriter() throws IOException {
+		return getWriter(true);
+	}
+
+	/**
+	 * Convenience method meant to be used when rendering directly to a browser with no buffering.
+	 * Sets the header <js>"x-content-type-options=nosniff"</js> so that output is rendered
+	 * immediately on IE and Chrome without any buffering for content-type sniffing.
+	 *
+	 * @param contentType The value to set as the <code>Content-Type</code> on the response.
+	 * @return The raw writer.
+	 * @throws IOException
+	 */
+	public PrintWriter getDirectWriter(String contentType) throws IOException {
+		setContentType(contentType);
+		setHeader("x-content-type-options", "nosniff");
+		return getWriter();
+	}
+
+	/**
+	 * Equivalent to {@link HttpServletResponse#getWriter()}, except
+	 * 	wraps the output stream if an {@link Encoder} was found that matched
+	 * 	the <code>Accept-Encoding</code> header and sets the <code>Content-Encoding</code>
+	 * 	header to the appropriate value.
+	 *
+	 * @return The negotiated writer.
+	 * @throws IOException
+	 */
+	public PrintWriter getNegotiatedWriter() throws IOException {
+		return getWriter(false);
+	}
+
+	private PrintWriter getWriter(boolean raw) throws IOException {
+		// If plain text requested, override it now.
+		if (request.isPlainText()) {
+			setHeader("Content-Type", "text/plain");
+		}
+
+		try {
+			OutputStream out = (raw ? getOutputStream() : getNegotiatedOutputStream());
+			return new PrintWriter(new OutputStreamWriter(out, getCharacterEncoding()));
+		} catch (UnsupportedEncodingException e) {
+			String ce = getCharacterEncoding();
+			setCharacterEncoding("UTF-8");
+			throw new RestException(SC_NOT_ACCEPTABLE, "Unsupported charset in request header ''Accept-Charset'': ''{0}''", ce);
+		}
+	}
+
+	/**
+	 * Returns the <code>Content-Type</code> header stripped of the charset attribute if present.
+	 *
+	 * @return The <code>media-type</code> portion of the <code>Content-Type</code> header.
+	 */
+	public String getMediaType() {
+		String contentType = getContentType();
+		if (contentType == null)
+			return null;
+		int i = contentType.indexOf(';');
+		if (i == -1)
+			return contentType;
+		return contentType.substring(0, i).trim();
+
+	}
+
+	/**
+	 * Redirects to the specified URI.
+	 * <p>
+	 * Relative URIs are always interpreted as relative to the context root.
+	 * This is similar to how WAS handles redirect requests, and is different from how Tomcat
+	 * 	handles redirect requests.
+	 */
+	@Override /* ServletResponse */
+	public void sendRedirect(String uri) throws IOException {
+		char c = (uri.length() > 0 ? uri.charAt(0) : 0);
+		if (c != '/' && uri.indexOf("://") == -1)
+			uri = request.getContextPath() + '/' + uri;
+		super.sendRedirect(uri);
+	}
+
+	/**
+	 * Returns the URL-encoding serializer associated with this response.
+	 *
+	 * @return The URL-encoding serializer associated with this response.
+	 */
+	public UrlEncodingSerializer getUrlEncodingSerializer() {
+		return urlEncodingSerializer;
+	}
+
+	@Override /* ServletResponse */
+	public void setHeader(String name, String value) {
+		// Jetty doesn't set the content type correctly if set through this method.
+		// Tomcat/WAS does.
+		if (name.equalsIgnoreCase("Content-Type"))
+			super.setContentType(value);
+		else
+			super.setHeader(name, value);
+	}
+}


Mime
View raw message