Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,948 @@
+// 
***************************************************************************************************************************
+// * 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 javax.servlet.http.HttpServletResponse.*;
+import static org.apache.juneau.dto.swagger.SwaggerBuilder.*;
+import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.Utils.*;
+import static org.apache.juneau.rest.RestContext.*;
+import static org.apache.juneau.rest.RestUtils.*;
+import static org.apache.juneau.rest.annotation.Inherit.*;
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import javax.servlet.http.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.dto.swagger.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.html.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.widget.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.urlencoding.*;
+
+/**
+ * Represents a single Java servlet/resource method annotated with {@link 
RestMethod @RestMethod}.
+ */
+@SuppressWarnings("hiding")
+class CallMethod implements Comparable<CallMethod>  {
+       private final java.lang.reflect.Method method;
+       private final String httpMethod;
+       private final UrlPathPattern pathPattern;
+       private final RestParam[] params;
+       private final RestGuard[] guards;
+       private final RestMatcher[] optionalMatchers;
+       private final RestMatcher[] requiredMatchers;
+       private final RestConverter[] converters;
+       private final SerializerGroup serializers;
+       private final ParserGroup parsers;
+       private final EncoderGroup encoders;
+       private final UrlEncodingParser urlEncodingParser;
+       private final UrlEncodingSerializer urlEncodingSerializer;
+       private final ObjectMap properties;
+       private final Map<String,String> defaultRequestHeaders, defaultQuery, 
defaultFormData;
+       private final String defaultCharset;
+       private final boolean deprecated;
+       private final String description, tags, summary, externalDocs;
+       private final Integer priority;
+       private final org.apache.juneau.rest.annotation.Parameter[] parameters;
+       private final Response[] responses;
+       private final RestContext context;
+       private final BeanContext beanContext;
+       final String htmlHeader, htmlNav, htmlAside, htmlFooter, htmlStyle, 
htmlStylesheet, htmlScript, htmlNoResultsMessage;
+       final String[] htmlLinks;
+       final boolean htmlNoWrap;
+       final HtmlDocTemplate htmlTemplate;
+       private final Map<String,Widget> widgets;
+
+       CallMethod(Object servlet, java.lang.reflect.Method method, RestContext 
context) throws RestServletException {
+               Builder b = new Builder(servlet, method, context);
+               this.context = context;
+               this.method = method;
+               this.httpMethod = b.httpMethod;
+               this.pathPattern = b.pathPattern;
+               this.params = b.params;
+               this.guards = b.guards;
+               this.optionalMatchers = b.optionalMatchers;
+               this.requiredMatchers = b.requiredMatchers;
+               this.converters = b.converters;
+               this.serializers = b.serializers;
+               this.parsers = b.parsers;
+               this.encoders = b.encoders;
+               this.urlEncodingParser = b.urlEncodingParser;
+               this.urlEncodingSerializer = b.urlEncodingSerializer;
+               this.beanContext = b.beanContext;
+               this.properties = b.properties;
+               this.defaultRequestHeaders = b.defaultRequestHeaders;
+               this.defaultQuery = b.defaultQuery;
+               this.defaultFormData = b.defaultFormData;
+               this.defaultCharset = b.defaultCharset;
+               this.deprecated = b.deprecated;
+               this.description = b.description;
+               this.tags = b.tags;
+               this.summary = b.summary;
+               this.externalDocs = b.externalDocs;
+               this.priority = b.priority;
+               this.parameters = b.parameters;
+               this.responses = b.responses;
+               this.htmlHeader = b.htmlHeader;
+               this.htmlLinks = b.htmlLinks;
+               this.htmlNav = b.htmlNav;
+               this.htmlAside = b.htmlAside;
+               this.htmlFooter = b.htmlFooter;
+               this.htmlStyle = b.htmlStyle;
+               this.htmlStylesheet = b.htmlStylesheet;
+               this.htmlScript = b.htmlScript;
+               this.htmlNoWrap = b.htmlNoWrap;
+               this.htmlTemplate = b.htmlTemplate;
+               this.htmlNoResultsMessage = b.htmlNoResultsMessage;
+               this.widgets = Collections.unmodifiableMap(b.htmlWidgets);
+       }
+
+       private static class Builder  {
+               private String httpMethod, defaultCharset, description, tags, 
summary, externalDocs, htmlNav, htmlAside,
+                       htmlFooter, htmlStyle, htmlStylesheet, htmlScript, 
htmlHeader, htmlNoResultsMessage;
+               private String[] htmlLinks;
+               private boolean htmlNoWrap;
+               private HtmlDocTemplate htmlTemplate;
+               private UrlPathPattern pathPattern;
+               private RestParam[] params;
+               private RestGuard[] guards;
+               private RestMatcher[] optionalMatchers, requiredMatchers;
+               private RestConverter[] converters;
+               private SerializerGroup serializers;
+               private ParserGroup parsers;
+               private EncoderGroup encoders;
+               private UrlEncodingParser urlEncodingParser;
+               private UrlEncodingSerializer urlEncodingSerializer;
+               private BeanContext beanContext;
+               private ObjectMap properties;
+               private Map<String,String> defaultRequestHeaders, defaultQuery, 
defaultFormData;
+               private boolean plainParams, deprecated;
+               private Integer priority;
+               private org.apache.juneau.rest.annotation.Parameter[] 
parameters;
+               private Response[] responses;
+               private Map<String,Widget> htmlWidgets;
+
+               private Builder(Object servlet, java.lang.reflect.Method 
method, RestContext context) throws RestServletException {
+                       String sig = method.getDeclaringClass().getName() + '.' 
+ method.getName();
+
+                       try {
+
+                               RestMethod m = 
method.getAnnotation(RestMethod.class);
+                               if (m == null)
+                                       throw new 
RestServletException("@RestMethod annotation not found on method ''{0}''", sig);
+
+                               if (! m.description().isEmpty())
+                                       description = m.description();
+                               MethodSwagger sm = m.swagger();
+                               if (! sm.tags().isEmpty())
+                                       tags = sm.tags();
+                               if (! m.summary().isEmpty())
+                                       summary = m.summary();
+                               if (! sm.externalDocs().isEmpty())
+                                       externalDocs = sm.externalDocs();
+                               deprecated = sm.deprecated();
+                               parameters = sm.parameters();
+                               responses = sm.responses();
+                               serializers = context.getSerializers();
+                               parsers = context.getParsers();
+                               urlEncodingSerializer = 
context.getUrlEncodingSerializer();
+                               urlEncodingParser = 
context.getUrlEncodingParser();
+                               beanContext = context.getBeanContext();
+                               encoders = context.getEncoders();
+                               properties = context.getProperties();
+
+                               HtmlDoc hd = m.htmldoc();
+                               htmlWidgets = new 
HashMap<String,Widget>(context.getHtmlWidgets());
+                               for (Class<? extends Widget> wc : hd.widgets()) 
{
+                                       Widget w = 
ClassUtils.newInstance(Widget.class, wc);
+                                       htmlWidgets.put(w.getName(), w);
+                               }
+
+                               htmlHeader = 
resolveNewlineSeparatedAnnotation(hd.header(), context.getHtmlHeader());
+                               htmlNav = 
resolveNewlineSeparatedAnnotation(hd.nav(), context.getHtmlNav());
+                               htmlAside = 
resolveNewlineSeparatedAnnotation(hd.aside(), context.getHtmlAside());
+                               htmlFooter = 
resolveNewlineSeparatedAnnotation(hd.footer(), context.getHtmlFooter());
+                               htmlStyle = 
resolveNewlineSeparatedAnnotation(hd.style(), context.getHtmlStyle());
+                               htmlScript = 
resolveNewlineSeparatedAnnotation(hd.script(), context.getHtmlScript());
+                               htmlLinks = resolveLinks(hd.links(), 
context.getHtmlLinks());
+                               htmlStylesheet = hd.stylesheet().isEmpty() ? 
context.getHtmlStylesheet() : hd.stylesheet();
+                               htmlNoWrap = hd.nowrap() ? hd.nowrap() : 
context.getHtmlNoWrap();
+                               htmlNoResultsMessage = 
hd.noResultsMessage().isEmpty() ? context.getHtmlNoResultsMessage() : 
hd.noResultsMessage();
+                               htmlTemplate =
+                                       hd.template() == HtmlDocTemplate.class
+                                       ? context.getHtmlTemplate()
+                                       : 
ClassUtils.newInstance(HtmlDocTemplate.class, hd.template());
+
+                               List<Inherit> si = 
Arrays.asList(m.serializersInherit());
+                               List<Inherit> pi = 
Arrays.asList(m.parsersInherit());
+
+                               SerializerGroupBuilder sgb = null;
+                               ParserGroupBuilder pgb = null;
+                               UrlEncodingParserBuilder uepb = null;
+
+                               if (m.serializers().length > 0 || 
m.parsers().length > 0 || m.properties().length > 0 || m.flags().length > 0
+                                               || m.beanFilters().length > 0 
|| m.pojoSwaps().length > 0 || m.bpi().length > 0
+                                               || m.bpx().length > 0) {
+                                       sgb = new SerializerGroupBuilder();
+                                       pgb = new ParserGroupBuilder();
+                                       uepb = new 
UrlEncodingParserBuilder(urlEncodingParser.createPropertyStore());
+
+                                       if (si.contains(SERIALIZERS) || 
m.serializers().length == 0)
+                                               
sgb.append(serializers.getSerializers());
+
+                                       if (pi.contains(PARSERS) || 
m.parsers().length == 0)
+                                               
pgb.append(parsers.getParsers());
+                               }
+
+                               httpMethod = 
m.name().toUpperCase(Locale.ENGLISH);
+                               if (httpMethod.equals("") && 
method.getName().startsWith("do"))
+                                       httpMethod = 
method.getName().substring(2).toUpperCase(Locale.ENGLISH);
+                               if (httpMethod.equals(""))
+                                       httpMethod = "GET";
+                               if (httpMethod.equals("METHOD"))
+                                       httpMethod = "*";
+
+                               priority = m.priority();
+
+                               String p = m.path();
+                               converters = new 
RestConverter[m.converters().length];
+                               for (int i = 0; i < converters.length; i++)
+                                       converters[i] = 
newInstance(RestConverter.class, m.converters()[i]);
+
+                               guards = new RestGuard[m.guards().length];
+                               for (int i = 0; i < guards.length; i++)
+                                       guards[i] = 
newInstance(RestGuard.class, m.guards()[i]);
+
+                               List<RestMatcher> optionalMatchers = new 
LinkedList<RestMatcher>(), requiredMatchers = new LinkedList<RestMatcher>();
+                               for (int i = 0; i < m.matchers().length; i++) {
+                                       Class<? extends RestMatcher> c = 
m.matchers()[i];
+                                       RestMatcher matcher = null;
+                                       if 
(isParentClass(RestMatcherReflecting.class, c))
+                                               matcher = 
newInstance(RestMatcherReflecting.class, c, servlet, method);
+                                       else
+                                               matcher = 
newInstance(RestMatcher.class, c);
+                                       if (matcher.mustMatch())
+                                               requiredMatchers.add(matcher);
+                                       else
+                                               optionalMatchers.add(matcher);
+                               }
+                               if (! m.clientVersion().isEmpty())
+                                       requiredMatchers.add(new 
ClientVersionMatcher(context.getClientVersionHeader(), method));
+
+                               this.requiredMatchers = 
requiredMatchers.toArray(new RestMatcher[requiredMatchers.size()]);
+                               this.optionalMatchers = 
optionalMatchers.toArray(new RestMatcher[optionalMatchers.size()]);
+
+                               Class<?>[] beanFilters = 
context.getBeanFilters(), pojoSwaps = context.getPojoSwaps();
+
+                               if (sgb != null) {
+                                       sgb.append(m.serializers());
+                                       if (si.contains(TRANSFORMS))
+                                               
sgb.beanFilters(beanFilters).pojoSwaps(pojoSwaps);
+                                       if (si.contains(PROPERTIES))
+                                               sgb.properties(properties);
+                                       for (Property p1 : m.properties())
+                                               sgb.property(p1.name(), 
p1.value());
+                                       for (String p1 : m.flags())
+                                               sgb.property(p1, true);
+                                       if (m.bpi().length > 0) {
+                                               Map<String,String> bpiMap = new 
LinkedHashMap<String,String>();
+                                               for (String s : m.bpi()) {
+                                                       for (String s2 : 
split(s, ';')) {
+                                                               int i = 
s2.indexOf(':');
+                                                               if (i == -1)
+                                                                       throw 
new RestServletException(
+                                                                               
"Invalid format for @RestMethod.bpi() on method ''{0}''.  Must be in the format 
\"ClassName: comma-delimited-tokens\".  \nValue: {1}", sig, s);
+                                                               
bpiMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim());
+                                                       }
+                                               }
+                                               sgb.includeProperties(bpiMap);
+                                       }
+                                       if (m.bpx().length > 0) {
+                                               Map<String,String> bpxMap = new 
LinkedHashMap<String,String>();
+                                               for (String s : m.bpx()) {
+                                                       for (String s2 : 
split(s, ';')) {
+                                                               int i = 
s2.indexOf(':');
+                                                               if (i == -1)
+                                                                       throw 
new RestServletException(
+                                                                               
"Invalid format for @RestMethod.bpx() on method ''{0}''.  Must be in the format 
\"ClassName: comma-delimited-tokens\".  \nValue: {1}", sig, s);
+                                                               
bpxMap.put(s2.substring(0, i).trim(), s2.substring(i+1).trim());
+                                                       }
+                                               }
+                                               sgb.excludeProperties(bpxMap);
+                                       }
+                                       sgb.beanFilters(m.beanFilters());
+                                       sgb.pojoSwaps(m.pojoSwaps());
+                               }
+
+                               if (pgb != null) {
+                                       pgb.append(m.parsers());
+                                       if (pi.contains(TRANSFORMS))
+                                               
pgb.beanFilters(beanFilters).pojoSwaps(pojoSwaps);
+                                       if (pi.contains(PROPERTIES))
+                                               pgb.properties(properties);
+                                       for (Property p1 : m.properties())
+                                               pgb.property(p1.name(), 
p1.value());
+                                       for (String p1 : m.flags())
+                                               pgb.property(p1, true);
+                                       pgb.beanFilters(m.beanFilters());
+                                       pgb.pojoSwaps(m.pojoSwaps());
+                               }
+
+                               if (uepb != null) {
+                                       for (Property p1 : m.properties())
+                                               uepb.property(p1.name(), 
p1.value());
+                                       for (String p1 : m.flags())
+                                               uepb.property(p1, true);
+                                       uepb.beanFilters(m.beanFilters());
+                                       uepb.pojoSwaps(m.pojoSwaps());
+                               }
+
+                               if (m.properties().length > 0 || 
m.flags().length > 0) {
+                                       properties = new 
ObjectMap().setInner(properties);
+                                       for (Property p1 : m.properties())
+                                               properties.put(p1.name(), 
p1.value());
+                                       for (String p1 : m.flags())
+                                               properties.put(p1, true);
+                               }
+
+                               if (m.encoders().length > 0 || ! 
m.inheritEncoders()) {
+                                       EncoderGroupBuilder g = new 
EncoderGroupBuilder();
+                                       if (m.inheritEncoders())
+                                               g.append(encoders);
+                                       else
+                                               
g.append(IdentityEncoder.INSTANCE);
+
+                                       for (Class<? extends Encoder> c : 
m.encoders()) {
+                                               try {
+                                                       g.append(c);
+                                               } catch (Exception e) {
+                                                       throw new 
RestServletException(
+                                                               "Exception 
occurred while trying to instantiate Encoder on method ''{0}'': ''{1}''", sig, 
c.getSimpleName()).initCause(e);
+                                               }
+                                       }
+                                       encoders = g.build();
+                               }
+
+                               defaultRequestHeaders = new 
TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+                               for (String s : m.defaultRequestHeaders()) {
+                                       String[] h = 
RestUtils.parseKeyValuePair(s);
+                                       if (h == null)
+                                               throw new RestServletException(
+                                                       "Invalid default 
request header specified on method ''{0}'': ''{1}''.  Must be in the format: 
''name[:=]value''", sig, s);
+                                       defaultRequestHeaders.put(h[0], h[1]);
+                               }
+
+                               defaultQuery = new 
LinkedHashMap<String,String>();
+                               for (String s : m.defaultQuery()) {
+                                       String[] h = 
RestUtils.parseKeyValuePair(s);
+                                       if (h == null)
+                                               throw new RestServletException(
+                                                       "Invalid default query 
parameter specified on method ''{0}'': ''{1}''.  Must be in the format: 
''name[:=]value''", sig, s);
+                                       defaultQuery.put(h[0], h[1]);
+                               }
+
+                               defaultFormData = new 
LinkedHashMap<String,String>();
+                               for (String s : m.defaultFormData()) {
+                                       String[] h = 
RestUtils.parseKeyValuePair(s);
+                                       if (h == null)
+                                               throw new RestServletException(
+                                                       "Invalid default form 
data parameter specified on method ''{0}'': ''{1}''.  Must be in the format: 
''name[:=]value''", sig, s);
+                                       defaultFormData.put(h[0], h[1]);
+                               }
+
+                               Type[] pt = method.getGenericParameterTypes();
+                               Annotation[][] pa = 
method.getParameterAnnotations();
+                               for (int i = 0; i < pt.length; i++) {
+                                       for (Annotation a : pa[i]) {
+                                               if (a instanceof Header) {
+                                                       Header h = (Header)a;
+                                                       if (! h.def().isEmpty())
+                                                               
defaultRequestHeaders.put(firstNonEmpty(h.name(), h.value()), h.def());
+                                               } else if (a instanceof Query) {
+                                                       Query q = (Query)a;
+                                                       if (! q.def().isEmpty())
+                                                               
defaultQuery.put(firstNonEmpty(q.name(), q.value()), q.def());
+                                               } else if (a instanceof 
FormData) {
+                                                       FormData f = 
(FormData)a;
+                                                       if (! f.def().isEmpty())
+                                                               
defaultFormData.put(firstNonEmpty(f.name(), f.value()), f.def());
+                                               }
+                                       }
+                               }
+
+                               defaultCharset = 
properties.getString(REST_defaultCharset, context.getDefaultCharset());
+                               String paramFormat = 
properties.getString(REST_paramFormat, context.getParamFormat());
+                               plainParams = paramFormat.equals("PLAIN");
+
+                               pathPattern = new UrlPathPattern(p);
+
+                               params = context.findParams(method, 
plainParams, pathPattern, false);
+
+                               if (sgb != null) {
+                                       serializers = sgb.build();
+                                       beanContext = 
serializers.getBeanContext();
+                               }
+                               if (pgb != null)
+                                       parsers = pgb.build();
+                               if (uepb != null)
+                                       urlEncodingParser = uepb.build();
+
+                               // Need this to access methods in anonymous 
inner classes.
+                               method.setAccessible(true);
+                       } catch (RestServletException e) {
+                               throw e;
+                       } catch (Exception e) {
+                               throw new RestServletException("Exception 
occurred while initializing method ''{0}''", sig).initCause(e);
+                       }
+               }
+       }
+
+       /**
+        * Returns <jk>true</jk> if this Java method has any guards or matchers.
+        */
+       boolean hasGuardsOrMatchers() {
+               return (guards.length != 0 || requiredMatchers.length != 0 || 
optionalMatchers.length != 0);
+       }
+
+       /**
+        * Returns the HTTP method name (e.g. <js>"GET"</js>).
+        */
+       String getHttpMethod() {
+               return httpMethod;
+       }
+
+       /**
+        * Returns the path pattern for this method.
+        */
+       String getPathPattern() {
+               return pathPattern.toString();
+       }
+
+       /**
+        * Returns the localized Swagger for this Java method.
+        */
+       Operation getSwaggerOperation(RestRequest req) throws ParseException {
+               Operation o = operation()
+                       .operationId(method.getName())
+                       .description(getDescription(req))
+                       .tags(getTags(req))
+                       .summary(getSummary(req))
+                       .externalDocs(getExternalDocs(req))
+                       .parameters(getParameters(req))
+                       .responses(getResponses(req));
+
+               if (isDeprecated())
+                       o.deprecated(true);
+
+               if (! 
parsers.getSupportedMediaTypes().equals(context.getParsers().getSupportedMediaTypes()))
+                       o.consumes(parsers.getSupportedMediaTypes());
+
+               if (! 
serializers.getSupportedMediaTypes().equals(context.getSerializers().getSupportedMediaTypes()))
+                       o.produces(serializers.getSupportedMediaTypes());
+
+               return o;
+       }
+
+       private Operation getSwaggerOperationFromFile(RestRequest req) {
+               Swagger s = req.getSwaggerFromFile();
+               if (s != null && s.getPaths() != null && 
s.getPaths().get(pathPattern.getPatternString()) != null)
+                       return 
s.getPaths().get(pathPattern.getPatternString()).get(httpMethod);
+               return null;
+       }
+
+       /**
+        * Returns the localized summary for this Java method.
+        */
+       String getSummary(RestRequest req) {
+               VarResolverSession vr = req.getVarResolverSession();
+               if (summary != null)
+                       return vr.resolve(summary);
+               String summary = 
context.getMessages().findFirstString(req.getLocale(), method.getName() + 
".summary");
+               if (summary != null)
+                       return vr.resolve(summary);
+               Operation o = getSwaggerOperationFromFile(req);
+               if (o != null)
+                       return o.getSummary();
+               return null;
+       }
+
+       /**
+        * Returns the localized description for this Java method.
+        */
+       String getDescription(RestRequest req) {
+               VarResolverSession vr = req.getVarResolverSession();
+               if (description != null)
+                       return vr.resolve(description);
+               String description = 
context.getMessages().findFirstString(req.getLocale(), method.getName() + 
".description");
+               if (description != null)
+                       return vr.resolve(description);
+               Operation o = getSwaggerOperationFromFile(req);
+               if (o != null)
+                       return o.getDescription();
+               return null;
+       }
+
+       /**
+        * Returns the localized Swagger tags for this Java method.
+        */
+       private List<String> getTags(RestRequest req) {
+               VarResolverSession vr = req.getVarResolverSession();
+               JsonParser jp = JsonParser.DEFAULT;
+               try {
+                       if (tags != null)
+                               return jp.parse(vr.resolve(tags), 
ArrayList.class, String.class);
+                       String tags = 
context.getMessages().findFirstString(req.getLocale(), method.getName() + 
".tags");
+                       if (tags != null)
+                               return jp.parse(vr.resolve(tags), 
ArrayList.class, String.class);
+                       Operation o = getSwaggerOperationFromFile(req);
+                       if (o != null)
+                               return o.getTags();
+                       return null;
+               } catch (Exception e) {
+                       throw new RestException(SC_INTERNAL_SERVER_ERROR, e);
+               }
+       }
+
+       /**
+        * Returns the localized Swagger external docs for this Java method.
+        */
+       private ExternalDocumentation getExternalDocs(RestRequest req) {
+               VarResolverSession vr = req.getVarResolverSession();
+               JsonParser jp = JsonParser.DEFAULT;
+               try {
+                       if (externalDocs != null)
+                               return jp.parse(vr.resolve(externalDocs), 
ExternalDocumentation.class);
+                       String externalDocs = 
context.getMessages().findFirstString(req.getLocale(), method.getName() + 
".externalDocs");
+                       if (externalDocs != null)
+                               return jp.parse(vr.resolve(externalDocs), 
ExternalDocumentation.class);
+                       Operation o = getSwaggerOperationFromFile(req);
+                       if (o != null)
+                               return o.getExternalDocs();
+                       return null;
+               } catch (Exception e) {
+                       throw new RestException(SC_INTERNAL_SERVER_ERROR, e);
+               }
+       }
+
+       /**
+        * Returns the Swagger deprecated flag for this Java method.
+        */
+       private boolean isDeprecated() {
+               return deprecated;
+       }
+
+       /**
+        * Returns the localized Swagger parameter information for this Java 
method.
+        */
+       private List<ParameterInfo> getParameters(RestRequest req) throws 
ParseException {
+               Operation o = getSwaggerOperationFromFile(req);
+               if (o != null && o.getParameters() != null)
+                       return o.getParameters();
+
+               VarResolverSession vr = req.getVarResolverSession();
+               JsonParser jp = JsonParser.DEFAULT;
+               Map<String,ParameterInfo> m = new 
TreeMap<String,ParameterInfo>();
+
+               // First parse @RestMethod.parameters() annotation.
+               for (org.apache.juneau.rest.annotation.Parameter v : 
parameters) {
+                       String in = vr.resolve(v.in());
+                       ParameterInfo p = parameterInfo(in, 
vr.resolve(v.name()));
+
+                       if (! v.description().isEmpty())
+                               p.description(vr.resolve(v.description()));
+                       if (v.required())
+                               p.required(v.required());
+
+                       if ("body".equals(in)) {
+                               if (! v.schema().isEmpty())
+                                       
p.schema(jp.parse(vr.resolve(v.schema()), SchemaInfo.class));
+                       } else {
+                               if (v.allowEmptyValue())
+                                       p.allowEmptyValue(v.allowEmptyValue());
+                               if (! v.collectionFormat().isEmpty())
+                                       
p.collectionFormat(vr.resolve(v.collectionFormat()));
+                               if (! v._default().isEmpty())
+                                       p._default(vr.resolve(v._default()));
+                               if (! v.format().isEmpty())
+                                       p.format(vr.resolve(v.format()));
+                               if (! v.items().isEmpty())
+                                       p.items(jp.parse(vr.resolve(v.items()), 
Items.class));
+                               p.type(vr.resolve(v.type()));
+                       }
+                       m.put(p.getIn() + '.' + p.getName(), p);
+               }
+
+               // Next, look in resource bundle.
+               String prefix = method.getName() + ".req";
+               for (String key : context.getMessages().keySet(prefix)) {
+                       if (key.length() > prefix.length()) {
+                               String value = 
vr.resolve(context.getMessages().getString(key));
+                               String[] parts = key.substring(prefix.length() 
+ 1).split("\\.");
+                               String in = parts[0], name, field;
+                               boolean isBody = "body".equals(in);
+                               if (parts.length == (isBody ? 2 : 3)) {
+                                       if ("body".equals(in)) {
+                                               name = null;
+                                               field = parts[1];
+                                       } else {
+                                               name = parts[1];
+                                               field = parts[2];
+                                       }
+                                       String k2 = in + '.' + name;
+                                       ParameterInfo p = m.get(k2);
+                                       if (p == null) {
+                                               p = parameterInfoStrict(in, 
name);
+                                               m.put(k2, p);
+                                       }
+
+                                       if (field.equals("description"))
+                                               p.description(value);
+                                       else if (field.equals("required"))
+                                               
p.required(Boolean.valueOf(value));
+
+                                       if ("body".equals(in)) {
+                                               if (field.equals("schema"))
+                                                       
p.schema(jp.parse(value, SchemaInfo.class));
+                                       } else {
+                                               if 
(field.equals("allowEmptyValue"))
+                                                       
p.allowEmptyValue(Boolean.valueOf(value));
+                                               else if 
(field.equals("collectionFormat"))
+                                                       
p.collectionFormat(value);
+                                               else if 
(field.equals("default"))
+                                                       p._default(value);
+                                               else if (field.equals("format"))
+                                                       p.format(value);
+                                               else if (field.equals("items"))
+                                                       p.items(jp.parse(value, 
Items.class));
+                                               else if (field.equals("type"))
+                                                       p.type(value);
+                                       }
+                               } else {
+                                       System.err.println("Unknown bundle key 
'"+key+"'");
+                               }
+                       }
+               }
+
+               // Finally, look for parameters defined on method.
+               for (RestParam mp : this.params) {
+                       RestParamType in = mp.getParamType();
+                       if (in != RestParamType.OTHER) {
+                               String k2 = in.toString() + '.' + (in == 
RestParamType.BODY ? null : mp.getName());
+                               ParameterInfo p = m.get(k2);
+                               if (p == null) {
+                                       p = parameterInfoStrict(in.toString(), 
mp.getName());
+                                       m.put(k2, p);
+                               }
+                       }
+               }
+
+               if (m.isEmpty())
+                       return null;
+               return new ArrayList<ParameterInfo>(m.values());
+       }
+
+       /**
+        * Returns the localized Swagger response information about this Java 
method.
+        */
+       @SuppressWarnings("unchecked")
+       private Map<Integer,ResponseInfo> getResponses(RestRequest req) throws 
ParseException {
+               Operation o = getSwaggerOperationFromFile(req);
+               if (o != null && o.getResponses() != null)
+                       return o.getResponses();
+
+               VarResolverSession vr = req.getVarResolverSession();
+               JsonParser jp = JsonParser.DEFAULT;
+               Map<Integer,ResponseInfo> m = new 
TreeMap<Integer,ResponseInfo>();
+               Map<String,HeaderInfo> m2 = new TreeMap<String,HeaderInfo>();
+
+               // First parse @RestMethod.parameters() annotation.
+               for (Response r : responses) {
+                       int httpCode = r.value();
+                       String description = r.description().isEmpty() ? 
RestUtils.getHttpResponseText(r.value()) : vr.resolve(r.description());
+                       ResponseInfo r2 = responseInfo(description);
+
+                       if (r.headers().length > 0) {
+                               for 
(org.apache.juneau.rest.annotation.Parameter v : r.headers()) {
+                                       HeaderInfo h = 
headerInfoStrict(vr.resolve(v.type()));
+                                       if (! v.collectionFormat().isEmpty())
+                                               
h.collectionFormat(vr.resolve(v.collectionFormat()));
+                                       if (! v._default().isEmpty())
+                                               
h._default(vr.resolve(v._default()));
+                                       if (! v.description().isEmpty())
+                                               
h.description(vr.resolve(v.description()));
+                                       if (! v.format().isEmpty())
+                                               
h.format(vr.resolve(v.format()));
+                                       if (! v.items().isEmpty())
+                                               
h.items(jp.parse(vr.resolve(v.items()), Items.class));
+                                       r2.header(v.name(), h);
+                                       m2.put(httpCode + '.' + v.name(), h);
+                               }
+                       }
+                       m.put(httpCode, r2);
+               }
+
+               // Next, look in resource bundle.
+               String prefix = method.getName() + ".res";
+               for (String key : context.getMessages().keySet(prefix)) {
+                       if (key.length() > prefix.length()) {
+                               String value = 
vr.resolve(context.getMessages().getString(key));
+                               String[] parts = key.substring(prefix.length() 
+ 1).split("\\.");
+                               int httpCode = Integer.parseInt(parts[0]);
+                               ResponseInfo r2 = m.get(httpCode);
+                               if (r2 == null) {
+                                       r2 = responseInfo(null);
+                                       m.put(httpCode, r2);
+                               }
+
+                               String name = parts.length > 1 ? parts[1] : "";
+
+                               if ("header".equals(name) && parts.length > 3) {
+                                       String headerName = parts[2];
+                                       String field = parts[3];
+
+                                       String k2 = httpCode + '.' + headerName;
+                                       HeaderInfo h = m2.get(k2);
+                                       if (h == null) {
+                                               h = headerInfoStrict("string");
+                                               m2.put(k2, h);
+                                               r2.header(name, h);
+                                       }
+                                       if (field.equals("collectionFormat"))
+                                               h.collectionFormat(value);
+                                       else if (field.equals("default"))
+                                               h._default(value);
+                                       else if (field.equals("description"))
+                                               h.description(value);
+                                       else if (field.equals("format"))
+                                               h.format(value);
+                                       else if (field.equals("items"))
+                                               h.items(jp.parse(value, 
Items.class));
+                                       else if (field.equals("type"))
+                                               h.type(value);
+
+                               } else if ("description".equals(name)) {
+                                       r2.description(value);
+                               } else if ("schema".equals(name)) {
+                                       r2.schema(jp.parse(value, 
SchemaInfo.class));
+                               } else if ("examples".equals(name)) {
+                                       r2.examples(jp.parse(value, 
TreeMap.class));
+                               } else {
+                                       System.err.println("Unknown bundle key 
'"+key+"'");
+                               }
+                       }
+               }
+
+               return m.isEmpty() ? null : m;
+       }
+
+       /**
+        * Returns <jk>true</jk> if the specified request object can call this 
method.
+        */
+       boolean isRequestAllowed(RestRequest req) {
+               for (RestGuard guard : guards) {
+                       req.setJavaMethod(method);
+                       if (! guard.isRequestAllowed(req))
+                               return false;
+               }
+               return true;
+       }
+
+       /**
+        * Workhorse method.
+        *
+        * @param pathInfo The value of {@link 
HttpServletRequest#getPathInfo()} (sorta)
+        * @return The HTTP response code.
+        */
+       int invoke(String pathInfo, RestRequest req, RestResponse res) throws 
RestException {
+
+               String[] patternVals = pathPattern.match(pathInfo);
+               if (patternVals == null)
+                       return SC_NOT_FOUND;
+
+               String remainder = null;
+               if (patternVals.length > pathPattern.getVars().length)
+                       remainder = patternVals[pathPattern.getVars().length];
+               for (int i = 0; i < pathPattern.getVars().length; i++)
+                       req.getPathMatch().put(pathPattern.getVars()[i], 
patternVals[i]);
+               req.getPathMatch().setRemainder(remainder);
+
+               ObjectMap requestProperties = 
createRequestProperties(properties, req);
+               req.init(method, requestProperties, defaultRequestHeaders, 
defaultQuery, defaultFormData, defaultCharset,
+                       serializers, parsers, urlEncodingParser, beanContext, 
encoders, widgets);
+               res.init(requestProperties, defaultCharset, serializers, 
urlEncodingSerializer, encoders);
+
+               // Class-level guards
+               for (RestGuard guard : context.getGuards())
+                       if (! guard.guard(req, res))
+                               return SC_UNAUTHORIZED;
+
+               // If the method implements matchers, test them.
+               for (RestMatcher m : requiredMatchers)
+                       if (! m.matches(req))
+                               return SC_PRECONDITION_FAILED;
+               if (optionalMatchers.length > 0) {
+                       boolean matches = false;
+                       for (RestMatcher m : optionalMatchers)
+                               matches |= m.matches(req);
+                       if (! matches)
+                               return SC_PRECONDITION_FAILED;
+               }
+
+               context.preCall(req, res);
+
+               Object[] args = new Object[params.length];
+               for (int i = 0; i < params.length; i++) {
+                       try {
+                               args[i] = params[i].resolve(req, res);
+                       } catch (RestException e) {
+                               throw e;
+                       } catch (Exception e) {
+                               throw new RestException(SC_BAD_REQUEST,
+                                       "Invalid data conversion.  Could not 
convert {0} ''{1}'' to type ''{2}'' on method ''{3}.{4}''.",
+                                       params[i].getParamType().name(), 
params[i].getName(), params[i].getType(), method.getDeclaringClass().getName(), 
method.getName()
+                               ).initCause(e);
+                       }
+               }
+
+               try {
+
+                       for (RestGuard guard : guards)
+                               if (! guard.guard(req, res))
+                                       return SC_OK;
+
+                       Object output = method.invoke(context.getResource(), 
args);
+                       if (! method.getReturnType().equals(Void.TYPE))
+                               if (output != null || ! 
res.getOutputStreamCalled())
+                                       res.setOutput(output);
+
+                       context.postCall(req, res);
+
+                       if (res.hasOutput()) {
+                               output = res.getOutput();
+                               for (RestConverter converter : converters)
+                                       output = converter.convert(req, output, 
beanContext.getClassMetaForObject(output));
+                               res.setOutput(output);
+                       }
+               } catch (IllegalArgumentException e) {
+                       throw new RestException(SC_BAD_REQUEST,
+                               "Invalid argument type passed to the following 
method: ''{0}''.\n\tArgument types: {1}",
+                               method.toString(), getReadableClassNames(args)
+                       ).initCause(e);
+               } catch (InvocationTargetException e) {
+                       Throwable e2 = e.getTargetException();          // Get 
the throwable thrown from the doX() method.
+                       if (e2 instanceof RestException)
+                               throw (RestException)e2;
+                       if (e2 instanceof ParseException)
+                               throw new RestException(SC_BAD_REQUEST, e2);
+                       if (e2 instanceof InvalidDataConversionException)
+                               throw new RestException(SC_BAD_REQUEST, e2);
+                       throw new RestException(SC_INTERNAL_SERVER_ERROR, e2);
+               } catch (RestException e) {
+                       throw e;
+               } catch (Exception e) {
+                       throw new RestException(SC_INTERNAL_SERVER_ERROR, e);
+               }
+               return SC_OK;
+       }
+
+       /**
+        * This method creates all the request-time properties.
+        */
+       ObjectMap createRequestProperties(final ObjectMap methodProperties, 
final RestRequest req) {
+               @SuppressWarnings("serial")
+               ObjectMap m = new ObjectMap() {
+                       @Override /* Map */
+                       public Object get(Object key) {
+                               Object o = super.get(key);
+                               if (o == null) {
+                                       String k = key.toString();
+                                       int i = k.indexOf('.');
+                                       if (i != -1) {
+                                               String prefix = k.substring(0, 
i);
+                                               String remainder = 
k.substring(i+1);
+                                               Object v = 
req.resolveProperty(CallMethod.this, prefix, remainder);
+                                               if (v != null)
+                                                       return v;
+                                       }
+                                       o = req.getPathMatch().get(k);
+                                       if (o == null)
+                                               o = req.getHeader(k);
+                               }
+                               if (o instanceof String)
+                                       o = 
req.getVarResolverSession().resolve(o.toString());
+                               return o;
+                       }
+               };
+               m.setInner(methodProperties);
+               return m;
+       }
+
+       @Override /* Object */
+       public String toString() {
+               return "SimpleMethod: name=" + httpMethod + ", path=" + 
pathPattern.getPatternString();
+       }
+
+       /*
+        * compareTo() method is used to keep SimpleMethods ordered in the 
CallRouter list.
+        * It maintains the order in which matches are made during requests.
+        */
+       @Override /* Comparable */
+       public int compareTo(CallMethod o) {
+               int c;
+
+               c = priority.compareTo(o.priority);
+               if (c != 0)
+                       return c;
+
+               c = pathPattern.compareTo(o.pathPattern);
+               if (c != 0)
+                       return c;
+
+               c = compare(o.requiredMatchers.length, requiredMatchers.length);
+               if (c != 0)
+                       return c;
+
+               c = compare(o.optionalMatchers.length, optionalMatchers.length);
+               if (c != 0)
+                       return c;
+
+               c = compare(o.guards.length, guards.length);
+               if (c != 0)
+                       return c;
+
+               return 0;
+       }
+
+       @Override /* Object */
+       public boolean equals(Object o) {
+               if (! (o instanceof CallMethod))
+                       return false;
+               return (compareTo((CallMethod)o) == 0);
+       }
+
+       @Override /* Object */
+       public int hashCode() {
+               return super.hashCode();
+       }
+}
\ No newline at end of file

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallMethod.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,100 @@
+// 
***************************************************************************************************************************
+// * 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 javax.servlet.http.HttpServletResponse.*;
+
+import java.util.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Represents a group of CallMethods on a REST resource that handle the same 
HTTP Method name but with different
+ * paths/matchers/guards/etc...
+ *
+ * <p>
+ * Incoming requests for a particular HTTP method type (e.g. <js>"GET"</js>) 
are handed off to this class and then
+ * dispatched to the appropriate CallMethod.
+ */
+class CallRouter {
+       private final CallMethod[] callMethods;
+
+       private CallRouter(CallMethod[] callMethods) {
+               this.callMethods = callMethods;
+       }
+
+       /**
+        * Builder class.
+        */
+       static class Builder {
+               private List<CallMethod> childMethods = new 
ArrayList<CallMethod>();
+               private Set<String> collisions = new HashSet<String>();
+               private String httpMethodName;
+
+               Builder(String httpMethodName) {
+                       this.httpMethodName = httpMethodName;
+               }
+
+               String getHttpMethodName() {
+                       return httpMethodName;
+               }
+
+               Builder add(CallMethod m) throws RestServletException {
+                       if (! m.hasGuardsOrMatchers()) {
+                               String p = m.getHttpMethod() + ":" + 
m.getPathPattern();
+                               if (collisions.contains(p))
+                                       throw new 
RestServletException("Duplicate Java methods assigned to the same 
method/pattern:  ''{0}''", p);
+                               collisions.add(p);
+                       }
+                       childMethods.add(m);
+                       return this;
+               }
+
+               CallRouter build() {
+                       Collections.sort(childMethods);
+                       return new CallRouter(childMethods.toArray(new 
CallMethod[childMethods.size()]));
+               }
+       }
+
+       /**
+        * Workhorse method.
+        *
+        * <p>
+        * Routes this request to one of the CallMethods.
+        *
+        * @param pathInfo The value of {@link 
HttpServletRequest#getPathInfo()} (sorta)
+        * @return The HTTP response code.
+        */
+       int invoke(String pathInfo, RestRequest req, RestResponse res) throws 
RestException {
+               if (callMethods.length == 1)
+                       return callMethods[0].invoke(pathInfo, req, res);
+
+               int maxRc = 0;
+               for (CallMethod m : callMethods) {
+                       int rc = m.invoke(pathInfo, req, res);
+                       if (rc == SC_OK)
+                               return SC_OK;
+                       maxRc = Math.max(maxRc, rc);
+               }
+               return maxRc;
+       }
+
+       @Override /* Object */
+       public String toString() {
+               StringBuilder sb = new StringBuilder("CallRouter: [\n");
+               for (CallMethod sm : callMethods)
+                       sb.append("\t" + sm + "\n");
+               sb.append("]");
+               return sb.toString();
+       }
+}
\ No newline at end of file

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/CallRouter.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,54 @@
+// 
***************************************************************************************************************************
+// * 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 org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.rest.annotation.*;
+
+/**
+ * Specialized matcher for matching client versions.
+ *
+ * <p>
+ * See {@link RestResource#clientVersionHeader} and {@link 
RestMethod#clientVersion} for more info.
+ */
+public class ClientVersionMatcher extends RestMatcher {
+
+       private final String clientVersionHeader;
+       private final VersionRange range;
+
+       /**
+        * Constructor.
+        *
+        * @param clientVersionHeader
+        *      The HTTP request header name containing the client version.
+        *      If <jk>null</jk> or an empty string, uses 
<js>"X-Client-Version"</js>
+        * @param javaMethod The version string that the client version must 
match.
+        */
+       protected ClientVersionMatcher(String clientVersionHeader, 
java.lang.reflect.Method javaMethod) {
+               this.clientVersionHeader = isEmpty(clientVersionHeader) ? 
"X-Client-Version" : clientVersionHeader;
+               RestMethod m = javaMethod.getAnnotation(RestMethod.class);
+               range = new VersionRange(m.clientVersion());
+       }
+
+       @Override /* RestMatcher */
+       public boolean matches(RestRequest req) {
+               return range.matches(req.getHeader(clientVersionHeader));
+       }
+
+       @Override /* RestMatcher */
+       public boolean mustMatch() {
+               return true;
+       }
+}

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,265 @@
+// 
***************************************************************************************************************************
+// * 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 org.apache.juneau.internal.IOUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.response.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Represents the contents of a text file with convenience methods for 
resolving {@link Parameter} variables and adding
+ * HTTP response headers.
+ *
+ * <p>
+ * This class is handled special by the {@link WritableHandler} class.
+ */
+public class ReaderResource implements Writable {
+
+       private final MediaType mediaType;
+       private final String[] contents;
+       private final VarResolverSession varSession;
+       private final Map<String,String> headers;
+
+       /**
+        * Constructor.
+        *
+        * @param mediaType The HTTP media type.
+        * @param contents
+        *      The contents of this resource.
+        *      <br>If multiple contents are specified, the results will be 
concatenated.
+        *      <br>Contents can be any of the following:
+        *      <ul>
+        *              <li><code>CharSequence</code>
+        *              <li><code>Reader</code>
+        *              <li><code>File</code>
+        *      </ul>
+        * @throws IOException
+        */
+       protected ReaderResource(MediaType mediaType, Object...contents) throws 
IOException {
+               this(mediaType, null, null, contents);
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param mediaType The resource media type.
+        * @param headers The HTTP response headers for this streamed resource.
+        * @param varSession Optional variable resolver for resolving variables 
in the string.
+        * @param contents
+        *      The resource contents.
+        *      <br>If multiple contents are specified, the results will be 
concatenated.
+        *      <br>Contents can be any of the following:
+        *      <ul>
+        *              <li><code>InputStream</code>
+        *              <li><code>Reader</code> - Converted to UTF-8 bytes.
+        *              <li><code>File</code>
+        *              <li><code>CharSequence</code> - Converted to UTF-8 
bytes.
+        *              </ul>
+        * @throws IOException
+        */
+       public ReaderResource(MediaType mediaType, Map<String,String> headers, 
VarResolverSession varSession, Object...contents) throws IOException {
+               this.mediaType = mediaType;
+               this.varSession = varSession;
+
+               Map<String,String> m = new LinkedHashMap<String,String>();
+               if (headers != null)
+                       for (Map.Entry<String,String> e : headers.entrySet())
+                               m.put(e.getKey(), 
StringUtils.toString(e.getValue()));
+               this.headers = Collections.unmodifiableMap(m);
+
+               this.contents = new String[contents.length];
+               for (int i = 0; i < contents.length; i++) {
+                       Object c = contents[i];
+                       if (c == null)
+                               this.contents[i] = "";
+                       else if (c instanceof InputStream)
+                               this.contents[i] = read((InputStream)c);
+                       else if (c instanceof File)
+                               this.contents[i] = read((File)c);
+                       else if (c instanceof Reader)
+                               this.contents[i] = read((Reader)c);
+                       else if (c instanceof CharSequence)
+                               this.contents[i] = ((CharSequence)c).toString();
+                       else
+                               throw new IOException("Invalid class type 
passed to ReaderResource: " + c.getClass().getName());
+               }
+       }
+
+       /**
+        * Builder class for constructing {@link ReaderResource} objects.
+        */
+       @SuppressWarnings("hiding")
+       public static class Builder {
+               ArrayList<Object> contents = new ArrayList<Object>();
+               MediaType mediaType;
+               VarResolverSession varResolver;
+               Map<String,String> headers = new LinkedHashMap<String,String>();
+
+               /**
+                * Specifies the resource media type string.
+                *
+                * @param mediaType The resource media type string.
+                * @return This object (for method chaining).
+                */
+               public Builder mediaType(String mediaType) {
+                       this.mediaType = MediaType.forString(mediaType);
+                       return this;
+               }
+
+               /**
+                * Specifies the resource media type string.
+                *
+                * @param mediaType The resource media type string.
+                * @return This object (for method chaining).
+                */
+               public Builder mediaType(MediaType mediaType) {
+                       this.mediaType = mediaType;
+                       return this;
+               }
+
+               /**
+                * Specifies the contents for this resource.
+                *
+                * <p>
+                * This method can be called multiple times to add more content.
+                *
+                * @param contents
+                *      The resource contents.
+                *      <br>If multiple contents are specified, the results 
will be concatenated.
+                *      <br>Contents can be any of the following:
+                *      <ul>
+                *              <li><code>InputStream</code>
+                *              <li><code>Reader</code> - Converted to UTF-8 
bytes.
+                *              <li><code>File</code>
+                *              <li><code>CharSequence</code> - Converted to 
UTF-8 bytes.
+                *              </ul>
+                * @return This object (for method chaining).
+                */
+               public Builder contents(Object...contents) {
+                       this.contents.addAll(Arrays.asList(contents));
+                       return this;
+               }
+
+               /**
+                * Specifies an HTTP response header value.
+                *
+                * @param name The HTTP header name.
+                * @param value
+                *      The HTTP header value.
+                *      Will be converted to a <code>String</code> using {@link 
Object#toString()}.
+                * @return This object (for method chaining).
+                */
+               public Builder header(String name, Object value) {
+                       this.headers.put(name, StringUtils.toString(value));
+                       return this;
+               }
+
+               /**
+                * Specifies HTTP response header values.
+                *
+                * @param headers
+                *      The HTTP headers.
+                *      Values will be converted to <code>Strings</code> using 
{@link Object#toString()}.
+                * @return This object (for method chaining).
+                */
+               public Builder headers(Map<String,Object> headers) {
+                       for (Map.Entry<String,Object> e : headers.entrySet())
+                               header(e.getKey(), e.getValue());
+                       return this;
+               }
+
+               /**
+                * Specifies the variable resolver to use for this resource.
+                *
+                * @param varResolver The variable resolver.
+                * @return This object (for method chaining).
+                */
+               public Builder varResolver(VarResolverSession varResolver) {
+                       this.varResolver = varResolver;
+                       return this;
+               }
+
+               /**
+                * Create a new {@link ReaderResource} using values in this 
builder.
+                *
+                * @return A new immutable {@link ReaderResource} object.
+                * @throws IOException
+                */
+               public ReaderResource build() throws IOException {
+                       return new ReaderResource(mediaType, headers, 
varResolver, contents.toArray());
+               }
+       }
+
+       /**
+        * Get the HTTP response headers.
+        *
+        * @return The HTTP response headers.
+        */
+       public Map<String,String> getHeaders() {
+               return headers;
+       }
+
+       @Override /* Writeable */
+       public void writeTo(Writer w) throws IOException {
+               for (String s : contents) {
+                       if (varSession != null)
+                               varSession.resolveTo(s, w);
+                       else
+                               w.write(s);
+               }
+       }
+
+       @Override /* Writeable */
+       public MediaType getMediaType() {
+               return mediaType;
+       }
+
+       @Override /* Object */
+       public String toString() {
+               if (contents.length == 1 && varSession == null)
+                       return contents[0];
+               StringWriter sw = new StringWriter();
+               for (String s : contents) {
+                       if (varSession != null)
+                               return varSession.resolve(s);
+                       sw.write(s);
+               }
+               return sw.toString();
+       }
+
+       /**
+        * Same as {@link #toString()} but strips comments from the text before 
returning it.
+        *
+        * <p>
+        * Supports stripping comments from the following media types: HTML, 
XHTML, XML, JSON, Javascript, CSS.
+        *
+        * @return The resource contents stripped of any comments.
+        */
+       public String toCommentStrippedString() {
+               String s = toString();
+               String subType = mediaType.getSubType();
+               if ("html".equals(subType) || "xhtml".equals(subType) || 
"xml".equals(subType))
+                       s = s.replaceAll("(?s)<!--(.*?)-->\\s*", "");
+               else if ("json".equals(subType) || "javascript".equals(subType) 
|| "css".equals(subType))
+                       s = s.replaceAll("(?s)\\/\\*(.*?)\\*\\/\\s*", "");
+               return s;
+       }
+}

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,165 @@
+// 
***************************************************************************************************************************
+// * 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 org.apache.juneau.internal.StringUtils.*;
+
+import java.net.*;
+import java.text.*;
+
+import org.apache.juneau.urlencoding.*;
+
+/**
+ * REST methods can return this object as a shortcut for performing <code>HTTP 
302</code> redirects.
+ *
+ * <p>
+ * The following example shows the difference between handling redirects via 
the {@link RestRequest}/{@link RestResponse},
+ * and the simplified approach of using this class.
+ * <p class='bcode'>
+ *     <jc>// Redirect to "/contextPath/servletPath/foobar"</jc>
+ *
+ *     <jc>// Using RestRequest and RestResponse</jc>
+ *     <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example1"</js>)
+ *     <jk>public void</jk> example1(RestRequest req, RestResponse res) 
<jk>throws</jk> IOException {
+ *             res.sendRedirect(req.getServletURI() + <js>"/foobar"</js>);
+ *     }
+ *
+ *     <jc>// Using Redirect</jc>
+ *     <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example2"</js>)
+ *     <jk>public</jk> Redirect example2() {
+ *             <jk>return new</jk> Redirect(<js>"foobar"</js>);
+ *     }
+ * </p>
+ *
+ * <p>
+ * The constructor can use a {@link MessageFormat}-style pattern with multiple 
arguments:
+ * <p class='bcode'>
+ *     <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example3"</js>)
+ *     <jk>public</jk> Redirect example3() {
+ *             <jk>return new</jk> Redirect(<js>"foo/{0}/bar/{1}"</js>, id1, 
id2);
+ *     }
+ * </p>
+ *
+ * <p>
+ * The arguments are serialized to strings using the servlet's {@link 
UrlEncodingSerializer}, so any filters defined on
+ * the serializer or REST method/class will be used when present.
+ * The arguments will also be automatically URL-encoded.
+ *
+ * <p>
+ * Redirecting to the servlet root can be accomplished by simply using the 
no-arg constructor.
+ * <p class='bcode'>
+ *     <jc>// Simply redirect to the servlet root.
+ *     // Equivalent to res.sendRedirect(req.getServletURI()).</jc>
+ *     <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/example4"</js>)
+ *     <jk>public</jk> Redirect exmaple4() {
+ *             <jk>return new</jk> Redirect();
+ *     }
+ * </p>
+ *
+ * <p>
+ * This class is handled by {@link 
org.apache.juneau.rest.response.RedirectHandler}, a built-in default response
+ * handler created in {@link RestConfig}.
+ */
+public final class Redirect {
+
+       private final int httpResponseCode;
+       private final URI uri;
+
+       /**
+        * Redirect to the specified URL.
+        *
+        * <p>
+        * Relative paths are interpreted as relative to the servlet path.
+        *
+        * @param uri
+        *      The URL to redirect to.
+        *      <br>Can be any of the following:
+        *      <ul>
+        *              <li><code>URL</code>
+        *              <li><code>URI</code>
+        *              <li><code>CharSequence</code>
+        *      </ul>
+        * @param args Optional {@link MessageFormat}-style arguments.
+        */
+       public Redirect(Object uri, Object...args) {
+               this(0, uri, args);
+       }
+
+       /**
+        * Convenience method for redirecting to instance of {@link URL} and 
{@link URI}.
+        *
+        * <p>
+        * Same as calling <code>toString()</code> on the object and using the 
other constructor.
+        *
+        * @param uri
+        *      The URL to redirect to.
+        *      <br>Can be any of the following:
+        *      <ul>
+        *              <li><code>URL</code>
+        *              <li><code>URI</code>
+        *              <li><code>CharSequence</code>
+        *      </ul>
+        */
+       public Redirect(Object uri) {
+               this(0, uri, (Object[])null);
+       }
+
+       /**
+        * Redirect to the specified URL.
+        *
+        * <p>
+        * Relative paths are interpreted as relative to the servlet path.
+        *
+        * @param httpResponseCode The HTTP response code.
+        * @param url
+        *      The URL to redirect to.
+        *      <br>Can be any of the following:
+        *      <ul>
+        *              <li><code>URL</code>
+        *              <li><code>URI</code>
+        *              <li><code>CharSequence</code>
+        *      </ul>
+        * @param args Optional {@link MessageFormat}-style arguments.
+        */
+       public Redirect(int httpResponseCode, Object url, Object...args) {
+               this.httpResponseCode = httpResponseCode;
+               if (url == null)
+                       url = "";
+               this.uri = toURI(format(url.toString(), args));
+       }
+
+       /**
+        * Shortcut for redirecting to the servlet root.
+        */
+       public Redirect() {
+               this(0, null, (Object[])null);
+       }
+
+       /**
+        * Returns the response code passed in through the constructor.
+        *
+        * @return The response code passed in through the constructor, or 
<code>0</code> if response code wasn't specified.
+        */
+       public int getHttpResponseCode() {
+               return httpResponseCode;
+       }
+
+       /**
+        * Returns the URI to redirect to.
+        *
+        * @return The URI to redirect to.
+        */
+       public URI getURI() {
+               return uri;
+       }
+}

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/Redirect.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java
==============================================================================
--- 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java
 (added)
+++ 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java
 Fri Sep  8 23:25:34 2017
@@ -0,0 +1,457 @@
+// 
***************************************************************************************************************************
+// * 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 javax.servlet.http.HttpServletResponse.*;
+import static org.apache.juneau.internal.IOUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import javax.servlet.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.urlencoding.*;
+
+/**
+ * Contains the body of the HTTP request.
+ */
+@SuppressWarnings("unchecked")
+public class RequestBody {
+
+       private byte[] body;
+       private final RestRequest req;
+       private EncoderGroup encoders;
+       private Encoder encoder;
+       private ParserGroup parsers;
+       private UrlEncodingParser urlEncodingParser;
+       private RequestHeaders headers;
+       private BeanSession beanSession;
+       private int contentLength = 0;
+
+       RequestBody(RestRequest req) {
+               this.req = req;
+       }
+
+       RequestBody setEncoders(EncoderGroup encoders) {
+               this.encoders = encoders;
+               return this;
+       }
+
+       RequestBody setParsers(ParserGroup parsers) {
+               this.parsers = parsers;
+               return this;
+       }
+
+       RequestBody setHeaders(RequestHeaders headers) {
+               this.headers = headers;
+               return this;
+       }
+
+       RequestBody setUrlEncodingParser(UrlEncodingParser urlEncodingParser) {
+               this.urlEncodingParser = urlEncodingParser;
+               return this;
+       }
+
+       RequestBody setBeanSession(BeanSession beanSession) {
+               this.beanSession = beanSession;
+               return this;
+       }
+
+       @SuppressWarnings("hiding")
+       RequestBody load(byte[] body) {
+               this.body = body;
+               return this;
+       }
+
+       boolean isLoaded() {
+               return body != null;
+       }
+
+       /**
+        * 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 class="doclink" 
href="../../../../overview-summary.html#Core.PojoCategories">POJO 
Categories</a> for
+        * a complete definition of supported POJOs.
+        *
+        * <h5 class='section'>Examples:</h5>
+        * <p class='bcode'>
+        *      <jc>// Parse into an integer.</jc>
+        *      <jk>int</jk> body = 
req.getBody().asType(<jk>int</jk>.<jk>class</jk>);
+        *
+        *      <jc>// Parse into an int array.</jc>
+        *      <jk>int</jk>[] body = 
req.getBody().asType(<jk>int</jk>[].<jk>class</jk>);
+
+        *      <jc>// Parse into a bean.</jc>
+        *      MyBean body = req.getBody().asType(MyBean.<jk>class</jk>);
+        *
+        *      <jc>// Parse into a linked-list of objects.</jc>
+        *      List body = req.getBody().asType(LinkedList.<jk>class</jk>);
+        *
+        *      <jc>// Parse into a map of object keys/values.</jc>
+        *      Map body = req.getBody().asType(TreeMap.<jk>class</jk>);
+        * </p>
+        *
+        * @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 asType(Class<T> type) throws IOException, ParseException {
+               return parse(beanSession.getClassMeta(type));
+       }
+
+       /**
+        * Reads the input from the HTTP request as JSON, XML, or HTML and 
converts the input to a POJO.
+        *
+        * <h5 class='section'>Examples:</h5>
+        * <p class='bcode'>
+        *      <jc>// Parse into a linked-list of strings.</jc>
+        *      List&lt;String&gt; body = 
req.getBody().asType(LinkedList.<jk>class</jk>, String.<jk>class</jk>);
+        *
+        *      <jc>// Parse into a linked-list of linked-lists of strings.</jc>
+        *      List&lt;List&lt;String&gt;&gt; body = 
req.getBody().asType(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, 
String.<jk>class</jk>);
+        *
+        *      <jc>// Parse into a map of string keys/values.</jc>
+        *      Map&lt;String,String&gt; body = 
req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, 
String.<jk>class</jk>);
+        *
+        *      <jc>// Parse into a map containing string keys and values of 
lists containing beans.</jc>
+        *      Map&lt;String,List&lt;MyBean&gt;&gt; body = 
req.getBody().asType(TreeMap.<jk>class</jk>, String.<jk>class</jk>, 
List.<jk>class</jk>, MyBean.<jk>class</jk>);
+        * </p>
+        *
+        * @param type
+        *      The type of object to create.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType},
+        *      {@link GenericArrayType}
+        * @param args
+        *      The type arguments of the class if it's a collection or map.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType},
+        *      {@link GenericArrayType}
+        *      <br>Ignored if the main type is not a map or collection.
+        * @param <T> The class type to instantiate.
+        * @return The input parsed to a POJO.
+        */
+       public <T> T asType(Type type, Type...args) {
+               return (T)parse(beanSession.getClassMeta(type, args));
+       }
+
+       /**
+        * 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 asString() throws IOException {
+               if (body == null)
+                       body = readBytes(getInputStream(), 1024);
+               return new String(body, UTF8);
+       }
+
+       /**
+        * Returns the HTTP body content as a simple hexadecimal character 
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 asHex() throws IOException {
+               if (body == null)
+                       body = readBytes(getInputStream(), 1024);
+               return toHex(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.
+        *
+        * @return The body contents as a reader.
+        * @throws IOException
+        */
+       public BufferedReader getReader() throws IOException {
+               Reader r = getUnbufferedReader();
+               if (r instanceof BufferedReader)
+                       return (BufferedReader)r;
+               int len = req.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(new String(body, UTF8));
+               return new InputStreamReader(getInputStream(), 
req.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.
+        */
+       public ServletInputStream getInputStream() throws IOException {
+
+               if (body != null)
+                       return new ServletInputStream2(body);
+
+               Encoder enc = getEncoder();
+
+               ServletInputStream is = req.getRawInputStream();
+               if (enc != null) {
+                       final InputStream is2 = enc.getInputStream(is);
+                       return new ServletInputStream2(is2);
+               }
+               return is;
+       }
+
+       /**
+        * Returns the parser and media type matching the request 
<code>Content-Type</code> header.
+        *
+        * @return
+        *      The parser matching the request <code>Content-Type</code> 
header, or <jk>null</jk> if no matching parser was
+        *      found.
+        *      Includes the matching media type.
+        */
+       public ParserMatch getParserMatch() {
+               MediaType mediaType = headers.getContentType();
+               if (isEmpty(mediaType)) {
+                       if (body != null)
+                               mediaType = MediaType.UON;
+                       else
+                               mediaType = MediaType.JSON;
+               }
+               ParserMatch pm = parsers.getParserMatch(mediaType);
+
+               // If no patching parser for URL-encoding, use the one defined 
on the servlet.
+               if (pm == null && mediaType.equals(MediaType.URLENCODING))
+                       pm = new ParserMatch(MediaType.URLENCODING, 
urlEncodingParser);
+
+               return pm;
+       }
+
+       /**
+        * Returns the parser matching the request <code>Content-Type</code> 
header.
+        *
+        * @return
+        *      The parser matching the request <code>Content-Type</code> 
header, or <jk>null</jk> if no matching parser was
+        *      found.
+        */
+       public Parser getParser() {
+               ParserMatch pm = getParserMatch();
+               return (pm == null ? null : pm.getParser());
+       }
+
+       /**
+        * Returns the reader parser matching the request 
<code>Content-Type</code> header.
+        *
+        * @return
+        *      The reader parser matching the request 
<code>Content-Type</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 != null && p.isReaderParser())
+                       return (ReaderParser)p;
+               return null;
+       }
+
+       /* Workhorse method */
+       private <T> T parse(ClassMeta<T> cm) throws RestException {
+
+               try {
+                       if (cm.isReader())
+                               return (T)getReader();
+
+                       if (cm.isInputStream())
+                               return (T)getInputStream();
+
+                       TimeZone timeZone = headers.getTimeZone();
+                       Locale locale = req.getLocale();
+                       ParserMatch pm = getParserMatch();
+
+                       if (pm != null) {
+                               Parser p = pm.getParser();
+                               MediaType mediaType = pm.getMediaType();
+                               try {
+                                       req.getProperties().append("mediaType", 
mediaType).append("characterEncoding", req.getCharacterEncoding());
+                                       ParserSession session = 
p.createSession(new ParserSessionArgs(req.getProperties(), req.getJavaMethod(), 
locale, timeZone, mediaType, req.getContext().getResource()));
+                                       Object in = session.isReaderParser() ? 
getUnbufferedReader() : getInputStream();
+                                       return session.parse(in, cm);
+                               } catch (ParseException e) {
+                                       throw new RestException(SC_BAD_REQUEST,
+                                               "Could not convert request body 
content to class type ''{0}'' using parser ''{1}''.",
+                                               cm, 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}",
+                               headers.getContentType(), 
req.getParserGroup().getSupportedMediaTypes()
+                       );
+
+               } catch (IOException e) {
+                       throw new RestException(SC_INTERNAL_SERVER_ERROR,
+                               "I/O exception occurred while attempting to 
handle request ''{0}''.",
+                               req.getDescription()
+                       ).initCause(e);
+               }
+       }
+
+       private Encoder getEncoder() {
+               if (encoder == null) {
+                       String ce = req.getHeader("content-encoding");
+                       if (! isEmpty(ce)) {
+                               ce = ce.trim();
+                               encoder = encoders.getEncoder(ce);
+                               if (encoder == null)
+                                       throw new 
RestException(SC_UNSUPPORTED_MEDIA_TYPE,
+                                               "Unsupported encoding in 
request header ''Content-Encoding'': ''{0}''\n\tSupported codings: {1}",
+                                               
req.getHeader("content-encoding"), encoders.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 the content length of the body.
+        *
+        * @return The content length of the body in bytes.
+        */
+       public int getContentLength() {
+               return contentLength == 0 ? req.getRawContentLength() : 
contentLength;
+       }
+
+       /**
+        * ServletInputStream wrapper around a normal input stream.
+        */
+       private static class ServletInputStream2 extends ServletInputStream {
+
+               private final InputStream is;
+
+               private ServletInputStream2(InputStream is) {
+                       this.is = is;
+               }
+
+               private ServletInputStream2(byte[] b) {
+                       this(new ByteArrayInputStream(b));
+               }
+
+               @Override /* InputStream */
+               public final int read() throws IOException {
+                       return is.read();
+               }
+
+               @Override /* InputStream */
+               public final void close() throws IOException {
+                       is.close();
+               }
+
+               @Override /* ServletInputStream */
+               public boolean isFinished() {
+                       return false;
+               }
+
+               @Override /* ServletInputStream */
+               public boolean isReady() {
+                       return true;
+               }
+
+               @Override /* ServletInputStream */
+               public void setReadListener(ReadListener arg0) {
+                       throw new NoSuchMethodError();
+               }
+       }
+}

Propchange: 
release/incubator/juneau/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestBody.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain


Reply via email to