JUNEAU-132, JUNEAU-135

Various HTTP parameter annotations should support Optional.
@RestMethod(debug=true) is not causing requests to be logged.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
index 1efadd5..3fc4797 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/oapi/OpenApiParserSession.java
@@ -71,9 +71,13 @@
 	@Override /* HttpPartParser */

 	public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException, SchemaValidationException {

 		boolean isOptional = type.isOptional();

-		while (isOptional)

+

+		while (type != null && type.isOptional())

 			type = (ClassMeta<T>)type.getElementType();

 

+		if (type == null)

+			type = (ClassMeta<T>)object();

+

 		schema = ObjectUtils.firstNonNull(schema, getSchema(), DEFAULT_SCHEMA);

 

 		T t = parseInner(partType, schema, in, type);

diff --git a/juneau-doc/docs/ReleaseNotes/8.1.1.html b/juneau-doc/docs/ReleaseNotes/8.1.1.html
index 4fcfa34..03addd5 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.1.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.1.html
@@ -27,3 +27,13 @@
 		Fixed a bug in the parsers where the generic subtype of a complex bean property type involving both collections and arrays
 		was not being found.  (e.g. <c>List&lt;Long&gt;[]</c>)
 </ul>
+
+<h5 class='topic w800'>juneau-rest-server</h5>
+<ul class='spaced-list'>
+	<li>
+		Support for {@link Optional} on method parameters annotated with {@link oaj.http.annotation.Header}, {@link oaj.http.annotation.FormData},  
+		{@link oaj.http.annotation.Query}, {@link oaj.http.annotation.Path}, and {@link oaj.http.annotation.PathRemainder}.
+	<li>
+		Fixed issue where {@link oajr.annotation.RestRequest#debug() RestRequest.debug()} annotation wasn't resulting
+		in the HTTP request being logged.
+</ul>
diff --git a/juneau-doc/docs/Topics/02.juneau-marshall/26.OpenApiDetails/03.Parsers.html b/juneau-doc/docs/Topics/02.juneau-marshall/26.OpenApiDetails/03.Parsers.html
index e54e6c3..086f0b3 100644
--- a/juneau-doc/docs/Topics/02.juneau-marshall/26.OpenApiDetails/03.Parsers.html
+++ b/juneau-doc/docs/Topics/02.juneau-marshall/26.OpenApiDetails/03.Parsers.html
@@ -216,6 +216,10 @@
 	</tr>
 </table>
 <p>
+	Additionally, any of the type above can also be wrapped as {@link java.util.Optional Optionals}.
+</p>
+
+<p>
 	For arrays, an example of "Any POJO transformable from arrays of the default types" is:
 </p>
 <p class='bpcode w800'>
diff --git a/juneau-doc/docs/Topics/07.juneau-rest-server/10.HttpPartAnnotations/01.Body.html b/juneau-doc/docs/Topics/07.juneau-rest-server/10.HttpPartAnnotations/01.Body.html
index 082e4e9..37dc260 100644
--- a/juneau-doc/docs/Topics/07.juneau-rest-server/10.HttpPartAnnotations/01.Body.html
+++ b/juneau-doc/docs/Topics/07.juneau-rest-server/10.HttpPartAnnotations/01.Body.html
@@ -98,6 +98,8 @@
 			<li><c><jk>public static</jk> T <jsm>forString</jsm>(String in) {...}</c>
 		</ul>
 		Note that this also includes all enums.
+	<li>
+		Any {@link java.util.Optional} of anything on this list.
 </ol>
 <p>
 	The {@link oaj.oapi.OpenApiSerializer} class can be used to serialize HTTP bodies to OpenAPI-based output.
diff --git a/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/ABean.java b/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/ABean.java
index 439937a..bde2c9a 100644
--- a/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/ABean.java
+++ b/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/ABean.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.testutils;
 
+import org.apache.juneau.marshall.*;
+
 public class ABean {
 	public int a;
 	public String b;
@@ -21,4 +23,9 @@
 		this.b = "foo";
 		return this;
 	}
+
+	@Override
+	public String toString() {
+		return SimpleJson.DEFAULT.toString(this);
+	}
 }
\ No newline at end of file
diff --git a/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/TestUtils.java b/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/TestUtils.java
index 10232b0..445204d 100755
--- a/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/TestUtils.java
+++ b/juneau-rest/juneau-rest-mock-utest/src/test/java/org/apache/juneau/rest/testutils/TestUtils.java
@@ -61,7 +61,7 @@
 	public static Swagger getSwagger(Class<?> c) {

 		try {

 			RestContext rc = RestContext.create(c.newInstance()).build();

-			RestRequest req = rc.getCallHandler().createRequest(new MockServletRequest());

+			RestRequest req = rc.getCallHandler().createRequest(new RestCall(new MockServletRequest(), null));

 			RestInfoProvider ip = rc.getInfoProvider();

 			return ip.getSwagger(req);

 		} catch (Exception e) {

diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestInfoProviderTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestInfoProviderTest.java
index 8699220..d3cdec8 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestInfoProviderTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestInfoProviderTest.java
@@ -52,14 +52,14 @@
 
 	private Swagger getSwaggerWithFile(Object resource) throws Exception {
 		RestContext rc = RestContext.create(resource).classpathResourceFinder(TestClasspathResourceFinder.class).build();
-		RestRequest req = rc.getCallHandler().createRequest(new MockServletRequest());
+		RestRequest req = rc.getCallHandler().createRequest(new RestCall(new MockServletRequest(), null));
 		RestInfoProvider ip = rc.getInfoProvider();
 		return ip.getSwagger(req);
 	}
 
 	private static Swagger getSwagger(Object resource) throws Exception {
 		RestContext rc = RestContext.create(resource).build();
-		RestRequest req = rc.getCallHandler().createRequest(new MockServletRequest());
+		RestRequest req = rc.getCallHandler().createRequest(new RestCall(new MockServletRequest(), null));
 		RestInfoProvider ip = rc.getInfoProvider();
 		return ip.getSwagger(req);
 	}
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/BodyAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/BodyAnnotationTest.java
index c8c4d19..a7476a9 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/BodyAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/BodyAnnotationTest.java
@@ -14,7 +14,7 @@
 
 import static org.apache.juneau.http.HttpMethodName.*;
 import static org.apache.juneau.rest.testutils.TestUtils.*;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.io.*;
 import java.util.*;
@@ -26,12 +26,14 @@
 import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.jsonschema.annotation.*;
+import org.apache.juneau.marshall.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.mock2.*;
 import org.apache.juneau.rest.testutils.*;
 import org.apache.juneau.rest.testutils.DTOs;
 import org.apache.juneau.uon.*;
 import org.apache.juneau.urlencoding.*;
+import org.apache.juneau.utils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -781,6 +783,59 @@
 		i.post("/", "{}").json().execute().assertStatus(200);
 	}
 
+	//=================================================================================================================
+	// Optional body parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class,parsers=JsonParser.class)
+	public static class J {
+		@RestMethod(name=POST,path="/a")
+		public Object a(@Body Optional<Integer> body) throws Exception {
+			assertNotNull(body);
+			return body;
+		}
+		@RestMethod(name=POST,path="/b")
+		public Object b(@Body Optional<ABean> body) throws Exception {
+			assertNotNull(body);
+			return body;
+		}
+		@RestMethod(name=POST,path="/c")
+		public Object c(@Body Optional<List<ABean>> body) throws Exception {
+			assertNotNull(body);
+			return body;
+		}
+		@RestMethod(name=POST,path="/d")
+		public Object d(@Body List<Optional<ABean>> body) throws Exception {
+			return body;
+		}
+	}
+	static MockRest j = MockRest.create(J.class).json().build();;
+
+	@Test
+	public void j01_optionalParam_integer() throws Exception {
+		j.post("/a", "123").execute().assertStatus(200).assertBody("123");
+		j.post("/a", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void j02_optionalParam_bean() throws Exception {
+		j.post("/b", new ABean().init()).execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+		j.post("/b", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void j03_optionalParam_listOfBeans() throws Exception {
+		String body = SimpleJson.DEFAULT.toString(AList.of(new ABean().init()));
+		j.post("/c", body).execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		j.post("/c", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void j04_optionalParam_listOfOptionals() throws Exception {
+		String body = SimpleJson.DEFAULT.toString(AList.of(Optional.of(new ABean().init())));
+		j.post("/d", body).execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		j.post("/d", "null").execute().assertStatus(200).assertBody("null");
+	}
 
 	//=================================================================================================================
 	// Swagger - @Body on POJO
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/FormDataAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/FormDataAnnotationTest.java
index 86f7c50..f5231a8 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/FormDataAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/FormDataAnnotationTest.java
@@ -14,16 +14,18 @@
 
 import static org.apache.juneau.http.HttpMethodName.*;
 import static org.apache.juneau.rest.testutils.TestUtils.*;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.dto.swagger.*;
-import org.apache.juneau.http.annotation.FormData;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.rest.testutils.*;
 import org.apache.juneau.urlencoding.*;
 import org.junit.*;
 import org.junit.runners.*;
@@ -156,6 +158,59 @@
 	}
 
 	//=================================================================================================================
+	// Optional form data parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class)
+	public static class D {
+		@RestMethod(name=POST,path="/a")
+		public Object a(@FormData("f1") Optional<Integer> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=POST,path="/b")
+		public Object b(@FormData("f1") Optional<ABean> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=POST,path="/c")
+		public Object c(@FormData("f1") Optional<List<ABean>> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=POST,path="/d")
+		public Object d(@FormData("f1") List<Optional<ABean>> f1) throws Exception {
+			return f1;
+		}
+	}
+	static MockRest d = MockRest.create(D.class).accept("application/json").contentType("application/x-www-form-urlencoded").build();
+
+	@Test
+	public void d01_optionalParam_integer() throws Exception {
+		d.post("/a", "f1=123").execute().assertStatus(200).assertBody("123");
+		d.post("/a", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void d02_optionalParam_bean() throws Exception {
+		d.post("/b", "f1=(a=1,b=foo)").execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+		d.post("/b", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void d03_optionalParam_listOfBeans() throws Exception {
+		d.post("/c", "f1=@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		d.post("/c", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void d04_optionalParam_listOfOptionals() throws Exception {
+		d.post("/d", "f1=@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		d.post("/d", "null").execute().assertStatus(200).assertBody("null");
+	}
+
+
+	//=================================================================================================================
 	// @FormData on POJO
 	//=================================================================================================================
 
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/HeaderAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/HeaderAnnotationTest.java
index 35d44a2..40909ab 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/HeaderAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/HeaderAnnotationTest.java
@@ -12,14 +12,18 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.annotation2;
 
+import static org.apache.juneau.http.HttpMethodName.*;
 import static org.apache.juneau.rest.testutils.TestUtils.*;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.util.*;
 
 import org.apache.juneau.dto.swagger.*;
-import org.apache.juneau.http.annotation.Header;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.rest.testutils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -31,6 +35,59 @@
 public class HeaderAnnotationTest {
 
 	//=================================================================================================================
+	// Optional header parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class)
+	public static class A {
+		@RestMethod(name=GET,path="/a")
+		public Object a(@Header("f1") Optional<Integer> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/b")
+		public Object b(@Header("f1") Optional<ABean> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/c")
+		public Object c(@Header("f1") Optional<List<ABean>> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/d")
+		public Object d(@Header("f1") List<Optional<ABean>> f1) throws Exception {
+			return f1;
+		}
+	}
+	static MockRest a = MockRest.create(A.class).json().build();
+
+	@Test
+	public void a01_optionalParam_integer() throws Exception {
+		a.get("/a").header("f1", 123).execute().assertStatus(200).assertBody("123");
+		a.get("/a").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void a02_optionalParam_bean() throws Exception {
+		a.get("/b").header("f1", "(a=1,b=foo)").execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+		a.get("/b").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void a03_optionalParam_listOfBeans() throws Exception {
+		a.get("/c").header("f1", "@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		a.get("/c").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void a04_optionalParam_listOfOptionals() throws Exception {
+		a.get("/d").header("f1", "@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		a.get("/d").execute().assertStatus(200).assertBody("null");
+	}
+
+
+	//=================================================================================================================
 	// @Header on POJO
 	//=================================================================================================================
 
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathAnnotationTest.java
index b2c6e1c..a53dd5e 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathAnnotationTest.java
@@ -14,17 +14,19 @@
 
 import static org.apache.juneau.http.HttpMethodName.*;
 import static org.apache.juneau.rest.testutils.TestUtils.*;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.dto.swagger.*;
-import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.marshall.*;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.rest.testutils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -677,6 +679,55 @@
 	}
 
 	//=================================================================================================================
+	// Optional path parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class)
+	public static class J {
+		@RestMethod(name=GET,path="/a/{f1}")
+		public Object a(@Path("f1") Optional<Integer> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/b/{f1}")
+		public Object b(@Path("f1") Optional<ABean> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/c/{f1}")
+		public Object c(@Path("f1") Optional<List<ABean>> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/d/{f1}")
+		public Object d(@Path("f1") List<Optional<ABean>> f1) throws Exception {
+			return f1;
+		}
+	}
+	static MockRest j = MockRest.create(J.class).json().build();
+
+	@Test
+	public void j01_optionalParam_integer() throws Exception {
+		j.get("/a/123").execute().assertStatus(200).assertBody("123");
+	}
+
+	@Test
+	public void j02_optionalParam_bean() throws Exception {
+		j.get("/b/(a=1,b=foo)").execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+	}
+
+	@Test
+	public void j03_optionalParam_listOfBeans() throws Exception {
+		j.get("/c/@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+	}
+
+	@Test
+	public void j04_optionalParam_listOfOptionals() throws Exception {
+		j.get("/d/@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+	}
+
+
+	//=================================================================================================================
 	// @Path on POJO
 	//=================================================================================================================
 
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathRemainderAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathRemainderAnnotationTest.java
index e4a5c0b..3d64ea7 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathRemainderAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/PathRemainderAnnotationTest.java
@@ -13,10 +13,15 @@
 package org.apache.juneau.rest.annotation2;
 
 import static org.apache.juneau.http.HttpMethodName.*;
+import static org.junit.Assert.*;
+
+import java.util.*;
 
 import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.json.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.rest.testutils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -52,4 +57,52 @@
 	public void a03_withRemainder2() throws Exception {
 		a.get("/foo/bar").execute().assertBody("foo/bar");
 	}
+
+	//=================================================================================================================
+	// Optional path remainer parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class)
+	public static class B {
+		@RestMethod(name=GET,path="/a/*")
+		public Object a(@Path("/*") Optional<Integer> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/b/*")
+		public Object b(@Path("/*") Optional<ABean> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/c/*")
+		public Object c(@Path("/*") Optional<List<ABean>> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/d/*")
+		public Object d(@Path("/*") List<Optional<ABean>> f1) throws Exception {
+			return f1;
+		}
+	}
+	static MockRest b = MockRest.create(B.class).json().build();
+
+	@Test
+	public void b01_optionalParam_integer() throws Exception {
+		b.get("/a/123").execute().assertStatus(200).assertBody("123");
+	}
+
+	@Test
+	public void b02_optionalParam_bean() throws Exception {
+		b.get("/b/(a=1,b=foo)").execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+	}
+
+	@Test
+	public void b03_optionalParam_listOfBeans() throws Exception {
+		b.get("/c/@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+	}
+
+	@Test
+	public void b04_optionalParam_listOfOptionals() throws Exception {
+		b.get("/d/@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+	}
 }
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/QueryAnnotationTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/QueryAnnotationTest.java
index b7a48f9..f34b306 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/QueryAnnotationTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation2/QueryAnnotationTest.java
@@ -14,18 +14,19 @@
 
 import static org.apache.juneau.http.HttpMethodName.*;
 import static org.apache.juneau.rest.testutils.TestUtils.*;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.dto.swagger.*;
-import org.apache.juneau.http.annotation.Query;
+import org.apache.juneau.http.annotation.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.jsonschema.annotation.Items;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.rest.mock2.*;
+import org.apache.juneau.rest.testutils.*;
 import org.junit.*;
 import org.junit.runners.*;
 
@@ -267,6 +268,59 @@
 	}
 
 	//=================================================================================================================
+	// Optional query parameter.
+	//=================================================================================================================
+
+	@RestResource(serializers=SimpleJsonSerializer.class)
+	public static class E {
+		@RestMethod(name=GET,path="/a")
+		public Object a(@Query("f1") Optional<Integer> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/b")
+		public Object b(@Query("f1") Optional<ABean> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/c")
+		public Object c(@Query("f1") Optional<List<ABean>> f1) throws Exception {
+			assertNotNull(f1);
+			return f1;
+		}
+		@RestMethod(name=GET,path="/d")
+		public Object d(@Query("f1") List<Optional<ABean>> f1) throws Exception {
+			return f1;
+		}
+	}
+	static MockRest e = MockRest.create(E.class).json().build();
+
+	@Test
+	public void e01_optionalParam_integer() throws Exception {
+		e.get("/a?f1=123").execute().assertStatus(200).assertBody("123");
+		e.get("/a").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void e02_optionalParam_bean() throws Exception {
+		e.get("/b?f1=(a=1,b=foo)").execute().assertStatus(200).assertBody("{a:1,b:'foo'}");
+		e.get("/b").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void e03_optionalParam_listOfBeans() throws Exception {
+		e.get("/c?f1=@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		e.get("/c").execute().assertStatus(200).assertBody("null");
+	}
+
+	@Test
+	public void e04_optionalParam_listOfOptionals() throws Exception {
+		e.get("/d?f1=@((a=1,b=foo))").execute().assertStatus(200).assertBody("[{a:1,b:'foo'}]");
+		e.get("/d").execute().assertStatus(200).assertBody("null");
+	}
+
+
+	//=================================================================================================================
 	// @Query on POJO
 	//=================================================================================================================
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
index 65c4c2a..63f407a 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
@@ -54,36 +54,19 @@
 		this.restCallRouters = context.getCallRouters();
 	}
 
-	/**
-	 * 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.
-	 */
 	@Override /* RestCallHandler */
-	public RestRequest createRequest(HttpServletRequest req) throws ServletException {
-		return new RestRequest(context, req);
+	public RestCall createCall(HttpServletRequest req, HttpServletResponse res) {
+		return new RestCall(req, res).logger(context.getCallLogger()).loggerConfig(context.getCallLoggerConfig());
 	}
 
-	/**
-	 * 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.
-	 */
 	@Override /* RestCallHandler */
-	public RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException {
-		return new RestResponse(context, req, res);
+	public RestRequest createRequest(RestCall call) throws ServletException {
+		return new RestRequest(context, call.getRequest());
+	}
+
+	@Override /* RestCallHandler */
+	public RestResponse createResponse(RestCall call) throws ServletException {
+		return new RestResponse(context, call.getRestRequest(), call.getResponse());
 	}
 
 	/**
@@ -100,112 +83,94 @@
 	@Override /* RestCallHandler */
 	public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException {
 
-		long startTime = System.currentTimeMillis();
-		RestRequest req = null;
-		RestResponse res = null;
-		RestCallLogger logger = context.getCallLogger();
-		RestCallLoggerConfig loggerConfig = context.getCallLoggerConfig();
+		RestCall call = createCall(r1, r2);
 
 		try {
 			context.checkForInitException();
 
-			String pathInfo = RestUtils.getPathInfoUndecoded(r1);  // Can't use r1.getPathInfo() because we don't want '%2F' resolved.
-			UrlPathInfo upi = new UrlPathInfo(pathInfo);
-
 			// If the resource path contains variables (e.g. @RestResource(path="/f/{a}/{b}"), then we want to resolve
 			// those variables and push the servletPath to include the resolved variables.  The new pathInfo will be
 			// the remainder after the new servletPath.
 			// Only do this for the top-level resource because the logic for child resources are processed next.
 			if (context.pathPattern.hasVars() && context.getParentContext() == null) {
-				String sp = r1.getServletPath();
-				UrlPathInfo upi2 = new UrlPathInfo(pathInfo == null ? sp : sp + pathInfo);
+				String sp = call.getServletPath();
+				String pi = call.getPathInfoUndecoded();
+				UrlPathInfo upi2 = new UrlPathInfo(pi == null ? sp : sp + pi);
 				UrlPathPatternMatch uppm = context.pathPattern.match(upi2);
 				if (uppm != null && ! uppm.hasEmptyVars()) {
-					RequestPath.addPathVars(r1, uppm.getVars());
-					r1 = new OverrideableHttpServletRequest(r1)
-						.pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix())))
-						.servletPath(uppm.getPrefix());
-					pathInfo = RestUtils.getPathInfoUndecoded(r1);  // Can't use r1.getPathInfo() because we don't want '%2F' resolved.
-					upi = new UrlPathInfo(pathInfo);
+					RequestPath.addPathVars(call.getRequest(), uppm.getVars());
+					call.request(
+						new OverrideableHttpServletRequest(call.getRequest())
+							.pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix())))
+							.servletPath(uppm.getPrefix())
+					);
 				} else {
-					if (isDebug(r1)) {
-						r1 = CachingHttpServletRequest.wrap(r1);
-						r1.setAttribute("Debug", true);
-					}
-					r2.setStatus(SC_NOT_FOUND);
+					call.debug(isDebug(call)).status(SC_NOT_FOUND).finish();
 					return;
 				}
 			}
 
 			// If this resource has child resources, try to recursively call them.
-			if (context.hasChildResources() && pathInfo != null && ! pathInfo.equals("/")) {
+			String pi = call.getPathInfoUndecoded();
+			if (context.hasChildResources() && pi != null && ! pi.equals("/")) {
 				for (RestContext rc : context.getChildResources().values()) {
 					UrlPathPattern upp = rc.pathPattern;
-					UrlPathPatternMatch uppm = upp.match(upi);
+					UrlPathPatternMatch uppm = upp.match(call.getUrlPathInfo());
 					if (uppm != null) {
 						if (! uppm.hasEmptyVars()) {
-							RequestPath.addPathVars(r1, uppm.getVars());
-							HttpServletRequest childRequest = new OverrideableHttpServletRequest(r1)
+							RequestPath.addPathVars(call.getRequest(), uppm.getVars());
+							HttpServletRequest childRequest = new OverrideableHttpServletRequest(call.getRequest())
 								.pathInfo(nullIfEmpty(urlDecode(uppm.getSuffix())))
-								.servletPath(r1.getServletPath() + uppm.getPrefix());
-							rc.getCallHandler().service(childRequest, r2);
+								.servletPath(call.getServletPath() + uppm.getPrefix());
+							rc.getCallHandler().service(childRequest, call.getResponse());
 						} else {
-							if (isDebug(r1)) {
-								r1 = CachingHttpServletRequest.wrap(r1);
-								r1.setAttribute("Debug", true);
-							}
-							r2.setStatus(SC_NOT_FOUND);
+							call.debug(isDebug(call)).status(SC_NOT_FOUND).finish();
 						}
 						return;
 					}
 				}
 			}
 
-			if (isDebug(r1)) {
-				r1 = CachingHttpServletRequest.wrap(r1);
-				r2 = CachingHttpServletResponse.wrap(r2);
-				r1.setAttribute("Debug", true);
-			}
+			call.debug(isDebug(call));
 
-			context.startCall(r1, r2);
+			context.startCall(call);
 
-			req = createRequest(r1);
-			res = createResponse(req, r2);
-			req.setResponse(res);
-			context.setRequest(req);
-			context.setResponse(res);
-			String method = req.getMethod();
-			String methodUC = method.toUpperCase(Locale.ENGLISH);
+			call.restRequest(createRequest(call));
+			call.restResponse(createResponse(call));
+
+			context.setRequest(call.getRestRequest());
+			context.setResponse(call.getRestResponse());
 
 			StreamResource r = null;
-			if (pathInfo != null) {
-				String p = pathInfo.substring(1);
+			if (call.getPathInfoUndecoded() != null) {
+				String p = call.getPathInfoUndecoded().substring(1);
 				if (context.isStaticFile(p)) {
 					StaticFile sf = context.resolveStaticFile(p);
 					r = sf.resource;
-					res.setResponseMeta(sf.meta);
+					call.responseMeta(sf.meta);
 				} else if (p.equals("favicon.ico")) {
-					res.setOutput(null);
+					call.output(null);
 				}
 			}
 
 			if (r != null) {
-				res.setStatus(SC_OK);
-				res.setOutput(r);
+				call.status(SC_OK);
+				call.output(r);
 			} else {
 
 				// If the specified method has been defined in a subclass, invoke it.
 				int rc = 0;
-				if (restCallRouters.containsKey(methodUC)) {
-					rc = restCallRouters.get(methodUC).invoke(upi, req, res);
+				String m = call.getMethod();
+				if (restCallRouters.containsKey(m)) {
+					rc = restCallRouters.get(m).invoke(call);
 				} else if (restCallRouters.containsKey("*")) {
-					rc = restCallRouters.get("*").invoke(upi, req, res);
+					rc = restCallRouters.get("*").invoke(call);
 				}
 
 				// Should be 405 if the URL pattern matched but HTTP method did not.
 				if (rc == 0)
 					for (RestCallRouter rcc : restCallRouters.values())
-						if (rcc.matches(upi))
+						if (rcc.matches(call))
 							rc = SC_METHOD_NOT_ALLOWED;
 
 				// Should be 404 if URL pattern didn't match.
@@ -214,54 +179,36 @@
 
 				// If not invoked above, see if it's an OPTIONs request
 				if (rc != SC_OK)
-					handleNotFound(rc, req, res);
+					handleNotFound(call.status(rc));
 
-				if (res.getStatus() == 0)
-					res.setStatus(rc);
+				if (call.getStatus() == 0)
+					call.status(rc);
 			}
 
-			if (res.hasOutput()) {
+			if (call.hasOutput()) {
 				// Now serialize the output if there was any.
 				// Some subclasses may write to the OutputStream or Writer directly.
-				handleResponse(req, res);
+				handleResponse(call);
 			}
 
-			// Make sure our writer in RestResponse gets written.
-			res.flushBuffer();
-			req.close();
-
-			r1 = req.getInner();
-			r2 = res.getInner();
-			loggerConfig = req.getCallLoggerConfig();
-
-			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
 
 		} catch (Throwable e) {
-			e = convertThrowable(e);
-			if (req != null) {
-				r1 = req.getInner();
-				loggerConfig = req.getCallLoggerConfig();
-			}
-			if (res != null)
-				r2 = res.getInner();
-			r1.setAttribute("Exception", e);
-			r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
-			handleError(r1, r2, e);
+			handleError(call, convertThrowable(e));
 		} finally {
 			context.clearState();
 		}
 
-		logger.log(loggerConfig, r1, r2);
-		context.finishCall(r1, r2);
+		call.finish();
+		context.finishCall(call);
 	}
 
-	private boolean isDebug(HttpServletRequest req) {
+	private boolean isDebug(RestCall call) {
 		Enablement e = context.getDebug();
 		if (e == TRUE)
 			return true;
 		if (e == FALSE)
 			return false;
-		return "true".equalsIgnoreCase(req.getHeader("X-Debug"));
+		return "true".equalsIgnoreCase(call.getRequest().getHeader("X-Debug"));
 	}
 
 	/**
@@ -274,20 +221,24 @@
 	 *
 	 * <p>
 	 * The default implementation simply iterates through the response handlers on this resource
-	 * looking for the first one whose {@link ResponseHandler#handle(RestRequest, RestResponse)} method returns
+	 * looking for the first one whose {@link ResponseHandler#handle(RestRequest,RestResponse)} method returns
 	 * <jk>true</jk>.
 	 *
-	 * @param req The HTTP request.
-	 * @param res The HTTP response.
+	 * @param call The HTTP call.
 	 * @throws IOException Thrown by underlying stream.
 	 * @throws RestException Non-200 response.
 	 */
 	@Override /* RestCallHandler */
-	public void handleResponse(RestRequest req, RestResponse res) throws IOException, RestException, NotImplemented {
+	public void handleResponse(RestCall call) throws IOException, RestException, NotImplemented {
+
+		RestRequest req = call.getRestRequest();
+		RestResponse res = call.getRestResponse();
+
 		// Loop until we find the correct handler for the POJO.
 		for (ResponseHandler h : context.getResponseHandlers())
 			if (h.handle(req, res))
 				return;
+
 		Object output = res.getOutput();
 		throw new NotImplemented("No response handlers found to process output of type '"+(output == null ? null : output.getClass().getName())+"'");
 	}
@@ -325,14 +276,13 @@
 	 * 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.
+	 * @param call The HTTP call.
 	 */
 	@Override /* RestCallHandler */
-	public void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception {
-		String pathInfo = req.getPathInfo();
-		String methodUC = req.getMethod();
+	public void handleNotFound(RestCall call) throws Exception {
+		String pathInfo = call.getPathInfo();
+		String methodUC = call.getMethod();
+		int rc = call.getStatus();
 		String onPath = pathInfo == null ? " on no pathInfo"  : String.format(" on path '%s'", pathInfo);
 		if (rc == SC_NOT_FOUND)
 			throw new NotFound("Method ''{0}'' not found on resource with matching pattern{1}.", methodUC, onPath);
@@ -350,18 +300,22 @@
 	 * <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 call The rest call.
 	 * @param e The exception that occurred.
 	 * @throws IOException Can be thrown if a problem occurred trying to write to the output stream.
 	 */
 	@Override /* RestCallHandler */
 	@SuppressWarnings("deprecation")
-	public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, Throwable e) throws IOException {
+	public synchronized void handleError(RestCall call, Throwable e) throws IOException {
+
+		call.exception(e);
 
 		int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e);
 		RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, 500)).setOccurrence(occurrence);
 
+		HttpServletRequest req = call.getRequest();
+		HttpServletResponse res = call.getResponse();
+
 		Throwable t = e2.getRootCause();
 		if (t != null) {
 			res.setHeader("Exception-Name", t.getClass().getName());
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java
new file mode 100644
index 0000000..b43afbf
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java
@@ -0,0 +1,334 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                                                              *
+// *                                                                                                                         *
+// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
+// *                                                                                                                         *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the License.                                              *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.http.*;
+
+import org.apache.juneau.httppart.bean.*;
+import org.apache.juneau.rest.util.*;
+
+/**
+ * A wrapper around a single HttpServletRequest/HttpServletResponse pair.
+ */
+public class RestCall {
+
+	private HttpServletRequest req;
+	private HttpServletResponse res;
+	private RestRequest rreq;
+	private RestResponse rres;
+	private UrlPathInfo urlPathInfo;
+	private String pathInfoUndecoded;
+	private long startTime = System.currentTimeMillis();
+	private RestCallLogger logger;
+	private RestCallLoggerConfig loggerConfig;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param req The incoming HTTP servlet request object.
+	 * @param res The incoming HTTP servlet response object.
+	 */
+	public RestCall(HttpServletRequest req, HttpServletResponse res) {
+		request(req).response(res);
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Request/response objects.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Overrides the request object on the REST call.
+	 *
+	 * @param req The new HTTP servlet request.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall request(HttpServletRequest req) {
+		this.req = req;
+		this.urlPathInfo = null;
+		this.pathInfoUndecoded = null;
+		return this;
+	}
+
+	/**
+	 * Overrides the response object on the REST call.
+	 *
+	 * @param res The new HTTP servlet response.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall response(HttpServletResponse res) {
+		this.res = res;
+		return this;
+	}
+
+	/**
+	 * Set the {@link RestRequest} object on this REST call.
+	 *
+	 * @param rreq The {@link RestRequest} object on this REST call.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall restRequest(RestRequest rreq) {
+		request(rreq);
+		this.rreq = rreq;
+		return this;
+	}
+
+	/**
+	 * Set the {@link RestResponse} object on this REST call.
+	 *
+	 * @param rres The {@link RestResponse} object on this REST call.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall restResponse(RestResponse rres) {
+		response(rres);
+		this.rres = rres;
+		this.rreq.setResponse(rres);
+		return this;
+	}
+
+	/**
+	 * Returns the HTTP servlet request of this REST call.
+	 *
+	 * @return the HTTP servlet request of this REST call.
+	 */
+	public HttpServletRequest getRequest() {
+		return req;
+	}
+
+	/**
+	 * Returns the HTTP servlet response of this REST call.
+	 *
+	 * @return the HTTP servlet response of this REST call.
+	 */
+	public HttpServletResponse getResponse() {
+		return res;
+	}
+
+	/**
+	 * Returns the REST request of this REST call.
+	 *
+	 * @return the REST request of this REST call.
+	 */
+	public RestRequest getRestRequest() {
+		return rreq;
+	}
+
+	/**
+	 * Returns the REST response of this REST call.
+	 *
+	 * @return the REST response of this REST call.
+	 */
+	public RestResponse getRestResponse() {
+		return rres;
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Setters.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Sets the logger to use when logging this call.
+	 *
+	 * @param logger The logger to use when logging this call.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall logger(RestCallLogger logger) {
+		this.logger = logger;
+		return this;
+	}
+
+	/**
+	 * Sets the logging configuration to use when logging this call.
+	 *
+	 * @param config The logging configuration to use when logging this call.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall loggerConfig(RestCallLoggerConfig config) {
+		this.loggerConfig = config;
+		return this;
+	}
+
+	/**
+	 * Enables or disabled debug mode on this call.
+	 *
+	 * @param b The debug flag value.
+	 * @return This object (for method chaining).
+	 * @throws IOException Occurs if request body could not be cached into memory.
+	 */
+	public RestCall debug(boolean b) throws IOException {
+		if (b) {
+			req = CachingHttpServletRequest.wrap(req);
+			res = CachingHttpServletResponse.wrap(res);
+		}
+		req.setAttribute("Debug", b);
+		return this;
+	}
+
+	/**
+	 * Sets the HTTP status on this call.
+	 *
+	 * @param code The status code.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall status(int code) {
+		res.setStatus(code);
+		return this;
+	}
+
+	/**
+	 * Identifies that an exception occurred during this call.
+	 *
+	 * @param e The thrown exception.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall exception(Throwable e) {
+		req.setAttribute("Exception", e);
+		return this;
+	}
+
+	/**
+	 * Sets metadata about the response.
+	 *
+	 * @param meta The metadata about the response.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall responseMeta(ResponseBeanMeta meta) {
+		if (rres != null)
+			rres.setResponseMeta(meta);
+		return this;
+	}
+
+	/**
+	 * Sets the output object to serialize as the response of this call.
+	 *
+	 * @param output The response output POJO.
+	 * @return This object (for method chaining).
+	 */
+	public RestCall output(Object output) {
+		if (rres != null)
+			rres.setOutput(output);
+		return this;
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Lifecycle methods.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Called at the end of a call to finish any remaining tasks such as flushing buffers and logging the response.
+	 *
+	 * @return This object (for method chaining).
+	 */
+	public RestCall finish() {
+		try {
+			res.flushBuffer();
+			req.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
+			if (rreq != null)
+				rreq.close();
+		} catch (Exception e) {
+			exception(e);
+		}
+		if (logger != null)
+			logger.log(loggerConfig, req, res);
+		return this;
+	}
+
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Pass-through convenience methods.
+	//------------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Shortcut for calling <c>getRequest().getServletPath()</c>.
+	 *
+	 * @return The request servlet path.
+	 */
+	public String getServletPath() {
+		return req.getServletPath();
+	}
+
+	/**
+	 * Returns the request path info as a {@link UrlPathInfo} bean.
+	 *
+	 * @return The request path info as a {@link UrlPathInfo} bean.
+	 */
+	public UrlPathInfo getUrlPathInfo() {
+		if (urlPathInfo == null)
+			urlPathInfo = new UrlPathInfo(getPathInfoUndecoded());
+		return urlPathInfo;
+	}
+
+	/**
+	 * Shortcut for calling <c>getRequest().getPathInfo()</c>.
+	 *
+	 * @return The request servlet path info.
+	 */
+	public String getPathInfo() {
+		return req.getPathInfo();
+	}
+
+	/**
+	 * Same as {@link #getPathInfo()} but doesn't decode encoded characters.
+	 *
+	 * @return The undecoded request servlet path info.
+	 */
+	public String getPathInfoUndecoded() {
+		if (pathInfoUndecoded == null)
+			pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req);
+		return pathInfoUndecoded;
+	}
+
+	/**
+	 * Returns the HTTP method name.
+	 *
+	 * @return The HTTP method name, always uppercased.
+	 */
+	public String getMethod() {
+		if (rreq != null)
+			return rreq.getMethod().toUpperCase(Locale.ENGLISH);
+		return req.getMethod().toUpperCase(Locale.ENGLISH);
+	}
+
+	/**
+	 * Shortcut for calling <c>getRequest().getStatus()</c>.
+	 *
+	 * @return The response status code.
+	 */
+	public int getStatus() {
+		return res.getStatus();
+	}
+
+	/**
+	 * Shortcut for calling <c>getRestResponse().hasOutput()</c>.
+	 *
+	 * @return <jk>true</jk> if response has output.
+	 */
+	public boolean hasOutput() {
+		if (rres != null)
+			return rres.hasOutput();
+		return false;
+	}
+
+	/**
+	 * Shortcut for calling <c>getRestResponse().getOutput()</c>.
+	 *
+	 * @return The response output.
+	 */
+	public Object getOutput() {
+		if (rres != null)
+			return rres.getOutput();
+		return null;
+	}
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
index 7587db5..6946f12 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallHandler.java
@@ -38,26 +38,6 @@
 	public interface Null extends RestCallHandler {}
 
 	/**
-	 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} 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.
-	 */
-	public RestRequest createRequest(HttpServletRequest req) throws ServletException;
-
-	/**
-	 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object
-	 * and the request returned by {@link #createRequest(HttpServletRequest)}.
-	 *
-	 * @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.
-	 */
-	public RestResponse createResponse(RestRequest req, HttpServletResponse res) throws ServletException;
-
-	/**
 	 * The main service method.
 	 *
 	 * @param r1 The incoming HTTP servlet request object.
@@ -68,34 +48,58 @@
 	public void service(HttpServletRequest r1, HttpServletResponse r2) throws ServletException, IOException;
 
 	/**
+	 * Wraps an incoming servlet request/response pair into a single {@link RestCall} object.
+	 *
+	 * @param req The rest request.
+	 * @param res The rest response.
+	 * @return The wrapped request/response pair.
+	 */
+	public RestCall createCall(HttpServletRequest req, HttpServletResponse res);
+
+	/**
+	 * Creates a {@link RestRequest} object based on the specified incoming {@link HttpServletRequest} object.
+	 *
+	 * @param call The current REST call.
+	 * @return The wrapped request object.
+	 * @throws ServletException If any errors occur trying to interpret the request.
+	 */
+	public RestRequest createRequest(RestCall call) throws ServletException;
+
+	/**
+	 * Creates a {@link RestResponse} object based on the specified incoming {@link HttpServletResponse} object
+	 * and the request returned by {@link #createRequest(RestCall)}.
+	 *
+	 * @param call The current REST call.
+	 * @return The wrapped response object.
+	 * @throws ServletException If any errors occur trying to interpret the request or response.
+	 */
+	public RestResponse createResponse(RestCall call) throws ServletException;
+
+	/**
 	 * The main method for serializing POJOs passed in through the {@link RestResponse#setOutput(Object)} method or
 	 * returned by the Java method.
 	 *
-	 * @param req The HTTP request.
-	 * @param res The HTTP response.
+	 * @param call The current REST call.
 	 * @throws Exception Can be thrown if error occurred while handling response.
 	 */
-	public void handleResponse(RestRequest req, RestResponse res) throws Exception;
+	public void handleResponse(RestCall call) throws Exception;
 
 	/**
 	 * Handle the case where a matching method was not found.
 	 *
-	 * @param rc The HTTP response code.
-	 * @param req The HTTP request.
-	 * @param res The HTTP response.
+	 * @param call The current REST call.
 	 * @throws Exception Can be thrown if error occurred while handling response.
 	 */
-	public void handleNotFound(int rc, RestRequest req, RestResponse res) throws Exception;
+	public void handleNotFound(RestCall call) throws Exception;
 
 	/**
 	 * Method for handling response errors.
 	 *
-	 * @param req The servlet request.
-	 * @param res The servlet response.
+	 * @param call The current REST call.
 	 * @param e The exception that occurred.
 	 * @throws Exception Can be thrown if error occurred while handling response.
 	 */
-	public void handleError(HttpServletRequest req, HttpServletResponse res, Throwable e) throws Exception;
+	public void handleError(RestCall call, Throwable e) throws Exception;
 
 	/**
 	 * Method for converting thrown exceptions into other types before they are handled.
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
index 58e0c63..6aa2fe3 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallRouter.java
@@ -68,9 +68,10 @@
 		}
 	}
 
-	boolean matches(UrlPathInfo pathInfo) {
+	boolean matches(RestCall call) {
+		UrlPathInfo pi = call.getUrlPathInfo();
 		for (RestMethodContext m : restJavaMethods)
-			if (m.matches(pathInfo))
+			if (m.matches(pi))
 				return true;
 		return false;
 	}
@@ -84,13 +85,13 @@
 	 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta)
 	 * @return The HTTP response code.
 	 */
-	int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {
+	int invoke(RestCall call) throws Throwable {
 		if (restJavaMethods.length == 1)
-			return restJavaMethods[0].invoke(pathInfo, req, res);
+			return restJavaMethods[0].invoke(call);
 
 		int maxRc = 0;
 		for (RestMethodContext m : restJavaMethods) {
-			int rc = m.invoke(pathInfo, req, res);
+			int rc = m.invoke(call);
 			if (rc == SC_OK)
 				return SC_OK;
 			maxRc = Math.max(maxRc, rc);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 6d0a2e3..57cb7b2 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -2297,7 +2297,7 @@
 	 * 		<ul>

 	 * 			<li class='jm'>{@link RestContext#isRenderResponseStackTraces() RestContext.isRenderResponseStackTraces()}

 	 * 		</ul>

-	 * 		That method is used by {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)}.

+	 * 		That method is used by {@link BasicRestCallHandler#handleError(RestCall, Throwable)}.

 	 * </ul>

 	 */

 	public static final String REST_renderResponseStackTraces = PREFIX + ".renderResponseStackTraces.b";

@@ -3085,7 +3085,7 @@
 	 * Affects the following methods:

 	 * <ul class='javatree'>

 	 * 	<li class='jm'>{@link RestContext#getStackTraceOccurrence(Throwable) RestContext.getStackTraceOccurrance(Throwable)}

-	 * 	<li class='jm'>{@link RestCallHandler#handleError(HttpServletRequest, HttpServletResponse, Throwable)}

+	 * 	<li class='jm'>{@link RestCallHandler#handleError(RestCall, Throwable)}

 	 * 	<li class='jm'>{@link RestException#getOccurrence()} - Returns the number of times this exception occurred.

 	 * </ul>

 	 *

@@ -3763,20 +3763,20 @@
 							sm = new RestMethodContext(smb) {

 

 								@Override

-								int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {

+								int invoke(RestCall call) throws Throwable {

 

-									int rc = super.invoke(pathInfo, req, res);

+									int rc = super.invoke(call);

 									if (rc != SC_OK)

 										return rc;

 

-									final Object o = res.getOutput();

+									final Object o = call.getOutput();

 

-									if ("GET".equals(req.getMethod())) {

-										res.setOutput(rim.getMethodsByPath().keySet());

+									if ("GET".equals(call.getMethod())) {

+										call.output(rim.getMethodsByPath().keySet());

 										return SC_OK;

 

-									} else if ("POST".equals(req.getMethod())) {

-										String pip = pathInfo.getPath();

+									} else if ("POST".equals(call.getMethod())) {

+										String pip = call.getUrlPathInfo().getPath();

 										if (pip.indexOf('/') != -1)

 											pip = pip.substring(pip.lastIndexOf('/')+1);

 										pip = urlDecode(pip);

@@ -3784,6 +3784,7 @@
 										if (rmm != null) {

 											Method m = rmm.getJavaMethod();

 											try {

+												RestRequest req = call.getRestRequest();

 												// Parse the args and invoke the method.

 												Parser p = req.getBody().getParser();

 												Object[] args = null;

@@ -3795,7 +3796,7 @@
 													}

 												}

 												Object output = m.invoke(o, args);

-												res.setOutput(output);

+												call.output(output);

 												return SC_OK;

 											} catch (Exception e) {

 												throw new InternalServerError(e);

@@ -5099,17 +5100,17 @@
 	/*

 	 * Calls all @RestHook(START) methods.

 	 */

-	void startCall(HttpServletRequest req, HttpServletResponse res) {

+	void startCall(RestCall call) {

 		for (int i = 0; i < startCallMethods.length; i++)

-			startOrFinish(resource, startCallMethods[i], startCallMethodParams[i], req, res);

+			startOrFinish(resource, startCallMethods[i], startCallMethodParams[i], call.getRequest(), call.getResponse());

 	}

 

 	/*

 	 * Calls all @RestHook(FINISH) methods.

 	 */

-	void finishCall(HttpServletRequest req, HttpServletResponse res) {

+	void finishCall(RestCall call) {

 		for (int i = 0; i < endCallMethods.length; i++)

-			startOrFinish(resource, endCallMethods[i], endCallMethodParams[i], req, res);

+			startOrFinish(resource, endCallMethods[i], endCallMethodParams[i], call.getRequest(), call.getResponse());

 	}

 

 	private static void startOrFinish(Object resource, Method m, Class<?>[] p, HttpServletRequest req, HttpServletResponse res) throws RestException, InternalServerError {

diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
index 5a8bd06..aed5bd0 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
@@ -687,13 +687,17 @@
 
 		this.debug = getInstanceProperty(RESTMETHOD_debug, Enablement.class, context.getDebug());
 
-		Object clc = getProperty(RESTMETHOD_callLoggerConfig);
-		if (clc instanceof RestCallLoggerConfig)
-			this.callLoggerConfig = (RestCallLoggerConfig)clc;
-		else if (clc instanceof ObjectMap)
-			this.callLoggerConfig = RestCallLoggerConfig.create().parent(context.getCallLoggerConfig()).apply((ObjectMap)clc).build();
-		else
-			this.callLoggerConfig = context.getCallLoggerConfig();
+		if (debug == Enablement.TRUE) {
+			this.callLoggerConfig = RestCallLoggerConfig.DEFAULT_DEBUG;
+		} else {
+			Object clc = getProperty(RESTMETHOD_callLoggerConfig);
+			if (clc instanceof RestCallLoggerConfig)
+				this.callLoggerConfig = (RestCallLoggerConfig)clc;
+			else if (clc instanceof ObjectMap)
+				this.callLoggerConfig = RestCallLoggerConfig.create().parent(context.getCallLoggerConfig()).apply((ObjectMap)clc).build();
+			else
+				this.callLoggerConfig = context.getCallLoggerConfig();
+		}
 	}
 
 	ResponseBeanMeta getResponseBeanMeta(Object o) {
@@ -797,12 +801,15 @@
 	 * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta)
 	 * @return The HTTP response code.
 	 */
-	int invoke(UrlPathInfo pathInfo, RestRequest req, RestResponse res) throws Throwable {
+	int invoke(RestCall call) throws Throwable {
 
-		UrlPathPatternMatch pm = pathPattern.match(pathInfo);
+		UrlPathPatternMatch pm = pathPattern.match(call.getUrlPathInfo());
 		if (pm == null)
 			return SC_NOT_FOUND;
 
+		RestRequest req = call.getRestRequest();
+		RestResponse res = call.getRestResponse();
+
 		RequestPath rp = req.getPathMatch();
 		for (Map.Entry<String,String> e : pm.getVars().entrySet())
 			rp.put(e.getKey(), e.getValue());
@@ -829,6 +836,8 @@
 
 		context.preCall(req, res);
 
+		call.debug(req.isDebug()).loggerConfig(req.getCallLoggerConfig());
+
 		Object[] args = new Object[methodParams.length];
 		for (int i = 0; i < methodParams.length; i++) {
 			try {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 78d9919..054a89c 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -1322,7 +1322,7 @@
 		Boolean b = ObjectUtils.castOrNull(getAttribute("Debug"), Boolean.class);

 		if (b != null)

 			return b;

-		Enablement e = context.getDebug();

+		Enablement e = restJavaMethod != null ? restJavaMethod.getDebug() : context.getDebug();

 		if (e == TRUE)

 			return true;

 		if (e == FALSE)