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> => <jk>null</jk> + * <li><js>"$s(foo)"</js> => <js>"foo"</js> + * <li><js>"(foo)"</js> => <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<Map<String,Integer>> cm = request.getBeanContext().getMapClassMeta(TreeMap.<jk>class</jk>, String.<jk>class</jk>, Integer.<jk>class</jk>); + * Map<String,Integer> m = request.getFormDataParameter(<js>"myParameter"</js>, cm, <jk>new</jk> TreeMap<String,Integer>()); + * </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<Map<String,Integer>> cm = request.getBeanContext().getMapClassMeta(TreeMap.<jk>class</jk>, String.<jk>class</jk>, Integer.<jk>class</jk>); + * Map<String,Integer> 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><object></xt>...<xt></object></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link ObjectMap}</td> + * </tr> + * <tr> + * <td>array</td> + * <td><js>"[...]"</js></td> + * <td><code><xt><array></xt>...<xt></array></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link ObjectList}</td> + * </tr> + * <tr> + * <td>string</td> + * <td><js>"'...'"</js></td> + * <td><code><xt><string></xt>...<xt></string></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link String}</td> + * </tr> + * <tr> + * <td>number</td> + * <td><code>123</code></td> + * <td><code><xt><number></xt>123<xt></number></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link Number}</td> + * </tr> + * <tr> + * <td>boolean</td> + * <td><jk>true</jk></td> + * <td><code><xt><boolean></xt>true<xt></boolean></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>...<xt></x></xt></code></td> + * <td>{@link Boolean}</td> + * </tr> + * <tr> + * <td>null</td> + * <td><jk>null</jk> or blank</td> + * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></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); + } +}
