You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2017/03/17 02:17:11 UTC

[6/7] incubator-juneau git commit: Allow @RestResource/@RestMethod annotations to be used on any classes.

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;
+	}
+}