http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ParamsResource.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ParamsResource.java
 
b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ParamsResource.java
index e690d88..d9424e7 100644
--- 
a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ParamsResource.java
+++ 
b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ParamsResource.java
@@ -12,7 +12,7 @@
 // 
***************************************************************************************************************************
 package org.apache.juneau.rest.test;
 
-import static org.apache.juneau.rest.RestServletContext.*;
+import static org.apache.juneau.rest.RestContext.*;
 
 import java.util.*;
 
@@ -35,7 +35,8 @@ import org.apache.juneau.urlencoding.*;
        serializers=PlainTextSerializer.class,
        properties={
                @Property(name=REST_allowMethodParam, value="*")
-       }
+       },
+       pojoSwaps={CalendarSwap.DateMedium.class}
 )
 public class ParamsResource extends RestServletDefault {
        private static final long serialVersionUID = 1L;
@@ -106,11 +107,6 @@ public class ParamsResource extends RestServletDefault {
                res.setOutput("PUT /uuid/"+uuid);
        }
 
-       @Override /* RestServlet */
-       public Class<?>[] createPojoSwaps() {
-               return new Class[]{CalendarSwap.DateMedium.class};
-       }
-
        
//====================================================================================================
        // @FormData annotation - GET
        
//====================================================================================================

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/PathResource.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/PathResource.java 
b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/PathResource.java
index a38cb68..e1b7f5c 100644
--- 
a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/PathResource.java
+++ 
b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/PathResource.java
@@ -33,7 +33,7 @@ public class PathResource extends RestServletDefault {
        
//====================================================================================================
        @RestMethod(name="GET", path="/")
        public String doGet() {
-               return getPath();
+               return getContext().getPath();
        }
 
        @RestResource(
@@ -47,7 +47,7 @@ public class PathResource extends RestServletDefault {
                // Basic tests
                @RestMethod(name="GET", path="/")
                public String doGet() {
-                       return getPath();
+                       return getContext().getPath();
                }
        }
 
@@ -59,7 +59,7 @@ public class PathResource extends RestServletDefault {
                // Basic tests
                @RestMethod(name="GET", path="/")
                public String doGet() {
-                       return getPath();
+                       return getContext().getPath();
                }
        }
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/Root.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/Root.java 
b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/Root.java
index 67634e4..f24c34c 100644
--- a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/Root.java
+++ b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/Root.java
@@ -66,6 +66,6 @@ public class Root extends RestServletDefault {
 
        @RestMethod(name="GET", path="/")
        public ChildResourceDescriptions doGet(RestRequest req) {
-               return new ChildResourceDescriptions(this, req);
+               return new ChildResourceDescriptions(getContext(), req);
        }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ErrorConditionsTest.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ErrorConditionsTest.java
 
b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ErrorConditionsTest.java
index eb46760..81bce1d 100644
--- 
a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ErrorConditionsTest.java
+++ 
b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ErrorConditionsTest.java
@@ -127,7 +127,7 @@ public class ErrorConditionsTest extends RestTestcase {
                } catch (RestCallException e) {
                        checkErrorResponse(debug, e, SC_BAD_REQUEST,
                                "Could not convert request body content to 
class type 'org.apache.juneau.rest.test.ErrorConditionsResource$Test3c' using 
parser 'org.apache.juneau.json.JsonParser'.",
-                               "Caused by (RuntimeException): Test error");
+                               "Test error");
                }
        }
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/TestUtils.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/TestUtils.java 
b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/TestUtils.java
index 22d84e0..caf9041 100644
--- a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/TestUtils.java
+++ b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/TestUtils.java
@@ -49,14 +49,24 @@ public class TestUtils {
                        System.err.println(r); // NOT DEBUG
                        e.printStackTrace();
                }
-               if (status != e.getResponseCode())
+               if (status != e.getResponseCode()) {
+                       dumpResponse(r, "Response status code was not correct.  
Expected: ''{0}''.  Actual: ''{1}''", status, e.getResponseCode());
                        throw new 
AssertionFailedError(MessageFormat.format("Response status code was not 
correct.  Expected: ''{0}''.  Actual: ''{1}''", status, e.getResponseCode()));
+               }
                for (String s : contains) {
                        if (r == null || ! r.contains(s)) {
                                if (! debug)
-                                       System.err.println(r); // NOT DEBUG
+                                       dumpResponse(r, "Response did not have 
the following expected text: ''{0}''", s);
                                throw new 
AssertionFailedError(MessageFormat.format("Response did not have the following 
expected text: ''{0}''", s));
                        }
                }
        }
+
+       private static void dumpResponse(String r, String msg, Object...args) {
+               System.err.println("*** Failure 
****************************************************************************************");
 // NOT DEBUG
+               System.err.println(MessageFormat.format(msg, args));
+               System.err.println("*** Response-Start 
*********************************************************************************");
 // NOT DEBUG
+               System.err.println(r); // NOT DEBUG
+               System.err.println("*** Response-End 
***********************************************************************************");
 // NOT DEBUG
+       }
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/CallMethod.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/CallMethod.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/CallMethod.java
new file mode 100644
index 0000000..5cfa4e5
--- /dev/null
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/CallMethod.java
@@ -0,0 +1,996 @@
+// 
***************************************************************************************************************************
+// * 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.rest.CallMethod.ParamType.*;
+import static org.apache.juneau.rest.RestContext.*;
+import static org.apache.juneau.rest.annotation.Inherit.*;
+import static org.apache.juneau.serializer.SerializerContext.*;
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+import java.lang.reflect.Method;
+import java.util.*;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.dto.swagger.*;
+import org.apache.juneau.encoders.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.annotation.Properties;
+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")
+final class CallMethod implements Comparable<CallMethod>  {
+       private final java.lang.reflect.Method method;
+       private final String httpMethod;
+       private final UrlPathPattern pathPattern;
+       private final CallMethod.MethodParam[] 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;
+       private final String defaultEncoding;
+       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;
+
+       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.properties = b.properties;
+               this.defaultRequestHeaders = b.defaultRequestHeaders;
+               this.defaultEncoding = b.defaultEncoding;
+               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;
+       }
+
+       private static class Builder  {
+               private String httpMethod, defaultEncoding, description, tags, 
summary, externalDocs;
+               private UrlPathPattern pathPattern;
+               private CallMethod.MethodParam[] 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 ObjectMap properties;
+               private Map<String,String> defaultRequestHeaders;
+               private boolean plainParams, deprecated;
+               private Integer priority;
+               private org.apache.juneau.rest.annotation.Parameter[] 
parameters;
+               private Response[] responses;
+
+               private Builder(Object servlet, java.lang.reflect.Method 
method, RestContext context) throws RestServletException {
+                       try {
+
+                               RestMethod m = 
method.getAnnotation(RestMethod.class);
+                               if (m == null)
+                                       throw new 
RestServletException("@RestMethod annotation not found on method ''{0}.{1}''", 
method.getDeclaringClass().getName(), method.getName());
+
+                               if (! m.description().isEmpty())
+                                       description = m.description();
+                               if (! m.tags().isEmpty())
+                                       tags = m.tags();
+                               if (! m.summary().isEmpty())
+                                       summary = m.summary();
+                               if (! m.externalDocs().isEmpty())
+                                       externalDocs = m.externalDocs();
+                               deprecated = m.deprecated();
+                               parameters = m.parameters();
+                               responses = m.responses();
+                               serializers = context.getSerializers();
+                               parsers = context.getParsers();
+                               urlEncodingSerializer = 
context.getUrlEncodingSerializer();
+                               urlEncodingParser = 
context.getUrlEncodingParser();
+                               encoders = context.getEncoders();
+                               properties = context.getProperties();
+
+                               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.beanFilters().length > 
0 || m.pojoSwaps().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(""))
+                                       throw new 
RestServletException("@RestMethod name not specified on method ''{0}.{1}''", 
method.getDeclaringClass().getName(), method.getName());
+                               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] = 
m.converters()[i].newInstance();
+
+                               guards = new RestGuard[m.guards().length];
+                               for (int i = 0; i < guards.length; i++)
+                                       guards[i] = m.guards()[i].newInstance();
+
+                               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 
(ClassUtils.isParentClass(RestMatcherReflecting.class, c))
+                                               matcher = 
c.getConstructor(Object.class, Method.class).newInstance(servlet, method);
+                                       else
+                                               matcher = c.newInstance();
+                                       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());
+                                       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());
+                                       pgb.beanFilters(m.beanFilters());
+                                       pgb.pojoSwaps(m.pojoSwaps());
+                               }
+
+                               if (uepb != null) {
+                                       for (Property p1 : m.properties())
+                                               uepb.property(p1.name(), 
p1.value());
+                                       uepb.beanFilters(m.beanFilters());
+                                       uepb.pojoSwaps(m.pojoSwaps());
+                               }
+
+                               if (m.properties().length > 0) {
+                                       properties = new 
ObjectMap().setInner(properties);
+                                       for (Property p1 : m.properties()) {
+                                               properties.put(p1.name(), 
p1.value());
+                                       }
+                               }
+
+                               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 
''{0}''", c.getSimpleName()).initCause(e);
+                                               }
+                                       }
+                                       encoders = g.build();
+                               }
+
+                               defaultRequestHeaders = new 
TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+                               for (String s : m.defaultRequestHeaders()) {
+                                       String[] h = RestUtils.parseHeader(s);
+                                       if (h == null)
+                                               throw new 
RestServletException("Invalid default request header specified: ''{0}''.  Must 
be in the format: ''Header-Name: header-value''", s);
+                                       defaultRequestHeaders.put(h[0], h[1]);
+                               }
+
+                               defaultEncoding = 
properties.getString(REST_defaultCharset, context.getDefaultCharset());
+                               String paramFormat = 
properties.getString(REST_paramFormat, context.getParamFormat());
+                               plainParams = paramFormat.equals("PLAIN");
+
+                               pathPattern = new UrlPathPattern(p);
+
+                               int attrIdx = 0;
+                               Type[] pt = method.getGenericParameterTypes();
+                               Annotation[][] pa = 
method.getParameterAnnotations();
+                               params = new CallMethod.MethodParam[pt.length];
+                               for (int i = 0; i < params.length; i++) {
+                                       params[i] = new 
CallMethod.MethodParam(pt[i], method, pa[i], plainParams, pathPattern, attrIdx);
+                                       attrIdx = params[i].attrIdx;
+                               }
+
+                               if (sgb != null)
+                                       serializers = sgb.build();
+                               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 (Exception e) {
+                               throw new RestServletException("Exception 
occurred while initializing method ''{0}''", method.getName()).initCause(e);
+                       }
+               }
+       }
+
+       /**
+        * Represents a single parameter in the Java method.
+        */
+       private static class MethodParam {
+
+               private final ParamType paramType;
+               private final Type type;
+               private final String name;
+               private final boolean multiPart, plainParams;
+               private final int attrIdx;
+
+               private MethodParam(Type type, Method method, Annotation[] 
annotations, boolean methodPlainParams, UrlPathPattern pathPattern, int 
attrIdx) throws ServletException {
+                       this.type = type;
+
+                       ParamType _paramType = null;
+                       String _name = "";
+                       boolean _multiPart = false, _plainParams = false;
+
+                       boolean isClass = type instanceof Class;
+                       if (isClass && isParentClass(HttpServletRequest.class, 
(Class<?>)type))
+                               _paramType = REQ;
+                       else if (isClass && 
isParentClass(HttpServletResponse.class, (Class<?>)type))
+                               _paramType = RES;
+                       else for (Annotation a : annotations) {
+                               if (a instanceof Path) {
+                                       Path a2 = (Path)a;
+                                       _paramType = PATH;
+                                       _name = a2.value();
+                               } else if (a instanceof Header) {
+                                       Header h = (Header)a;
+                                       _paramType = HEADER;
+                                       _name = h.value();
+                               } else if (a instanceof FormData) {
+                                       FormData p = (FormData)a;
+                                       if (p.multipart())
+                                               assertCollection(type, method);
+                                       _paramType = FORMDATA;
+                                       _multiPart = p.multipart();
+                                       _plainParams = 
p.format().equals("INHERIT") ? methodPlainParams : p.format().equals("PLAIN");
+                                       _name = p.value();
+                               } else if (a instanceof Query) {
+                                       Query p = (Query)a;
+                                       if (p.multipart())
+                                               assertCollection(type, method);
+                                       _paramType = QUERY;
+                                       _multiPart = p.multipart();
+                                       _plainParams = 
p.format().equals("INHERIT") ? methodPlainParams : p.format().equals("PLAIN");
+                                       _name = p.value();
+                               } else if (a instanceof HasFormData) {
+                                       HasFormData p = (HasFormData)a;
+                                       _paramType = HASFORMDATA;
+                                       _name = p.value();
+                               } else if (a instanceof HasQuery) {
+                                       HasQuery p = (HasQuery)a;
+                                       _paramType = HASQUERY;
+                                       _name = p.value();
+                               } else if (a instanceof Body) {
+                                       _paramType = BODY;
+                               } else if (a instanceof 
org.apache.juneau.rest.annotation.Method) {
+                                       _paramType = METHOD;
+                                       if (type != String.class)
+                                               throw new 
ServletException("@Method parameters must be of type String");
+                               } else if (a instanceof PathRemainder) {
+                                       _paramType = PATHREMAINDER;
+                                       if (type != String.class)
+                                               throw new 
ServletException("@PathRemainder parameters must be of type String");
+                               } else if (a instanceof Properties) {
+                                       _paramType = PROPS;
+                                       _name = "PROPERTIES";
+                               } else if (a instanceof Messages) {
+                                       _paramType = MESSAGES;
+                                       _name = "MESSAGES";
+                               }
+                       }
+                       if (_paramType == null)
+                               _paramType = PATH;
+
+                       if (_paramType == PATH && _name.isEmpty()) {
+                               if (pathPattern.getVars().length <= attrIdx)
+                                       throw new RestServletException("Number 
of attribute parameters in method ''{0}'' exceeds the number of URL pattern 
variables.", method.getName());
+                               _name = pathPattern.getVars()[attrIdx++];
+                       }
+
+                       this.paramType = _paramType;
+                       this.name = _name;
+                       this.multiPart = _multiPart;
+                       this.plainParams = _plainParams;
+                       this.attrIdx = attrIdx;
+               }
+
+               /**
+                * Throws an exception if the specified type isn't an array or 
collection.
+                */
+               private static void assertCollection(Type t, Method m) throws 
ServletException {
+                       ClassMeta<?> cm = BeanContext.DEFAULT.getClassMeta(t);
+                       if (! cm.isCollectionOrArray())
+                               throw new ServletException("Use of multipart 
flag on parameter that's not an array or Collection on method" + m);
+               }
+
+               private Object getValue(RestRequest req, RestResponse res) 
throws Exception {
+                       BeanSession session = req.getBeanSession();
+                       switch(paramType) {
+                               case REQ:        return req;
+                               case RES:        return res;
+                               case PATH:       return 
req.getPathParameter(name, type);
+                               case BODY:       return req.getBody(type);
+                               case HEADER:     return req.getHeader(name, 
type);
+                               case METHOD:     return req.getMethod();
+                               case FORMDATA: {
+                                       if (multiPart)
+                                               return 
req.getFormDataParameters(name, type);
+                                       if (plainParams)
+                                               return 
session.convertToType(req.getFormDataParameter(name), 
session.getClassMeta(type));
+                                       return req.getFormDataParameter(name, 
type);
+                               }
+                               case QUERY: {
+                                       if (multiPart)
+                                               return 
req.getQueryParameters(name, type);
+                                       if (plainParams)
+                                               return 
session.convertToType(req.getQueryParameter(name), session.getClassMeta(type));
+                                       return req.getQueryParameter(name, 
type);
+                               }
+                               case HASFORMDATA:   return 
session.convertToType(req.hasFormDataParameter(name), 
session.getClassMeta(type));
+                               case HASQUERY:      return 
session.convertToType(req.hasQueryParameter(name), session.getClassMeta(type));
+                               case PATHREMAINDER: return 
req.getPathRemainder();
+                               case PROPS:         return res.getProperties();
+                               case MESSAGES:      return 
req.getResourceBundle();
+                               default:            return null;
+                       }
+               }
+       }
+
+       static enum ParamType {
+               REQ, RES, PATH, BODY, HEADER, METHOD, FORMDATA, QUERY, 
HASFORMDATA, HASQUERY, PATHREMAINDER, PROPS, MESSAGES;
+
+               private String getSwaggerParameterType() {
+                       switch(this) {
+                               case PATH: return "path";
+                               case HEADER: return "header";
+                               case FORMDATA: return "formData";
+                               case QUERY: return "query";
+                               case BODY: return "body";
+                               default: return null;
+                       }
+               }
+       }
+
+       /**
+        * 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 (CallMethod.MethodParam mp : this.params) {
+                       String in = mp.paramType.getSwaggerParameterType();
+                       if (in != null) {
+                               String k2 = in + '.' + ("body".equals(in) ? 
null : mp.name);
+                               ParameterInfo p = m.get(k2);
+                               if (p == null) {
+                                       p = parameterInfoStrict(in, mp.name);
+                                       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.setPathParameter(pathPattern.getVars()[i], 
patternVals[i]);
+
+               req.init(method, remainder, createRequestProperties(properties, 
req), defaultRequestHeaders, defaultEncoding, serializers, parsers, 
urlEncodingParser, encoders);
+               res.init(req.getProperties(), defaultEncoding, 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.getCallHandler().onPreCall(req);
+
+               Object[] args = new Object[params.length];
+               for (int i = 0; i < params.length; i++) {
+                       try {
+                               args[i] = params[i].getValue(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].paramType.name(), 
params[i].name, params[i].type, 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.getCallHandler().onPostCall(req, res);
+
+                       if (res.hasOutput()) {
+                               output = res.getOutput();
+                               for (RestConverter converter : converters)
+                                       output = converter.convert(req, output, 
context.getBeanContext().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(), 
ClassUtils.getReadableClassNames(args)
+                       );
+               } 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.
+        */
+       static 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();
+                                       if (k.indexOf('.') != -1) {
+                                               String prefix = k.substring(0, 
k.indexOf('.'));
+                                               String remainder = 
k.substring(k.indexOf('.')+1);
+                                               if ("path".equals(prefix))
+                                                       return 
req.getPathParameter(remainder);
+                                               if ("query".equals(prefix))
+                                                       return 
req.getQueryParameter(remainder);
+                                               if ("formData".equals(prefix))
+                                                       return 
req.getFormDataParameter(remainder);
+                                               if ("header".equals(prefix))
+                                                       return 
req.getHeader(remainder);
+                                       }
+                                       if 
(k.equals(SERIALIZER_absolutePathUriBase)) {
+                                               int serverPort = 
req.getServerPort();
+                                               String serverName = 
req.getServerName();
+                                               return req.getScheme() + "://" 
+ serverName + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort);
+                                       }
+                                       if (k.equals(REST_servletPath))
+                                               return req.getServletPath();
+                                       if (k.equals(REST_servletURI))
+                                               return req.getServletURI();
+                                       if (k.equals(REST_relativeServletURI))
+                                               return 
req.getRelativeServletURI();
+                                       if (k.equals(REST_pathInfo))
+                                               return req.getPathInfo();
+                                       if (k.equals(REST_requestURI))
+                                               return req.getRequestURI();
+                                       if (k.equals(REST_method))
+                                               return req.getMethod();
+                                       if (k.equals(REST_servletTitle))
+                                               return req.getServletTitle();
+                                       if (k.equals(REST_servletDescription))
+                                               return 
req.getServletDescription();
+                                       if (k.equals(REST_methodSummary))
+                                               return req.getMethodSummary();
+                                       if (k.equals(REST_methodDescription))
+                                               return 
req.getMethodDescription();
+                                       o = req.getPathParameter(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 = Utils.compare(o.requiredMatchers.length, 
requiredMatchers.length);
+               if (c != 0)
+                       return c;
+
+               c = Utils.compare(o.optionalMatchers.length, 
optionalMatchers.length);
+               if (c != 0)
+                       return c;
+
+               c = Utils.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

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/CallRouter.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/CallRouter.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/CallRouter.java
new file mode 100644
index 0000000..bbac14c
--- /dev/null
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/CallRouter.java
@@ -0,0 +1,98 @@
+// 
***************************************************************************************************************************
+// * 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

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
index 4f65615..ccf4dd1 100644
--- a/juneau-rest/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/ClientVersionMatcher.java
@@ -20,7 +20,7 @@ import org.apache.juneau.rest.annotation.*;
  * <p>
  * See {@link RestResource#clientVersionHeader} and {@link 
RestMethod#clientVersion} for more info.
  */
-public class ClientVersionMatcher extends RestMatcherReflecting {
+public class ClientVersionMatcher extends RestMatcher {
 
        private final String clientVersionHeader;
        private final VersionRange range;
@@ -28,12 +28,12 @@ public class ClientVersionMatcher extends 
RestMatcherReflecting {
        /**
         * Constructor.
         *
-        * @param servlet The servlet.
+        * @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(RestServlet servlet, 
java.lang.reflect.Method javaMethod) {
-               super(servlet, javaMethod);
-               this.clientVersionHeader = servlet.getClientVersionHeader();
+       protected ClientVersionMatcher(String clientVersionHeader, 
java.lang.reflect.Method javaMethod) {
+               this.clientVersionHeader = 
StringUtils.isEmpty(clientVersionHeader) ? "X-Client-Version" : 
clientVersionHeader;
                RestMethod m = javaMethod.getAnnotation(RestMethod.class);
                range = new VersionRange(m.clientVersion());
        }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/Redirect.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/Redirect.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/Redirect.java
index 348346a..f08d679 100644
--- a/juneau-rest/src/main/java/org/apache/juneau/rest/Redirect.java
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/Redirect.java
@@ -15,7 +15,6 @@ package org.apache.juneau.rest;
 import java.net.*;
 import java.text.*;
 
-import org.apache.juneau.*;
 import org.apache.juneau.urlencoding.*;
 
 /**
@@ -62,7 +61,7 @@ import org.apache.juneau.urlencoding.*;
  * </p>
  * <p>
  * This class is handled by {@link 
org.apache.juneau.rest.response.RedirectHandler}, a built-in default
- *     response handler created by {@link 
RestServlet#createResponseHandlers(ObjectMap)}.
+ *     response handler created in {@link RestConfig}.
  */
 public final class Redirect {
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/ResponseHandler.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest/src/main/java/org/apache/juneau/rest/ResponseHandler.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/ResponseHandler.java
index 6607e64..da7c3e9 100644
--- a/juneau-rest/src/main/java/org/apache/juneau/rest/ResponseHandler.java
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/ResponseHandler.java
@@ -30,7 +30,7 @@ import org.apache.juneau.rest.response.*;
  * Response handlers can be associated with {@link RestServlet RestServlets} 
through the following ways:
  * <ul class='spaced-list'>
  *     <li>Through the {@link RestResource#responseHandlers 
@RestResource.responseHandlers} annotation.
- *     <li>By overriding {@link RestServlet#createResponseHandlers(ObjectMap)} 
and augmenting or creating your
+ *     <li>By calling the {@link RestConfig#addResponseHandlers(Class...)} and 
augmenting or creating your
  *             own list of handlers.
  * </ul>
  * <p>
@@ -40,6 +40,8 @@ import org.apache.juneau.rest.response.*;
  *     <li>{@link ReaderHandler} - Pipes the output of {@link Reader Readers} 
to the response writer ({@link RestResponse#getWriter()}).
  *     <li>{@link InputStreamHandler} - Pipes the output of {@link InputStream 
InputStreams} to the response output stream ({@link 
RestResponse#getOutputStream()}).
  *     <li>{@link RedirectHandler} - Handles {@link Redirect} objects.
+ *     <li>{@link WritableHandler} - Handles {@link Writable} objects.
+ *     <li>{@link StreamableHandler} - Handles {@link Streamable} objects.
  * </ul>
  * <p>
  * Response handlers can be used to process POJOs that cannot normally be 
handled through Juneau serializers, or

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/07843d64/juneau-rest/src/main/java/org/apache/juneau/rest/RestCallHandler.java
----------------------------------------------------------------------
diff --git 
a/juneau-rest/src/main/java/org/apache/juneau/rest/RestCallHandler.java 
b/juneau-rest/src/main/java/org/apache/juneau/rest/RestCallHandler.java
new file mode 100644
index 0000000..5d6a780
--- /dev/null
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestCallHandler.java
@@ -0,0 +1,348 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static java.util.logging.Level.*;
+import static javax.servlet.http.HttpServletResponse.*;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.vars.*;
+
+/**
+ * Class that handles the basic lifecycle of an HTTP REST call.
+ * <p>
+ * Subclasses can override these methods to tailor how HTTP REST calls are 
handled.
+ * Subclasses MUST implement a public constructor that takes in a {@link 
RestContext} object.
+ * <p>
+ * RestCallHandlers are associated with servlets/resources in one of the 
following ways:
+ * <ul>
+ *     <li>The {@link RestResource#callHandler @RestResource.callHandler()} 
annotation.
+ *     <li>The {@link RestConfig#setCallHandler(Class)} method.
+ * </ul>
+ */
+public class RestCallHandler {
+
+       private final RestContext context;
+       private final RestLogger logger;
+       private final RestServlet restServlet;
+       private final Map<String,CallRouter> callRouters;
+
+       /**
+        * Constructor.
+        * @param context The resource context.
+        */
+       public RestCallHandler(RestContext context) {
+               this.context = context;
+               this.logger = context.getLogger();
+               this.callRouters = context.getCallRouters();
+               this.restServlet = context.getRestServlet();  // Null if this 
isn't a RestServlet!
+       }
+
+       /**
+        * Creates a {@link RestRequest} object based on the specified incoming 
{@link HttpServletRequest} object.
+        * <p>
+        * Subclasses may choose to override this method to provide a 
specialized request object.
+        *
+        * @param req The request object from the {@link 
#service(HttpServletRequest, HttpServletResponse)} method.
+        * @return The wrapped request object.
+        * @throws ServletException If any errors occur trying to interpret the 
request.
+        */
+       protected RestRequest createRequest(HttpServletRequest req) throws 
ServletException {
+               return new RestRequest(context, req);
+       }
+
+       /**
+        * Creates a {@link RestResponse} object based on the specified 
incoming {@link HttpServletResponse} object
+        *      and the request returned by {@link 
#createRequest(HttpServletRequest)}.
+        * <p>
+        * Subclasses may choose to override this method to provide a 
specialized response object.
+        *
+        * @param req The request object returned by {@link 
#createRequest(HttpServletRequest)}.
+        * @param res The response object from the {@link 
#service(HttpServletRequest, HttpServletResponse)} method.
+        * @return The wrapped response object.
+        * @throws ServletException If any errors occur trying to interpret the 
request or response.
+        */
+       protected RestResponse createResponse(RestRequest req, 
HttpServletResponse res) throws ServletException {
+               return new RestResponse(context, req, res);
+       }
+
+       /**
+        * The main service method.
+        * <p>
+        * Subclasses can optionally override this method if they want to 
tailor the behavior of requests.
+        *
+        * @param r1 The incoming HTTP servlet request object.
+        * @param r2 The incoming HTTP servlet response object.
+        * @throws ServletException
+        * @throws IOException
+        */
+       protected void service(HttpServletRequest r1, HttpServletResponse r2) 
throws ServletException, IOException {
+
+               logger.log(FINE, "HTTP: {0} {1}", r1.getMethod(), 
r1.getRequestURI());
+               long startTime = System.currentTimeMillis();
+
+               try {
+                       context.checkForInitException();
+
+                       String pathInfo = RestUtils.getPathInfoUndecoded(r1);  
// Can't use r1.getPathInfo() because we don't want '%2F' resolved.
+
+                       // If this resource has child resources, try to 
recursively call them.
+                       if (pathInfo != null && context.hasChildResources() && 
(! pathInfo.equals("/"))) {
+                               int i = pathInfo.indexOf('/', 1);
+                               String pathInfoPart = i == -1 ? 
pathInfo.substring(1) : pathInfo.substring(1, i);
+                               RestContext childResource = 
context.getChildResource(pathInfoPart);
+                               if (childResource != null) {
+                                       final String pathInfoRemainder = (i == 
-1 ? null : pathInfo.substring(i));
+                                       final String servletPath = 
r1.getServletPath() + "/" + pathInfoPart;
+                                       final HttpServletRequest childRequest = 
new HttpServletRequestWrapper(r1) {
+                                               @Override /* ServletRequest */
+                                               public String getPathInfo() {
+                                                       return 
RestUtils.decode(pathInfoRemainder);
+                                               }
+                                               @Override /* ServletRequest */
+                                               public String getServletPath() {
+                                                       return servletPath;
+                                               }
+                                       };
+                                       
childResource.getCallHandler().service(childRequest, r2);
+                                       return;
+                               }
+                       }
+
+                       RestRequest req = createRequest(r1);
+                       RestResponse res = createResponse(req, r2);
+                       String method = req.getMethod();
+                       String methodUC = method.toUpperCase(Locale.ENGLISH);
+
+                       StreamResource r = null;
+                       if (pathInfo != null) {
+                               String p = pathInfo.substring(1);
+                               if (p.equals("favicon.ico"))
+                                       r = context.getFavIcon();
+                               else if (p.equals("style.css"))
+                                       r = context.getStyleSheet();
+                               else if (context.isStaticFile(p))
+                                       r = context.resolveStaticFile(p);
+                       }
+
+                       if (r != null) {
+                               res.setStatus(SC_OK);
+                               res.setOutput(r);
+                       } else {
+                               // If the specified method has been defined in 
a subclass, invoke it.
+                               int rc = SC_METHOD_NOT_ALLOWED;
+                               if (callRouters.containsKey(methodUC)) {
+                                       rc = 
callRouters.get(methodUC).invoke(pathInfo, req, res);
+                               } else if (callRouters.containsKey("*")) {
+                                       rc = 
callRouters.get("*").invoke(pathInfo, req, res);
+                               }
+
+                               // If not invoked above, see if it's an OPTIONs 
request
+                               if (rc != SC_OK)
+                                       handleNotFound(rc, req, res);
+                       }
+
+                       if (res.hasOutput()) {
+                               Object output = res.getOutput();
+
+                               // Do any class-level transforming.
+                               for (RestConverter converter : 
context.getConverters())
+                                       output = converter.convert(req, output, 
context.getBeanContext().getClassMetaForObject(output));
+
+                               res.setOutput(output);
+
+                               // Now serialize the output if there was any.
+                               // Some subclasses may write to the 
OutputStream or Writer directly.
+                               handleResponse(req, res, output);
+                       }
+
+                       onSuccess(req, res, System.currentTimeMillis() - 
startTime);
+
+               } catch (RestException e) {
+                       handleError(r1, r2, e);
+               } catch (Throwable e) {
+                       handleError(r1, r2, new 
RestException(SC_INTERNAL_SERVER_ERROR, e));
+               }
+               logger.log(FINE, "HTTP: [{0} {1}] finished in {2}ms", 
r1.getMethod(), r1.getRequestURI(), System.currentTimeMillis()-startTime);
+       }
+
+       /**
+        * The main method for serializing POJOs passed in through the {@link 
RestResponse#setOutput(Object)} method or returned by
+        * the Java method.
+        * <p>
+        * Subclasses may override this method if they wish to modify the way 
the output is rendered or support
+        *      other output formats.
+        * <p>
+        * The default implementation simply iterates through the response 
handlers on this resource
+        * looking for the first one whose {@link 
ResponseHandler#handle(RestRequest, RestResponse, Object)} method returns 
<jk>true</jk>.
+        *
+        * @param req The HTTP request.
+        * @param res The HTTP response.
+        * @param output The output to serialize in the response.
+        * @throws IOException
+        * @throws RestException
+        */
+       protected void handleResponse(RestRequest req, RestResponse res, Object 
output) throws IOException, RestException {
+               // Loop until we find the correct handler for the POJO.
+               for (ResponseHandler h : context.getResponseHandlers())
+                       if (h.handle(req, res, output))
+                               return;
+               throw new RestException(SC_NOT_IMPLEMENTED, "No response 
handlers found to process output of type '"+(output == null ? null : 
output.getClass().getName())+"'");
+       }
+
+       /**
+        * Handle the case where a matching method was not found.
+        * <p>
+        * Subclasses can override this method to provide a 2nd-chance for 
specifying a response.
+        * The default implementation will simply throw an exception with an 
appropriate message.
+        *
+        * @param rc The HTTP response code.
+        * @param req The HTTP request.
+        * @param res The HTTP response.
+        * @throws Exception
+        */
+       protected void handleNotFound(int rc, RestRequest req, RestResponse 
res) throws Exception {
+               String pathInfo = req.getPathInfo();
+               String methodUC = req.getMethod();
+               String onPath = pathInfo == null ? " on no pathInfo"  : 
String.format(" on path '%s'", pathInfo);
+               if (rc == SC_NOT_FOUND)
+                       throw new RestException(rc, "Method ''{0}'' not found 
on resource with matching pattern{1}.", methodUC, onPath);
+               else if (rc == SC_PRECONDITION_FAILED)
+                       throw new RestException(rc, "Method ''{0}'' not found 
on resource{1} with matching matcher.", methodUC, onPath);
+               else if (rc == SC_METHOD_NOT_ALLOWED)
+                       throw new RestException(rc, "Method ''{0}'' not found 
on resource.", methodUC);
+               else
+                       throw new ServletException("Invalid method response: " 
+ rc);
+       }
+
+       /**
+        * Method for handling response errors.
+        * <p>
+        * The default implementation logs the error and calls {@link 
#renderError(HttpServletRequest,HttpServletResponse,RestException)}.
+        * <p>
+        * Subclasses can override this method to provide their own custom 
error response handling.
+        *
+        * @param req The servlet request.
+        * @param res The servlet response.
+        * @param e The exception that occurred.
+        * @throws IOException Can be thrown if a problem occurred trying to 
write to the output stream.
+        */
+       protected synchronized void handleError(HttpServletRequest req, 
HttpServletResponse res, RestException e) throws IOException {
+               e.setOccurrence(context == null ? 0 : 
context.getStackTraceOccurrence(e));
+               logger.onError(req, res, e);
+               renderError(req, res, e);
+       }
+
+       /**
+        * Method for rendering response errors.
+        * <p>
+        * The default implementation renders a plain text English message, 
optionally with a stack trace
+        *      if {@link RestContext#REST_renderResponseStackTraces} is 
enabled.
+        * <p>
+        * Subclasses can override this method to provide their own custom 
error response handling.
+        *
+        * @param req The servlet request.
+        * @param res The servlet response.
+        * @param e The exception that occurred.
+        * @throws IOException Can be thrown if a problem occurred trying to 
write to the output stream.
+        */
+       protected void renderError(HttpServletRequest req, HttpServletResponse 
res, RestException e) throws IOException {
+
+               int status = e.getStatus();
+               res.setStatus(status);
+               res.setContentType("text/plain");
+               res.setHeader("Content-Encoding", "identity");
+               PrintWriter w = null;
+               try {
+                       w = res.getWriter();
+               } catch (IllegalStateException e2) {
+                       w = new PrintWriter(new 
OutputStreamWriter(res.getOutputStream(), IOUtils.UTF8));
+               }
+               String httpMessage = RestUtils.getHttpResponseText(status);
+               if (httpMessage != null)
+                       w.append("HTTP 
").append(String.valueOf(status)).append(": 
").append(httpMessage).append("\n\n");
+               if (context != null && context.isRenderResponseStackTraces())
+                       e.printStackTrace(w);
+               else
+                       w.append(e.getFullStackMessage(true));
+               w.flush();
+               w.close();
+       }
+
+       /**
+        * Callback method for listening for successful completion of requests.
+        * <p>
+        * Subclasses can override this method for gathering performance 
statistics.
+        * <p>
+        * The default implementation does nothing.
+        *
+        * @param req The HTTP request.
+        * @param res The HTTP response.
+        * @param time The time in milliseconds it took to process the request.
+        */
+       protected void onSuccess(RestRequest req, RestResponse res, long time) {
+               if (restServlet != null)
+                       restServlet.onSuccess(req, res, time);
+       }
+
+       /**
+        * Callback method that gets invoked right before the REST Java method 
is invoked.
+        * <p>
+        * Subclasses can override this method to override request headers or 
set request-duration properties
+        *      before the Java method is invoked.
+        *
+        * @param req The HTTP servlet request object.
+        * @throws RestException If any error occurs.
+        */
+       protected void onPreCall(RestRequest req) throws RestException {
+               if (restServlet != null)
+                       restServlet.onPreCall(req);
+       }
+
+       /**
+        * Callback method that gets invoked right after the REST Java method 
is invoked, but before
+        *      the serializer is invoked.
+        * <p>
+        * Subclasses can override this method to override request and response 
headers, or
+        *      set/override properties used by the serializer.
+        *
+        * @param req The HTTP servlet request object.
+        * @param res The HTTP servlet response object.
+        * @throws RestException If any error occurs.
+        */
+       protected void onPostCall(RestRequest req, RestResponse res) throws 
RestException {
+               if (restServlet != null)
+                       restServlet.onPostCall(req, res);
+       }
+
+       /**
+        * Returns the session objects for the specified request.
+        * <p>
+        * The default implementation simply returns a single map containing 
<code>{'req':req}</code>.
+        *
+        * @param req The REST request.
+        * @return The session objects for that request.
+        */
+       public Map<String,Object> getSessionObjects(RestRequest req) {
+               Map<String,Object> m = new HashMap<String,Object>();
+               m.put(RequestVar.SESSION_req, req);
+               return m;
+       }
+}

Reply via email to