This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push: new 15fa71d Fix bugs in @Response annotation support. 15fa71d is described below commit 15fa71d1bb2987711e649978c261a48255ca1e76 Author: JamesBognar <jamesbog...@apache.org> AuthorDate: Sat Jul 28 14:54:55 2018 -0400 Fix bugs in @Response annotation support. --- .../httppart/HttpPartSchemaTest_Response.java | 38 -- .../juneau/http/annotation/AnnotationUtils.java | 1 + .../apache/juneau/http/annotation/RequestBean.java | 8 - .../apache/juneau/http/annotation/Response.java | 2 +- .../org/apache/juneau/http/annotation/Schema.java | 45 ++ .../juneau/httppart/HttpPartSchemaBuilder.java | 1 + .../httppart/OpenApiPartSerializerSession.java | 5 +- .../apache/juneau/remoteable/RemoteMethodArg.java | 10 - juneau-doc/src/main/javadoc/overview.html | 351 +++++++++++++++- .../09.HttpPartAnnotations/08.RequestBean.html | 54 ++- .../09.HttpPartAnnotations/09.Response.html | 296 ++++++++++++- .../apache/juneau/rest/BasicRestCallHandler.java | 115 +++--- .../org/apache/juneau/rest/RestCallHandler.java | 15 +- .../org/apache/juneau/rest/RestCallRouter.java | 2 +- .../java/org/apache/juneau/rest/RestContext.java | 12 +- .../org/apache/juneau/rest/RestContextBuilder.java | 8 + .../java/org/apache/juneau/rest/RestException.java | 9 +- .../org/apache/juneau/rest/RestJavaMethod.java | 20 +- .../org/apache/juneau/rest/RestMethodReturn.java | 38 +- .../org/apache/juneau/rest/RestMethodThrown.java | 38 +- .../org/apache/juneau/rest/RestParamDefaults.java | 13 +- .../java/org/apache/juneau/rest/RestRequest.java | 23 ++ .../juneau/rest/annotation/RestResource.java | 6 +- .../juneau/rest/mock/MockServletRequest.java | 36 ++ .../juneau/rest/response/DefaultHandler.java | 35 +- .../rest/annotation/ResponseAnnotationTest.java | 458 +++++++++++++++++++++ 26 files changed, 1454 insertions(+), 185 deletions(-) diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Response.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Response.java index c3c0df5..03f98ad 100644 --- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Response.java +++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/httppart/HttpPartSchemaTest_Response.java @@ -52,44 +52,6 @@ public class HttpPartSchemaTest_Response { assertObjectEquals("{description:'b1\\nb2',example:'f1',schema:{'$ref':'c1'},_value:'{g1:true}'}", s.getApi()); } - public static class A03 { - public void a( - @Response( - description={"b1","b2"}, - schema=@Schema($ref="c1"), - example="f1", - api="{g1:true}" - ) String x - ) { - - } - } - - @Test - public void a03_basic_onParameter() throws Exception { - HttpPartSchema s = HttpPartSchema.create().apply(Response.class, A03.class.getMethod("a", String.class), 0).noValidate().build(); - assertObjectEquals("{description:'b1\\nb2',example:'f1',schema:{'$ref':'c1'},_value:'{g1:true}'}", s.getApi()); - } - - public static class A04 { - public void a( - @Response( - description={"b3","b3"}, - schema=@Schema($ref="c3"), - example="f2", - api="{g2:true}" - ) A02 x - ) { - - } - } - - @Test - public void a04_basic_onParameterAndClass() throws Exception { - HttpPartSchema s = HttpPartSchema.create().apply(Response.class, A04.class.getMethod("a", A02.class), 0).noValidate().build(); - assertObjectEquals("{description:'b3\\nb3',example:'f2',schema:{'$ref':'c3'},_value:'{g2:true}'}", s.getApi()); - } - @Response( schema=@Schema( type="number", diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java index a4739b6..2b51f45 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/AnnotationUtils.java @@ -90,6 +90,7 @@ public class AnnotationUtils { return om .appendSkipEmpty("additionalProperties", toObjectMap(a.additionalProperties())) .appendSkipEmpty("allOf", joinnl(a.allOf())) + .appendSkipEmpty("collectionFormat", a.collectionFormat()) .appendSkipEmpty("default", joinnl(a._default())) .appendSkipEmpty("discriminator", a.discriminator()) .appendSkipEmpty("description", joinnl(a.description())) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/RequestBean.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/RequestBean.java index 3b8bb29..b20f863 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/RequestBean.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/RequestBean.java @@ -55,10 +55,6 @@ import org.apache.juneau.httppart.*; * <ja>@Query</ja> * String getQ1(); * - * <ja>@Query</ja> - * <ja>@BeanProperty</ja>(name=<js>"q2"</js>) - * String getQuery2(); - * * <jc>// Schema-based query parameter: Pipe-delimited lists of comma-delimited lists of integers.</jc> * <ja>@Query</ja>( * collectionFormat=<js>"pipes"</js> @@ -121,10 +117,6 @@ import org.apache.juneau.httppart.*; * <ja>@Query</ja> * <jk>public</jk> String getQ1() {...} * - * <ja>@Query</ja> - * <ja>@BeanProperty</ja>(name=<js>"q2"</js>) - * <jk>public</jk> String getQuery2() {...} - * * <jc>// Schema-based query parameter: Pipe-delimited lists of comma-delimited lists of integers.</jc> * <ja>@Query</ja>( * collectionFormat=<js>"pipes"</js> diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java index 7f3efd3..4f3527a 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Response.java @@ -115,7 +115,7 @@ import org.apache.juneau.jsonschema.*; * </ul> */ @Documented -@Target({PARAMETER,TYPE}) +@Target({PARAMETER,TYPE,METHOD}) @Retention(RUNTIME) @Inherited public @interface Response { diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java index 1bb9490..4eebb27 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Schema.java @@ -497,6 +497,51 @@ public @interface Schema { Items items() default @Items; /** + * <mk>collectionFormat</mk> field. + * + * <p> + * Note that this field isn't part of the Swagger 2.0 specification, but the specification does not specify how + * items are supposed to be represented. + * + * <p> + * Determines the format of the array if <code>type</code> <js>"array"</js> is used. + * <br>Can only be used if <code>type</code> is <js>"array"</js>. + * + * <br>Possible values are: + * <ul class='spaced-list'> + * <li> + * <js>"csv"</js> (default) - Comma-separated values (e.g. <js>"foo,bar"</js>). + * <li> + * <js>"ssv"</js> - Space-separated values (e.g. <js>"foo bar"</js>). + * <li> + * <js>"tsv"</js> - Tab-separated values (e.g. <js>"foo\tbar"</js>). + * <li> + * <js>"pipes</js> - Pipe-separated values (e.g. <js>"foo|bar"</js>). + * <li> + * <js>"multi"</js> - Corresponds to multiple parameter instances instead of multiple values for a single instance (e.g. <js>"foo=bar&foo=baz"</js>). + * <li> + * <js>"uon"</js> - UON notation (e.g. <js>"@(foo,bar)"</js>). + * </ul> + * + * <p> + * Static strings are defined in {@link CollectionFormatType}. + * + * <p> + * Note that for collections/arrays parameters with POJO element types, the input is broken into a string array before being converted into POJO elements. + * + * <h5 class='section'>Used for:</h5> + * <ul class='spaced-list'> + * <li> + * Server-side schema-based parsing. + * <li> + * Server-side generated Swagger documentation. + * <li> + * Client-side schema-based serializing. + * </ul> + */ + String collectionFormat() default ""; + + /** * <mk>allOf</mk> field of the Swagger <a class="doclink" href="https://swagger.io/specification/v2/#schemaObject">Schema</a> object. * * <h5 class='section'>Notes:</h5> diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java index 9c902ba..ba00a97 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/HttpPartSchemaBuilder.java @@ -337,6 +337,7 @@ public class HttpPartSchemaBuilder { type(a.type()); format(a.format()); items(a.items()); + collectionFormat(a.collectionFormat()); _default(joinnl(a._default())); maximum(HttpPartSchema.toNumber(a.maximum())); exclusiveMaximum(a.exclusiveMaximum()); diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/OpenApiPartSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/OpenApiPartSerializerSession.java index 62e820b..14829b3 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/OpenApiPartSerializerSession.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/httppart/OpenApiPartSerializerSession.java @@ -68,6 +68,7 @@ public class OpenApiPartSerializerSession extends UonPartSerializerSession { // Cache these for faster lookup private static final BeanContext BC = BeanContext.DEFAULT; private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class); + private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class); private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class); private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class); private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class); @@ -173,7 +174,9 @@ public class OpenApiPartSerializerSession extends UonPartSerializerSession { for (Object o : (Collection<?>)value) l.add(serialize(partType, items, o)); } else if (vt.hasTransformTo(String[].class)) { - l.add(serialize(partType, items, value)); + String[] ss = toType(value, CM_StringArray); + for (int i = 0; i < ss.length; i++) + l.add(serialize(partType, items, ss[i])); } if (cf == PIPES) diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java index 71bb648..eb7501c 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteMethodArg.java @@ -20,7 +20,6 @@ import java.lang.reflect.*; import org.apache.juneau.http.annotation.*; import org.apache.juneau.httppart.*; -import org.apache.juneau.internal.*; /** * Represents the metadata about an annotated argument of a method on a remote proxy interface. @@ -48,15 +47,6 @@ public final class RemoteMethodArg { this.skipIfEmpty = schema == null ? false : schema.isSkipIfEmpty(); } - RemoteMethodArg(HttpPartType partType, HttpPartSchema schema, String defaultName) { - this.index = -1; - this.partType = partType; - this.serializer = createSerializer(partType, schema); - this.schema = schema; - this.name = StringUtils.firstNonEmpty(schema == null ? null : schema.getName(), defaultName); - this.skipIfEmpty = schema == null ? false : schema.isSkipIfEmpty(); - } - private static HttpPartSerializer createSerializer(HttpPartType partType, HttpPartSchema schema) { if (schema == null) return null; diff --git a/juneau-doc/src/main/javadoc/overview.html b/juneau-doc/src/main/javadoc/overview.html index ce0516d..2e1cd6f 100644 --- a/juneau-doc/src/main/javadoc/overview.html +++ b/juneau-doc/src/main/javadoc/overview.html @@ -312,8 +312,8 @@ <li><p class='new'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.HasQuery'>@HasQuery</a></p> <li><p class='new'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.Header'>@Header</a></p> <li><p class='new'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.Path'>@Path</a></p> - <li><p class='todo'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.RequestBean'>RequestBean</a></p> - <li><p class='todo'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.Response'>@Response</a></p> + <li><p class='new'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.RequestBean'>@RequestBean</a></p> + <li><p class='new'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.Response'>@Response</a></p> <li><p class='todo'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.ResponseHeader'>@ResponseHeader</a></p> <li><p class='todo'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.ResponseStatus'>@ResponseStatus</a></p> <li><p class='todo'><a class='doclink' href='#juneau-rest-server.HttpPartAnnotations.ResponseStatuses'>@ResponseStatuses</a></p> @@ -15350,16 +15350,316 @@ VariantAlsoNegotiates <!-- ==================================================================================================== --> -<h4 class='topic todo' onclick='toggle(this)'><a href='#juneau-rest-server.HttpPartAnnotations.RequestBean' id='juneau-rest-server.HttpPartAnnotations.RequestBean'>7.9.8 - RequestBean</a></h4> +<h4 class='topic new' onclick='toggle(this)'><a href='#juneau-rest-server.HttpPartAnnotations.RequestBean' id='juneau-rest-server.HttpPartAnnotations.RequestBean'>7.9.8 - @RequestBean</a></h4> <div class='topic'><!-- START: 7.9.8 - juneau-rest-server.HttpPartAnnotations.RequestBean --> -TODO(7.2.0) +<p> + The {@link org.apache.juneau.http.annotation.RequestBean @RequestBean} annotation can be applied to a parameter or parameter type of a <ja>@RestMethod</ja>-annotated method + to identify it as an interface for retrieving HTTP parts through a bean interface. +</p> + +<h5 class='section'>Example:</h5> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(path=<js>"/mypath/{p1}/{p2}/*"</js>) + <jk>public void</jk> myMethod(<ja>@RequestBean</ja> MyRequestBean rb) {...} + + <jk>public interface</jk> MyRequestBean { + + <ja>@Path</ja> <jc>// Path variable name inferred from getter.</jc> + String getP1(); + + <ja>@Path</ja>(<js>"p2"</js>) + String getX(); + + <ja>@Path</ja>(<js>"/*"</js>) + String getRemainder(); + + <ja>@Query</ja> + String getQ1(); + + <jc>// Schema-based query parameter: Pipe-delimited lists of comma-delimited lists of integers.</jc> + <ja>@Query</ja>( + collectionFormat=<js>"pipes"</js> + items=<ja>@Items</ja>( + items=<ja>@SubItems</ja>( + collectionFormat=<js>"csv"</js> + type=<js>"integer"</js> + ) + ) + ) + <jk>int</jk>[][] getQ3(); + + <ja>@Header</ja>(<js>"*"</js>) + Map<String,Object> getHeaders(); +</p> +<p class='bpcode w800'> + <jc>// Same as above but annotation defined on interface.</jc> + <ja>@RestMethod</ja>(path=<js>"/mypath/{p1}/{p2}/*"</js>) + <jk>public void</jk> myMethod(MyRequestBean rb) {...} + + <ja>@RequestBean</ja> + <jk>public interface</jk> MyRequestBean {...} +</p> +<p> + The return types of the getters must be the supported parameter types for the HTTP-part annotation used. + <br>Schema-based serialization and parsing is used just as if used as individual parameter types. +</p> </div><!-- END: 7.9.8 - juneau-rest-server.HttpPartAnnotations.RequestBean --> <!-- ==================================================================================================== --> -<h4 class='topic todo' onclick='toggle(this)'><a href='#juneau-rest-server.HttpPartAnnotations.Response' id='juneau-rest-server.HttpPartAnnotations.Response'>7.9.9 - @Response</a></h4> +<h4 class='topic new' onclick='toggle(this)'><a href='#juneau-rest-server.HttpPartAnnotations.Response' id='juneau-rest-server.HttpPartAnnotations.Response'>7.9.9 - @Response</a></h4> <div class='topic'><!-- START: 7.9.9 - juneau-rest-server.HttpPartAnnotations.Response --> -TODO(7.2.0) +<p> + The {@link org.apache.juneau.http.annotation.Response @Response} annotation is used to identify schema information about an HTTP response. +</p> +<ul class='doctree'> + <li class='ja'>{@link org.apache.juneau.http.annotation.Response Response} + <ul> + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#api() api()} - Free-form Swagger JSON. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#code() code()} - HTTP status code. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#description() description()} - Description. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#example() example()} - Serialized example. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#examples() examples()} - Serialized examples per media type. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#headers() headers()} - Swagger about headers added to response. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#partSerializer() partSerializer()} - Override the part serializer. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#schema() schema()} - Swagger schema. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#usePartSerializer() usePartSerializer()} - Use the HTTP-Part serializer for serializing the body. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#value() value()} - Free-form Swagger JSON. + </ul> +</ul> +<p> + <br>It can be used in the following locations: +<ul> + <li>Exception classes thrown from <ja>@RestMethod</ja>-annotated REST Java methods. + <li>Type classes returned by <ja>@RestMethod</ja>-annotated REST Java methods. + <li><ja>@RestMethod</ja>-annotated REST Java methods themselves. + <li>Java method arguments and argument-types of <ja>@RestMethod</ja>-annotated REST Java methods. +</ul> + +<p> + When applied to exception classes, this annotation defines Swagger information on non-200 return types. +</p> +<p> + The following example shows the <ja>@Response</ja> annotation used to define a subclass of <code>Unauthorized</code> for an invalid login attempt. +</p> +<p class='bpcode w800'> + <jc>// Our REST method that throws an annotated exception.</jc> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <jk>public</jk> Ok login(String username, String password) <jk>throws</jk> InvalidLogin { + checkCredentials(username, password); + <jk>return new</jk> Ok(); + } + + <jc>// Our annotated exception.</jc> + <ja>@Response</ja>(description=<js>"Invalid username or password provided"</js>) + <jk>public class</jk> InvalidLogin <jk>extends</jk> Unauthorized { + <jk>public</jk> InvalidLogin() { + <jk>super</jk>(<js>"Invalid username or password."</js>); <jc>// Message sent in response</jc> + } + } + + <jc>// Parent exception class.</jc> + <jc>// Note that the default description is overridden above.</jc> + <ja>@Response</ja>(code=401, description=<js>"Unauthorized"</js>) + <jk>public class</jk> Unauthorized <jk>extends</jk> RestException { ... } +</p> +<p> + The attributes on this annotation are used to populate the generated Swagger for the method. + <br>In this case, the Swagger is populated with the following: +</p> +<p class='bpcode w800'> + <js>'/user/login'</js>: { + get: { + responses: { + 401: { + description: <js>'Invalid username or password provided'</js> + } + } + } + } +</p> +<p> + When applied type classes returned by a Java method, this annotation defines Swagger information on the body of responses. +</p> +<p> + In the example above, we're using the <code>Ok</code> class which is defined like so: +</p> +<p class='bpcode w800'> + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public class</jk> Ok { ... } +</p> +<p> + Another example is <code>Redirect</code> which is defined like so: +</p> +<p class='bpcode w800'> + <ja>@Response</ja>( + code=302, + description=<js>"Redirect"</js>, + headers={ + <ja>@ResponseHeader</ja>( + name=<js>"Location"</js>, + type=<js>"string"</js>, + format=<js>"uri"</js> + ) + } + ) + <jk>public class</jk> Redirect { ... } +</p> +<p> + The <ja>@Response</ja> annotation can also be applied to the Java method itself which is effectively + the same as applying it to the return type, albeit for this method only. +</p> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public</jk> Ok login(String username, String password) <jk>throws</jk> InvalidLogin {...} +</p> +<p> + The <ja>@Response</ja> annotation can also be applied to the Java method parameters when the parameter type + is {@link org.apache.juneau.utils.Value} (a placeholder for objects). +</p> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public void</jk> login( + String username, + String password, + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) Value<Object> + ) <jk>throws</jk> InvalidLogin {...} +</p> + + +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + +<p> + Any of the following types can be used for the parameter or POJO class (matched in the specified order): +</p> +<ol class='spaced-list'> + <li> + {@link java.io.Reader} + <br><ja>@Body</ja> annotation is optional. + <br><code>Content-Type</code> is ignored. + <li> + {@link java.io.InputStream} + <br><ja>@Body</ja> annotation is optional. + <br><code>Content-Type</code> is ignored. + <li> + Any <a class='doclink' href='#juneau-marshall.PojoCategories'>Parsable POJO</a> type. + <br><code>Content-Type</code> is required to identify correct parser. + <li> + Objects convertible from {@link java.io.Reader} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(Reader in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(Reader in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromReader</jsm>(Reader in) {...}</code> + </ul> + <code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO. + <li> + Objects convertible from {@link java.io.InputStream} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(InputStream in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(InputStream in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromInputStream</jsm>(InputStream in) {...}</code> + </ul> + <code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO. + <li> + Objects convertible from {@link java.lang.String} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromString</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>parse</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>parseString</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>forName</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>forString</jsm>(String in) {...}</code> + </ul> + Note that this also includes all enums. +</ol> +<p> + If the {@link org.apache.juneau.http.annotation.Body#usePartParser usePartParser} flag is set on the annotation, + then the body can be parsed using the registered {@link org.apache.juneau.httppart.HttpPartParser} which by default + is {@link org.apache.juneau.httppart.OpenApiPartParser}. +</p> +<p> + For example, the following shows how a pipe-delimited list of comma-delimited numbers (e.g. <js>"1,2,3|4,5,6|7,8,9"</js>) can be converted to a 2-dimensional array of <code>Longs</code>: +</p> +<p class='bpcode w800'> + <jc>// Body is a pipe-delimited list of comma-delimited lists of longs.</jc> + <ja>@RestMethod</ja>(method=<js>"POST"</js>, path=<js>"/testBody"</js>) + <jk>public void</jk> testBody( + <ja>@Body</ja>( + usePartParser=<jk>true</jk>, + schema=<ja>@Schema</ja>( + items=<ja>@Items</ja>( + collectionFormat=<js>"pipes"</js>, + items=<ja>@SubItems</ja>( + collectionFormat=<js>"csv"</js>, + type=<js>"integer"</js>, + format=<js>"int64"</js>, + minimum=<js>"0"</js>, + maximum=<js>"100"</js> + minLength=1, + maxLength=10 + ) + ), + minLength=1, + maxLength=10 + ) + ) + Long[][] body + ) {...} +</p> +<p> + Input will be converted based on the types and formats defined in the schema definition. + <br>Input validations such as <code>minLength/maxLength</code> that don't match the input will result in automatic <code>400 Bad Request</code> responses. +</p> +<p> + For more information about valid parameter types when using OpenAPI parsing, see <a class='doclink' href='#juneau-marshall.OpenApiDetails.Parsers'>OpenAPI Parsers</a> +</p> + +<p> + The <ja>@Body</ja> annotation is also used for supplying swagger information about the body of the request. + <br>This information is used to populate the auto-generated Swagger documentation and UI. +</p> +<h5 class='figure'>Examples:</h5> +<p class='bpcode w800'> + <jc>// Normal</jc> + <ja>@Body</ja>( + description=<js>"Pet object to add to the store"</js>, + required=<jk>true</jk>, + example=<js>"{name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']}"</js> + ) +</p> +<p class='bpcode w800'> + <jc>// Free-form</jc> + <jc>// Note the extra field</jc> + <ja>@Body</ja>({ + <js>"description: 'Pet object to add to the store',"</js>, + <js>"required: true,"</js>, + <js>"example: {name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']},"</js> + <js>"x-extra: 'extra field'"</js> + }) +</p> +<p> + <a class='doclink' href='#DefaultRestSvlVariables'>Initialization and request-time variables</a> (e.g. "$L{my.localized.variable}") + are supported on annotation fields. +</p> +<h5 class='figure'>Example:</h5> +<p class='bpcode w800'> + <jc>// Localized</jc> + <ja>@Body</ja>( + description=<js>"$L{PetObjectDescription}"</js> + ) +</p> + + + +<h5 class='section'>See Also:</h5> +<ul> + <li class='jc'>{@link org.apache.juneau.rest.RequestBody} + <li class='link'><a class='doclink' href='#juneau-rest-server.OpenApiSchemaPartSerializing'>OpenAPI Schema Part Serializing</a> +</ul> </div><!-- END: 7.9.9 - juneau-rest-server.HttpPartAnnotations.Response --> <!-- ==================================================================================================== --> @@ -29796,6 +30096,45 @@ TODO(7.2.0) <li><code>RdfParser.Turtle</code> -> {@link org.apache.juneau.jena.TurtleParser} </ul> <li> + New API for pairing serializers and parsers for simplified syntax: + <h5 class='figure'>Examples:</h5> + <p class='bpcode w800'> + <jc>// Using instance.</jc> + Json json = <jk>new</jk> Json(); + MyPojo myPojo = json.read(string, MyPojo.<jk>class</jk>); + String string = json.write(myPojo); + </p> + <p class='bpcode w800'> + <jc>// Using DEFAULT instance.</jc> + MyPojo myPojo = Json.<jsf>DEFAULT</jsf>.read(string, MyPojo.<jk>class</jk>); + String string = Json.<jsf>DEFAULT</jsf>.write(myPojo); + </p> + <ul class='doctree'> + <li class='jac'>{@link org.apache.juneau.marshall.Marshall} + <ul> + <li class='jac'>{@link org.apache.juneau.marshall.CharMarshall} + <ul> + <li class='jc'>{@link org.apache.juneau.marshall.Html} + <li class='jc'>{@link org.apache.juneau.marshall.Json} + <li class='jc'>{@link org.apache.juneau.marshall.PlainText} + <li class='jc'>{@link org.apache.juneau.marshall.SimpleJson} + <li class='jc'>{@link org.apache.juneau.marshall.Uon} + <li class='jc'>{@link org.apache.juneau.marshall.UrlEncoding} + <li class='jc'>{@link org.apache.juneau.marshall.Xml} + <li class='jc'>{@link org.apache.juneau.marshall.N3} + <li class='jc'>{@link org.apache.juneau.marshall.NTriple} + <li class='jc'>{@link org.apache.juneau.marshall.RdfXml} + <li class='jc'>{@link org.apache.juneau.marshall.RdfXmlAbbrev} + <li class='jc'>{@link org.apache.juneau.marshall.Turtle} + </ul> + <li class='jac'>{@link org.apache.juneau.marshall.StreamMarshall} + <ul> + <li class='jc'>{@link org.apache.juneau.marshall.Jso} + <li class='jc'>{@link org.apache.juneau.marshall.MsgPack} + </ul> + </ul> + </ul> + <li> New/updated documentation: <ul> <li><a href='#juneau-marshall.JsonDetails.SimplifiedJson'>2.15.3 - Simplified JSON</a> diff --git a/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/08.RequestBean.html b/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/08.RequestBean.html index 85220da..5bd004d 100644 --- a/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/08.RequestBean.html +++ b/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/08.RequestBean.html @@ -13,6 +13,56 @@ ***************************************************************************************************************************/ --> -{todo} RequestBean +{new} @RequestBean -TODO(7.2.0) \ No newline at end of file +<p> + The {@link org.apache.juneau.http.annotation.RequestBean @RequestBean} annotation can be applied to a parameter or parameter type of a <ja>@RestMethod</ja>-annotated method + to identify it as an interface for retrieving HTTP parts through a bean interface. +</p> + +<h5 class='section'>Example:</h5> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(path=<js>"/mypath/{p1}/{p2}/*"</js>) + <jk>public void</jk> myMethod(<ja>@RequestBean</ja> MyRequestBean rb) {...} + + <jk>public interface</jk> MyRequestBean { + + <ja>@Path</ja> <jc>// Path variable name inferred from getter.</jc> + String getP1(); + + <ja>@Path</ja>(<js>"p2"</js>) + String getX(); + + <ja>@Path</ja>(<js>"/*"</js>) + String getRemainder(); + + <ja>@Query</ja> + String getQ1(); + + <jc>// Schema-based query parameter: Pipe-delimited lists of comma-delimited lists of integers.</jc> + <ja>@Query</ja>( + collectionFormat=<js>"pipes"</js> + items=<ja>@Items</ja>( + items=<ja>@SubItems</ja>( + collectionFormat=<js>"csv"</js> + type=<js>"integer"</js> + ) + ) + ) + <jk>int</jk>[][] getQ3(); + + <ja>@Header</ja>(<js>"*"</js>) + Map<String,Object> getHeaders(); +</p> +<p class='bpcode w800'> + <jc>// Same as above but annotation defined on interface.</jc> + <ja>@RestMethod</ja>(path=<js>"/mypath/{p1}/{p2}/*"</js>) + <jk>public void</jk> myMethod(MyRequestBean rb) {...} + + <ja>@RequestBean</ja> + <jk>public interface</jk> MyRequestBean {...} +</p> +<p> + The return types of the getters must be the supported parameter types for the HTTP-part annotation used. + <br>Schema-based serialization and parsing is used just as if used as individual parameter types. +</p> diff --git a/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/09.Response.html b/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/09.Response.html index 76c71ae..16b892c 100644 --- a/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/09.Response.html +++ b/juneau-doc/src/main/resources/Topics/07.juneau-rest-server/09.HttpPartAnnotations/09.Response.html @@ -13,6 +13,298 @@ ***************************************************************************************************************************/ --> -{todo} @Response +{new} @Response + +<p> + The {@link org.apache.juneau.http.annotation.Response @Response} annotation is used to identify schema information about an HTTP response. +</p> +<ul class='doctree'> + <li class='ja'>{@link org.apache.juneau.http.annotation.Response Response} + <ul> + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#api() api()} - Free-form Swagger JSON. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#code() code()} - HTTP status code. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#description() description()} - Description. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#example() example()} - Serialized example. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#examples() examples()} - Serialized examples per media type. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#headers() headers()} - Swagger about headers added to response. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#partSerializer() partSerializer()} - Override the part serializer. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#schema() schema()} - Swagger schema. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#usePartSerializer() usePartSerializer()} - Use the HTTP-Part serializer for serializing the body. + <li class='jf'>{@link org.apache.juneau.http.annotation.Response#value() value()} - Free-form Swagger JSON. + </ul> +</ul> +<p> + <br>It can be used in the following locations: +<ul> + <li>Exception classes thrown from <ja>@RestMethod</ja>-annotated REST Java methods. + <li>Type classes returned by <ja>@RestMethod</ja>-annotated REST Java methods. + <li><ja>@RestMethod</ja>-annotated REST Java methods themselves. + <li>Java method arguments and argument-types of <ja>@RestMethod</ja>-annotated REST Java methods. +</ul> + +<p> + When applied to exception classes, this annotation defines Swagger information on non-200 return types. +</p> +<p> + The following example shows the <ja>@Response</ja> annotation used to define a subclass of <code>Unauthorized</code> for an invalid login attempt. +</p> +<p class='bpcode w800'> + <jc>// Our REST method that throws an annotated exception.</jc> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <jk>public</jk> Ok login(String username, String password) <jk>throws</jk> InvalidLogin { + checkCredentials(username, password); + <jk>return new</jk> Ok(); + } + + <jc>// Our annotated exception.</jc> + <ja>@Response</ja>(description=<js>"Invalid username or password provided"</js>) + <jk>public class</jk> InvalidLogin <jk>extends</jk> Unauthorized { + <jk>public</jk> InvalidLogin() { + <jk>super</jk>(<js>"Invalid username or password."</js>); <jc>// Message sent in response</jc> + } + } + + <jc>// Parent exception class.</jc> + <jc>// Note that the default description is overridden above.</jc> + <ja>@Response</ja>(code=401, description=<js>"Unauthorized"</js>) + <jk>public class</jk> Unauthorized <jk>extends</jk> RestException { ... } +</p> +<p> + The attributes on this annotation are used to populate the generated Swagger for the method. + <br>In this case, the Swagger is populated with the following: +</p> +<p class='bpcode w800'> + <js>'/user/login'</js>: { + get: { + responses: { + 401: { + description: <js>'Invalid username or password provided'</js> + } + } + } + } +</p> +<p> + When applied type classes returned by a Java method, this annotation defines Swagger information on the body of responses. +</p> +<p> + In the example above, we're using the <code>Ok</code> class which is defined like so: +</p> +<p class='bpcode w800'> + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public class</jk> Ok { ... } +</p> +<p> + Another example is <code>Redirect</code> which is defined like so: +</p> +<p class='bpcode w800'> + <ja>@Response</ja>( + code=302, + description=<js>"Redirect"</js>, + headers={ + <ja>@ResponseHeader</ja>( + name=<js>"Location"</js>, + type=<js>"string"</js>, + format=<js>"uri"</js> + ) + } + ) + <jk>public class</jk> Redirect { ... } +</p> +<p> + The <ja>@Response</ja> annotation can also be applied to the Java method itself which is effectively + the same as applying it to the return type, albeit for this method only. +</p> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public</jk> Ok login(String username, String password) <jk>throws</jk> InvalidLogin {...} +</p> +<p> + The <ja>@Response</ja> annotation can also be applied to the Java method parameters when the parameter type + is {@link org.apache.juneau.utils.Value} (a placeholder for objects). +</p> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(name=<js>"GET"</js>, path=<js>"/user/login"</js>) + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) + <jk>public void</jk> login( + String username, + String password, + <ja>@Response</ja>(code=200, example=<js>"'OK'"</js>) Value<Object> + ) <jk>throws</jk> InvalidLogin {...} +</p> + + +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + + +<p> + The return type can be any serializable POJO as defined in <a class='doclink' href='#juneau-marshall.PojoCategories'>POJO Categories</a>. + <br>It can also be <jk>void</jk> if the method is not sending any output (e.g. a request redirect) or is + setting the output using the {@link org.apache.juneau.rest.RestResponse#setOutput(Object)} method. +</p> +<h5 class='figure'>Example:</h5> +<p class='bpcode w800'> + <ja>@RestMethod</ja>(name=<jsf>GET</jsf>) + <jk>public</jk> String doGet() { + <jk>return</jk> <js>"Hello World!"</js>; + } +</p> +<p> + Out-of-the-box, besides POJOs, the following return types are handled as special cases: +</p> +<ul class='spaced-list'> + <li class='jc'>{@link java.io.InputStream} + <br>The contents are simply piped to the output stream returned by + {@link org.apache.juneau.rest.RestResponse#getNegotiatedOutputStream()}. + <br>Note that you should call {@link org.apache.juneau.rest.RestResponse#setContentType(String)} to set + the <l>Content-Type</l> header if you use this object type. + <li class='jc'>{@link java.io.InputStream} + <br>The contents are simply piped to the output stream returned by + {@link org.apache.juneau.rest.RestResponse#getNegotiatedOutputStream()}. + <br>Note that you should call {@link org.apache.juneau.rest.RestResponse#setContentType(String)} to set + the <l>Content-Type</l> header if you use this object type. + <li class='jc'>{@link java.io.Reader} + <br>The contents are simply piped to the output stream returned by + {@link org.apache.juneau.rest.RestResponse#getNegotiatedWriter()}. + <br>Note that you should call {@link org.apache.juneau.rest.RestResponse#setContentType(String)} to set + the <l>Content-Type</l> header if you use this object type. + <li class='jc'>{@link org.apache.juneau.rest.helper.Redirect} + <br>Represents an HTTP redirect response. + <li class='jc'>{@link org.apache.juneau.Streamable} + <br>Interface that identifies that an object can be serialized directly to an output stream. + <li class='jc'>{@link org.apache.juneau.Writable} + <br>Interface that identifies that an object can be serialized directly to a writer. + <li class='jc'>{@link org.apache.juneau.utils.ZipFileList} + <br>Special interface for sending zip files as responses. +</ul> +<p> + Any of the following types can be used for the parameter or POJO class (matched in the specified order): +</p> +<ol class='spaced-list'> + <li> + {@link java.io.Reader} + <br><ja>@Body</ja> annotation is optional. + <br><code>Content-Type</code> is ignored. + <li> + {@link java.io.InputStream} + <br><ja>@Body</ja> annotation is optional. + <br><code>Content-Type</code> is ignored. + <li> + Any <a class='doclink' href='#juneau-marshall.PojoCategories'>Parsable POJO</a> type. + <br><code>Content-Type</code> is required to identify correct parser. + <li> + Objects convertible from {@link java.io.Reader} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(Reader in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(Reader in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromReader</jsm>(Reader in) {...}</code> + </ul> + <code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO. + <li> + Objects convertible from {@link java.io.InputStream} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(InputStream in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(InputStream in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromInputStream</jsm>(InputStream in) {...}</code> + </ul> + <code>Content-Type</code> must not be present or match an existing parser so that it's not parsed as a POJO. + <li> + Objects convertible from {@link java.lang.String} by having one of the following non-deprecated methods: + <ul> + <li><code><jk>public</jk> T(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>create</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>fromString</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>parse</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>parseString</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>forName</jsm>(String in) {...}</code> + <li><code><jk>public static</jk> T <jsm>forString</jsm>(String in) {...}</code> + </ul> + Note that this also includes all enums. +</ol> +<p> + If the {@link org.apache.juneau.http.annotation.Body#usePartParser usePartParser} flag is set on the annotation, + then the body can be parsed using the registered {@link org.apache.juneau.httppart.HttpPartParser} which by default + is {@link org.apache.juneau.httppart.OpenApiPartParser}. +</p> +<p> + For example, the following shows how a pipe-delimited list of comma-delimited numbers (e.g. <js>"1,2,3|4,5,6|7,8,9"</js>) can be converted to a 2-dimensional array of <code>Longs</code>: +</p> +<p class='bpcode w800'> + <jc>// Body is a pipe-delimited list of comma-delimited lists of longs.</jc> + <ja>@RestMethod</ja>(method=<js>"POST"</js>, path=<js>"/testBody"</js>) + <jk>public void</jk> testBody( + <ja>@Body</ja>( + usePartParser=<jk>true</jk>, + schema=<ja>@Schema</ja>( + items=<ja>@Items</ja>( + collectionFormat=<js>"pipes"</js>, + items=<ja>@SubItems</ja>( + collectionFormat=<js>"csv"</js>, + type=<js>"integer"</js>, + format=<js>"int64"</js>, + minimum=<js>"0"</js>, + maximum=<js>"100"</js> + minLength=1, + maxLength=10 + ) + ), + minLength=1, + maxLength=10 + ) + ) + Long[][] body + ) {...} +</p> +<p> + Input will be converted based on the types and formats defined in the schema definition. + <br>Input validations such as <code>minLength/maxLength</code> that don't match the input will result in automatic <code>400 Bad Request</code> responses. +</p> +<p> + For more information about valid parameter types when using OpenAPI parsing, see <a class='doclink' href='#juneau-marshall.OpenApiDetails.Parsers'>OpenAPI Parsers</a> +</p> + +<p> + The <ja>@Body</ja> annotation is also used for supplying swagger information about the body of the request. + <br>This information is used to populate the auto-generated Swagger documentation and UI. +</p> +<h5 class='figure'>Examples:</h5> +<p class='bpcode w800'> + <jc>// Normal</jc> + <ja>@Body</ja>( + description=<js>"Pet object to add to the store"</js>, + required=<jk>true</jk>, + example=<js>"{name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']}"</js> + ) +</p> +<p class='bpcode w800'> + <jc>// Free-form</jc> + <jc>// Note the extra field</jc> + <ja>@Body</ja>({ + <js>"description: 'Pet object to add to the store',"</js>, + <js>"required: true,"</js>, + <js>"example: {name:'Doggie',price:9.99,species:'Dog',tags:['friendly','cute']},"</js> + <js>"x-extra: 'extra field'"</js> + }) +</p> +<p> + <a class='doclink' href='#DefaultRestSvlVariables'>Initialization and request-time variables</a> (e.g. "$L{my.localized.variable}") + are supported on annotation fields. +</p> +<h5 class='figure'>Example:</h5> +<p class='bpcode w800'> + <jc>// Localized</jc> + <ja>@Body</ja>( + description=<js>"$L{PetObjectDescription}"</js> + ) +</p> + + + +<h5 class='section'>See Also:</h5> +<ul> + <li class='jc'>{@link org.apache.juneau.rest.RequestBody} + <li class='link'><a class='doclink' href='#juneau-rest-server.OpenApiSchemaPartSerializing'>OpenAPI Schema Part Serializing</a> +</ul> -TODO(7.2.0) \ No newline at end of file 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 a9f9c7c..4b1a0d5 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 @@ -23,8 +23,7 @@ import java.util.*; import javax.servlet.*; import javax.servlet.http.*; -import org.apache.juneau.http.annotation.*; -import org.apache.juneau.rest.annotation.*; +import org.apache.juneau.httppart.*; import org.apache.juneau.rest.exception.*; import org.apache.juneau.rest.helper.*; import org.apache.juneau.rest.util.*; @@ -107,6 +106,7 @@ public class BasicRestCallHandler implements RestCallHandler { logger.log(FINE, "HTTP: {0} {1}", r1.getMethod(), r1.getRequestURI()); long startTime = System.currentTimeMillis(); + RestRequest req = null; try { context.checkForInitException(); @@ -138,7 +138,7 @@ public class BasicRestCallHandler implements RestCallHandler { context.startCall(r1, r2); - RestRequest req = createRequest(r1); + req = createRequest(r1); RestResponse res = createResponse(req, r2); String method = req.getMethod(); String methodUC = method.toUpperCase(Locale.ENGLISH); @@ -192,23 +192,10 @@ public class BasicRestCallHandler implements RestCallHandler { r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); - } catch (RestException e) { - r1.setAttribute("Exception", e); - r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); - handleError(r1, r2, e); } catch (Throwable e) { - Response ri = e.getClass().getAnnotation(Response.class); - int code = SC_INTERNAL_SERVER_ERROR; - if (ri != null) { - if (ri.code().length > 0) - code = ri.code()[0]; - else if (ri.value().length > 0) - code = ri.value()[0]; - } - RestException e2 = new RestException(e, code); r1.setAttribute("Exception", e); r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime); - handleError(r1, r2, e2); + handleError(r1, r2, req, e); } context.finishCall(r1, r2); @@ -274,10 +261,6 @@ public class BasicRestCallHandler implements RestCallHandler { * 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. @@ -286,64 +269,62 @@ public class BasicRestCallHandler implements RestCallHandler { * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. */ @Override /* RestCallHandler */ - public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { - if (context.isDebug()) { - String qs = req.getQueryString(); - int c = e.getOccurrence(); - String msg = '[' + Integer.toHexString(e.hashCode()) + '.' + e.getStatus() + '.' + c + "] HTTP " + req.getMethod() + " " + e.getStatus() + " " + req.getRequestURI() + (qs == null ? "" : "?" + qs); - System.err.println(msg); - e.printStackTrace(System.err); - } - e.setOccurrence(context == null ? 0 : context.getStackTraceOccurrence(e)); - logger.onError(req, res, e); - renderError(req, res, e); - } + public synchronized void handleError(HttpServletRequest req, HttpServletResponse res, RestRequest rreq, Throwable e) throws IOException { - /** - * Method for rendering response errors. - * - * <p> - * The default implementation renders a plain text English message, optionally with a stack trace if - * {@link RestResource#renderResponseStackTraces() @RestResource.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. - */ - @Override /* RestCallHandler */ - public void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException { + RestMethodThrown rmt = rreq == null ? null : rreq.getRestMethodThrown(e); + int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e); + RestException e2 = (e instanceof RestException ? (RestException)e : new RestException(e, rmt == null ? 500 : rmt.getCode())).setOccurrence(occurrence); - int status = e.getStatus(); - res.setStatus(status); - res.setContentType("text/plain"); - res.setHeader("Content-Encoding", "identity"); + HttpPartSerializer ps = rmt == null ? null : rmt.getPartSerializer(); - Throwable t = e.getRootCause(); + Throwable t = e2.getRootCause(); if (t != null) { res.setHeader("Exception-Name", t.getClass().getName()); res.setHeader("Exception-Message", t.getMessage()); } - PrintWriter w = null; try { - w = res.getWriter(); - } catch (IllegalStateException e2) { - w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); + res.setContentType("text/plain"); + res.setHeader("Content-Encoding", "identity"); + res.setStatus(e2.getStatus()); + + PrintWriter w = null; + try { + w = res.getWriter(); + } catch (IllegalStateException x) { + w = new PrintWriter(new OutputStreamWriter(res.getOutputStream(), UTF8)); + } + + try (PrintWriter w2 = w) { + + // Throwable can be handled as an HTTP part. + if (rmt != null && ps != null) { + w2.append(ps.serialize(rmt.getSchema(), e)); + + // It's some other exception. + } else { + String httpMessage = RestUtils.getHttpResponseText(e2.getStatus()); + if (httpMessage != null) + w2.append("HTTP ").append(String.valueOf(e2.getStatus())).append(": ").append(httpMessage).append("\n\n"); + if (context != null && context.isRenderResponseStackTraces()) + e.printStackTrace(w2); + else + w2.append(e2.getFullStackMessage(true)); + } + } + + } catch (Exception e1) { + logger.onError(req, res, new RestException(e1, 0)); } - try (PrintWriter w2 = w) { - String httpMessage = RestUtils.getHttpResponseText(status); - if (httpMessage != null) - w2.append("HTTP ").append(String.valueOf(status)).append(": ").append(httpMessage).append("\n\n"); - if (context != null && context.isRenderResponseStackTraces()) - e.printStackTrace(w2); - else - w2.append(e.getFullStackMessage(true)); + if (context.isDebug()) { + String qs = req.getQueryString(); + String msg = '[' + Integer.toHexString(e.hashCode()) + '.' + e2.getStatus() + '.' + e2.getOccurrence() + "] HTTP " + req.getMethod() + " " + e2.getStatus() + " " + req.getRequestURI() + (qs == null ? "" : "?" + qs); + System.err.println(msg); + e.printStackTrace(System.err); } + + logger.onError(req, res, e2); } /** 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 cf007fa..41f5f18 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 @@ -95,20 +95,13 @@ public interface RestCallHandler { * * @param req The servlet request. * @param res The servlet response. + * @param rreq + * The REST request. + * <br>This may be <jk>null</jk> if an error occurred before this was initialized. * @param e The exception that occurred. * @throws IOException Can be thrown if a problem occurred trying to write to the output stream. */ - public void handleError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException; - - /** - * Method for rendering response errors. - * - * @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. - */ - public void renderError(HttpServletRequest req, HttpServletResponse res, RestException e) throws IOException; + public void handleError(HttpServletRequest req, HttpServletResponse res, RestRequest rreq, Throwable e) throws IOException; /** * Returns the session objects for the specified request. 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 48bac28..3bf4358 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 @@ -75,7 +75,7 @@ public class RestCallRouter { * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta) * @return The HTTP response code. */ - int invoke(String pathInfo, RestRequest req, RestResponse res) throws RestException { + int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable { if (restJavaMethods.length == 1) return restJavaMethods[0].invoke(pathInfo, req, res); 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 a7251de..a926559 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 @@ -834,7 +834,7 @@ public final class RestContext extends BeanContext { * <p> * Enables the following: * <ul> - * <li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestException)} is called. + * <li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)} is called. * </ul> */ public static final String REST_debug = PREFIX + "debug.b"; @@ -2030,7 +2030,7 @@ public final class RestContext extends BeanContext { * <ul> * <li class='jm'>{@link RestContext#isRenderResponseStackTraces() RestContext.isRenderResponseStackTraces()} * </ul> - * That method is used by {@link BasicRestCallHandler#renderError(HttpServletRequest, HttpServletResponse, RestException)}. + * That method is used by {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)}. * </ul> */ public static final String REST_renderResponseStackTraces = PREFIX + "renderResponseStackTraces.b"; @@ -2666,7 +2666,7 @@ public final class RestContext extends BeanContext { * Affects the following methods: * <ul> * <li class='jm'>{@link RestContext#getStackTraceOccurrence(Throwable) RestContext.getStackTraceOccurrance(Throwable)} - * <li class='jm'>{@link RestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestException)} + * <li class='jm'>{@link RestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)} * <li class='jm'>{@link RestException#getOccurrence()} - Returns the number of times this exception occurred. * </ul> * @@ -2955,7 +2955,7 @@ public final class RestContext extends BeanContext { serializers = SerializerGroup.create().append(getInstanceArrayProperty(REST_serializers, Serializer.class, new Serializer[0], true, resource, ps)).build(); parsers = ParserGroup.create().append(getInstanceArrayProperty(REST_parsers, Parser.class, new Parser[0], true, resource, ps)).build(); partSerializer = getInstanceProperty(REST_partSerializer, HttpPartSerializer.class, OpenApiPartSerializer.class, true, resource, ps); - partParser = getInstanceProperty(REST_partSerializer, HttpPartParser.class, OpenApiPartParser.class, true, resource, ps); + partParser = getInstanceProperty(REST_partParser, HttpPartParser.class, OpenApiPartParser.class, true, resource, ps); jsonSchemaSerializer = new JsonSchemaSerializer(ps); encoders = new EncoderGroupBuilder().append(getInstanceArrayProperty(REST_encoders, Encoder.class, new Encoder[0], true, resource, ps)).build(); beanContext = BeanContext.create().apply(ps).build(); @@ -3044,7 +3044,7 @@ public final class RestContext extends BeanContext { sm = new RestJavaMethod(resource, method, this) { @Override - int invoke(String pathInfo, RestRequest req, RestResponse res) throws InternalServerError { + int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable { int rc = super.invoke(pathInfo, req, res); if (rc != SC_OK) @@ -3093,7 +3093,7 @@ public final class RestContext extends BeanContext { _javaRestMethods.put(method.getName(), sm); addToRouter(routers, httpMethod, sm); } - } catch (RestServletException e) { + } catch (Throwable e) { throw new RestServletException("Problem occurred trying to serialize methods on class {0}, methods={1}", resourceClass.getName(), SimpleJsonSerializer.DEFAULT.serialize(methodsFound)).initCause(e); } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java index 4a90673..d7e32b6 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java @@ -116,7 +116,11 @@ public class RestContextBuilder extends BeanContextBuilder implements ServletCon this.parentContext = parentContext; properties = new RestContextProperties(); + + // Default values. logger(BasicRestLogger.class); + partSerializer(OpenApiPartSerializer.class); + partParser(OpenApiPartParser.class); staticFileResponseHeader("Cache-Control", "max-age=86400, public"); encoders(IdentityEncoder.INSTANCE); @@ -169,6 +173,10 @@ public class RestContextBuilder extends BeanContextBuilder implements ServletCon set(p, true); serializers(false, merge(ObjectUtils.toType(psb.peek(REST_serializers), Object[].class), r.serializers())); parsers(false, merge(ObjectUtils.toType(psb.peek(REST_parsers), Object[].class), r.parsers())); + if (r.partSerializer() != HttpPartSerializer.Null.class) + partSerializer(r.partSerializer()); + if (r.partParser() != HttpPartParser.Null.class) + partParser(r.partParser()); encoders(r.encoders()); if (r.produces().length > 0) produces(false, resolveVars(vr, r.produces())); diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestException.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestException.java index 45bc86e..a06489e 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestException.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestException.java @@ -31,7 +31,7 @@ public class RestException extends FormattedRuntimeException { private static final long serialVersionUID = 1L; - private final int status; + private int status; private int occurrence; /** @@ -134,8 +134,13 @@ public class RestException extends FormattedRuntimeException { return i; } - void setOccurrence(int occurrence) { + RestException setOccurrence(int occurrence) { this.occurrence = occurrence; + return this; + } + + void setStatus(int status) { + this.status = status; } /** diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java index 41bbc77..8021822 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java @@ -384,11 +384,11 @@ public class RestJavaMethod implements Comparable<RestJavaMethod> { methodParams = context.findParams(method, pathPattern, false); - methodReturn = new RestMethodReturn(method.getGenericReturnType()); + methodReturn = new RestMethodReturn(method, partSerializer, ps); methodThrowns = new RestMethodThrown[method.getExceptionTypes().length]; for (int i = 0; i < methodThrowns.length; i++) - methodThrowns[i] = new RestMethodThrown(method.getExceptionTypes()[i]); + methodThrowns[i] = new RestMethodThrown(method.getExceptionTypes()[i], partSerializer, ps); // Need this to access methods in anonymous inner classes. setAccessible(method, true); @@ -439,7 +439,7 @@ public class RestJavaMethod implements Comparable<RestJavaMethod> { * @param pathInfo The value of {@link HttpServletRequest#getPathInfo()} (sorta) * @return The HTTP response code. */ - int invoke(String pathInfo, RestRequest req, RestResponse res) throws RestException, BadRequest, InternalServerError { + int invoke(String pathInfo, RestRequest req, RestResponse res) throws Throwable { String[] patternVals = pathPattern.match(pathInfo); if (patternVals == null) @@ -522,11 +522,7 @@ public class RestJavaMethod implements Comparable<RestJavaMethod> { throw new BadRequest(e2); if (e2 instanceof InvalidDataConversionException) throw new BadRequest(e2); - throw new InternalServerError(e2); - } catch (RestException e) { - throw e; - } catch (Exception e) { - throw new InternalServerError(e); + throw e2; } return SC_OK; } @@ -621,4 +617,12 @@ public class RestJavaMethod implements Comparable<RestJavaMethod> { out[i] = vr.resolve(in[i]); return out; } + + RestMethodReturn getRestMethodReturn() { + return this.methodReturn; + } + + List<RestMethodThrown> getRestMethodThrown() { + return Collections.unmodifiableList(Arrays.asList(this.methodThrowns)); + } } \ No newline at end of file diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java index ffb7fa8..c8f75ee 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodReturn.java @@ -12,11 +12,14 @@ // *************************************************************************************************************************** package org.apache.juneau.rest; +import static org.apache.juneau.internal.ReflectionUtils.*; + import java.lang.reflect.*; import org.apache.juneau.*; import org.apache.juneau.http.annotation.*; import org.apache.juneau.httppart.*; +import org.apache.juneau.internal.*; /** * Contains metadata about the return type on a REST Java method. @@ -26,12 +29,21 @@ public class RestMethodReturn { private final Type type; private final int code; private final ObjectMap api; + private final HttpPartSchema schema; + private final HttpPartSerializer partSerializer; + + RestMethodReturn(Method m, HttpPartSerializer partSerializer, PropertyStore ps) { + HttpPartSchema s = HttpPartSchema.DEFAULT; + if (hasAnnotation(Response.class, m)) + s = HttpPartSchema.create(Response.class, m); - RestMethodReturn(Type type) { - HttpPartSchema s = HttpPartSchema.create(Response.class, type); - this.type = type; + this.schema = s; + this.type = m.getGenericReturnType(); this.api = HttpPartSchema.getApiCodeMap(s, 200).unmodifiable(); this.code = s.getCode(200); + + boolean usePS = (s.isUsePartSerializer() || s.getSerializer() != null); + this.partSerializer = usePS ? ObjectUtils.firstNonNull(ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps), partSerializer) : null; } /** @@ -60,4 +72,24 @@ public class RestMethodReturn { public ObjectMap getApi() { return api; } + + /** + * Returns the schema for the method return type. + * + * @return The schema for the method return type. Never <jk>null</jk>. + */ + public HttpPartSchema getSchema() { + return schema; + } + + /** + * Returns the part serializer for the method return type. + * + * @return + * The part serializer for the method return type. + * <br><jk>null</jk> if {@link Response#usePartSerializer()} is <jk>false</jk>. + */ + public HttpPartSerializer getPartSerializer() { + return partSerializer; + } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java index f37bb67..a4176f5 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodThrown.java @@ -12,11 +12,12 @@ // *************************************************************************************************************************** package org.apache.juneau.rest; -import java.lang.reflect.*; +import static org.apache.juneau.internal.ReflectionUtils.*; import org.apache.juneau.*; import org.apache.juneau.http.annotation.*; import org.apache.juneau.httppart.*; +import org.apache.juneau.internal.*; /** * Contains metadata about a throwable on a REST Java method. @@ -26,12 +27,21 @@ public class RestMethodThrown { final Class<?> type; final int code; final ObjectMap api; + private final HttpPartSchema schema; + final HttpPartSerializer partSerializer; - RestMethodThrown(Class<?> type) { - HttpPartSchema s = HttpPartSchema.create(Response.class, type); + RestMethodThrown(Class<?> type, HttpPartSerializer partSerializer, PropertyStore ps) { + HttpPartSchema s = HttpPartSchema.DEFAULT; + if (hasAnnotation(Response.class, type)) + s = HttpPartSchema.create(Response.class, type); + + this.schema = s; this.type = type; this.api = HttpPartSchema.getApiCodeMap(s, 500).unmodifiable(); this.code = s.getCode(500); + + boolean usePS = (s.isUsePartSerializer() || s.getSerializer() != null); + this.partSerializer = usePS ? ObjectUtils.firstNonNull(ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps), partSerializer) : null; } /** @@ -39,7 +49,7 @@ public class RestMethodThrown { * * @return The return type of the Java method. */ - public Type getType() { + public Class<?> getType() { return type; } @@ -60,4 +70,24 @@ public class RestMethodThrown { public ObjectMap getApi() { return api; } + + /** + * Returns the schema for the method return type. + * + * @return The schema for the method return type. Never <jk>null</jk>. + */ + public HttpPartSchema getSchema() { + return schema; + } + + /** + * Returns the part serializer for the method return type. + * + * @return + * The part serializer for the method return type. + * <br><jk>null</jk> if {@link Response#usePartSerializer()} is <jk>false</jk>. + */ + public HttpPartSerializer getPartSerializer() { + return partSerializer; + } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java index 7156308..a45b31c 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java @@ -662,12 +662,14 @@ class RestParamDefaults { static final class ResponseObject extends RestMethodParam { final HttpPartSerializer partSerializer; + final boolean usePartSerializer; final HttpPartSchema schema; private String _default; protected ResponseObject(Method m, HttpPartSchema s, Type t, PropertyStore ps) { super(RESPONSE, m, s.getName(), t, HttpPartSchema.getApiCodeMap(s, 200)); - this.partSerializer = s.isUsePartSerializer() ? ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps) : null; + this.usePartSerializer = s.isUsePartSerializer() || s.getSerializer() != null; + this.partSerializer = usePartSerializer ? ClassUtils.newInstance(HttpPartSerializer.class, s.getSerializer(), true, ps) : null; this.schema = s; this._default = s.getDefault(); @@ -679,14 +681,17 @@ class RestParamDefaults { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override /* RestMethodParam */ - public Object resolve(RestRequest req, final RestResponse res) throws Exception { + public Object resolve(final RestRequest req, final RestResponse res) throws Exception { Value<Object> v = (Value<Object>)c.newInstance(); v.listener(new ValueListener() { @Override public void onSet(Object newValue) { try { - if (partSerializer != null) - newValue = new StringReader(partSerializer.serialize(HttpPartType.BODY, schema, newValue)); + if (usePartSerializer) { + HttpPartSerializer ps = partSerializer == null ? req.getPartSerializer() : partSerializer; + if (ps != null) + newValue = new StringReader(ps.serialize(HttpPartType.BODY, schema, newValue)); + } res.setOutput(newValue); } catch (SchemaValidationException | SerializeException e) { throw new RuntimeException(e); 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 067ceba..5507f15 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 @@ -1609,4 +1609,27 @@ public final class RestRequest extends HttpServletRequestWrapper { void setJavaMethod(Method method) { this.javaMethod = method; } + + /** + * Returns metadata about the method return type. + * + * @return Metadata about the method return type. + */ + public RestMethodReturn getRestMethodReturn() { + return restJavaMethod.getRestMethodReturn(); + } + + /** + * Returns metadata about the specified exception. + * + * @param e + * @return Metadata about the specified exception. + */ + public RestMethodThrown getRestMethodThrown(Throwable e) { + if (restJavaMethod != null) + for (RestMethodThrown rmt : restJavaMethod.getRestMethodThrown()) + if (rmt.getType().isAssignableFrom(e.getClass())) + return rmt; + return null; + } } \ No newline at end of file diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java index a7f9b0e..daf3fea 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java @@ -572,7 +572,7 @@ public @interface RestResource { * <li class='jf'>{@link RestContext#REST_partParser} * </ul> */ - Class<? extends HttpPartParser> partParser() default UonPartParser.class; + Class<? extends HttpPartParser> partParser() default HttpPartParser.Null.class; /** * HTTP part serializer. @@ -585,7 +585,7 @@ public @interface RestResource { * <li class='jf'>{@link RestContext#REST_partSerializer} * </ul> */ - Class<? extends HttpPartSerializer> partSerializer() default SimpleUonPartSerializer.class; + Class<? extends HttpPartSerializer> partSerializer() default HttpPartSerializer.Null.class; /** * Resource path. @@ -952,7 +952,7 @@ public @interface RestResource { * <p> * Enables the following: * <ul> - * <li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestException)} is called. + * <li>A message and stack trace is printed to STDERR when {@link BasicRestCallHandler#handleError(HttpServletRequest, HttpServletResponse, RestRequest, Throwable)} is called. * </ul> * * <h5 class='section'>Notes:</h5> diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/mock/MockServletRequest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/mock/MockServletRequest.java index 26a64c4..eaae103 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/mock/MockServletRequest.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/mock/MockServletRequest.java @@ -73,6 +73,7 @@ public class MockServletRequest implements HttpServletRequest, MockHttpRequest { private HttpSession httpSession = MockHttpSession.create(); private RestContext restContext; private String uri = ""; + private boolean debug = false; /** * Creates a new servlet request. @@ -221,9 +222,31 @@ public class MockServletRequest implements HttpServletRequest, MockHttpRequest { if (res.getStatus() == 0) throw new RuntimeException("Response status was 0."); + if (debug) + log(this, res); + return res; } + private void log(MockServletRequest req, MockServletResponse res) { + StringBuilder sb = new StringBuilder(); + sb.append("\n=== HTTP Call ================================================================="); + + sb.append("\n=== REQUEST ==="); + sb.append("\nTODO"); + sb.append("\n=== RESPONSE ==="); + sb.append("\nStatus: ").append(res.getStatus()); + sb.append("\n---response headers---"); + for (Map.Entry<String,String[]> h : res.getHeaders().entrySet()) + for (String h2 : h.getValue()) + sb.append("\n").append(h.getKey()).append(": ").append(h2); + sb.append("\n---response content---\n"); + sb.append(res.getBodyAsString()); + sb.append("\n=== END ========================================================================"); + + System.err.println(sb); + } + /** * Fluent setter. * @@ -1326,4 +1349,17 @@ public class MockServletRequest implements HttpServletRequest, MockHttpRequest { public MockServletRequest warning(Object value) { return header("Warning", value); } + + /** + * Enabled debug mode on this request. + * + * <p> + * Causes information about the request execution to be sent to STDERR. + * + * @return This object (for method chaining). + */ + public MockServletRequest debug() { + this.debug = true; + return this; + } } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/DefaultHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/DefaultHandler.java index 22cae79..89ed2a9 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/DefaultHandler.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/DefaultHandler.java @@ -17,6 +17,7 @@ import java.util.*; import org.apache.juneau.*; import org.apache.juneau.http.*; +import org.apache.juneau.httppart.*; import org.apache.juneau.internal.*; import org.apache.juneau.rest.*; import org.apache.juneau.rest.exception.*; @@ -50,6 +51,9 @@ public class DefaultHandler implements ResponseHandler { String accept = req.getHeaders().getString("Accept", ""); SerializerMatch sm = g.getSerializerMatch(accept); + RestMethodReturn rmr = req.getRestMethodReturn(); + res.setStatus(rmr.getCode()); + if (sm != null) { Serializer s = sm.getSerializer(); MediaType mediaType = res.getMediaType(); @@ -97,22 +101,37 @@ public class DefaultHandler implements ResponseHandler { } catch (SerializeException e) { throw new InternalServerError(e); } + return true; + + } + + HttpPartSerializer ps = rmr.getPartSerializer(); + if (ps != null) { + try { + FinishablePrintWriter w = res.getNegotiatedWriter(); + w.append(ps.serialize(rmr.getSchema(), output)); + w.flush(); + w.finish(); + } catch (SchemaValidationException | SerializeException e) { + throw new InternalServerError(e); + } + return true; + } // Non-existent Accept or plain/text can just be serialized as-is. - } else if ("".equals(accept) || "plain/text".equals(accept)) { + if ("".equals(accept) || "plain/text".equals(accept)) { FinishablePrintWriter w = res.getNegotiatedWriter(); ClassMeta<?> cm = req.getBeanSession().getClassMetaForObject(output); if (cm != null) w.append(cm.toString(output)); w.flush(); w.finish(); - - } else { - throw new NotAcceptable( - "Unsupported media-type in request header ''Accept'': ''{0}''\n\tSupported media-types: {1}", - req.getHeaders().getString("Accept", ""), g.getSupportedMediaTypes() - ); + return true; } - return true; + + throw new NotAcceptable( + "Unsupported media-type in request header ''Accept'': ''{0}''\n\tSupported media-types: {1}", + req.getHeaders().getString("Accept", ""), g.getSupportedMediaTypes() + ); } } diff --git a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java index 917659b..f5f2317 100644 --- a/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java +++ b/juneau-rest/juneau-rest-server/src/test/java/org/apache/juneau/rest/annotation/ResponseAnnotationTest.java @@ -20,10 +20,13 @@ import java.util.*; import org.apache.juneau.dto.swagger.*; import org.apache.juneau.http.annotation.*; +import org.apache.juneau.httppart.*; import org.apache.juneau.rest.*; import org.apache.juneau.rest.mock.*; +import org.apache.juneau.serializer.*; import org.junit.*; import org.junit.runners.*; +import org.apache.juneau.utils.*; /** * Tests the @Response annotation. @@ -43,6 +46,461 @@ public class ResponseAnnotationTest { return ip.getSwagger(req); } + + //================================================================================================================= + // Status codes + //================================================================================================================= + + //----------------------------------------------------------------------------------------------------------------- + // HTTP status code + //----------------------------------------------------------------------------------------------------------------- + + @RestResource + public static class A { + + @Response(code=201) + @RestMethod(name=GET,path="/codeOnMethod") + public String a01() { + return "foo"; + } + + @RestMethod(name=GET,path="/codeOnClass") + public A02 a02() { + return new A02(); + } + + @RestMethod(name=GET,path="/codeOnThrown") + public String a03() throws A03 { + throw new A03(); + } + + @Response(code=201) + @RestMethod(name=GET,path="/codeOnParameter") + public void a04(@Response(code=201) Value<String> value) { + value.set("foo"); + } + } + + @Response(code=201) + public static class A02 { + @Override + public String toString() {return "foo";} + } + + @Response(code=501) + public static class A03 extends Exception { + @Override + public String toString() {return "foo";} + } + + static MockRest a = MockRest.create(A.class); + + @Test + public void a01_codeOnMethod() throws Exception { + a.get("/codeOnMethod").execute().assertStatus(201).assertBody("foo"); + } + + @Test + public void a02_codeOnClass() throws Exception { + a.get("/codeOnClass").execute().assertStatus(201).assertBody("foo"); + } + + @Test + public void a03_codeOnThrown() throws Exception { + a.get("/codeOnThrown").execute().assertStatus(501); + } + + @Test + public void a04_codeOnParameter() throws Exception { + a.get("/codeOnParameter").execute().assertStatus(201).assertBody("foo"); + } + + //================================================================================================================= + // PartSerializers + //================================================================================================================= + + public static class XPartSerializer implements HttpPartSerializer { + @Override + public HttpPartSerializerSession createSession(SerializerSessionArgs args) { + return new HttpPartSerializerSession() { + @Override + public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException { + return "x" + value + "x"; + } + }; + } + + @Override + public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SchemaValidationException, SerializeException { + return createSession(null).serialize(partType, schema, value); + } + + @Override + public String serialize(HttpPartSchema schema, Object value) throws SchemaValidationException, SerializeException { + return createSession(null).serialize(null, schema, value); + } + } + + //----------------------------------------------------------------------------------------------------------------- + // @Response(usePartSerializer) + //----------------------------------------------------------------------------------------------------------------- + + @RestResource(partSerializer=XPartSerializer.class) + public static class B { + + @Response(usePartSerializer=true) + @RestMethod(name=GET,path="/useOnMethod") + public String b01() { + return "foo"; + } + + @Response(usePartSerializer=false) + @RestMethod(name=GET,path="/dontUseOnMethod") + public String b02() { + return "foo"; + } + + @RestMethod(name=GET,path="/useOnClass") + public B03 b03() { + return new B03(); + } + + @RestMethod(name=GET,path="/dontUseOnClass") + public B04 b04() { + return new B04(); + } + + @RestMethod(name=GET,path="/useOnThrown") + public String b05() throws B05 { + throw new B05(); + } + + @RestMethod(name=GET,path="/dontUseOnThrown") + public String b06() throws B06 { + throw new B06(); + } + + @RestMethod(name=GET,path="/useOnParameter") + public void b07(@Response(usePartSerializer=true) Value<String> value) { + value.set("foo"); + } + + @RestMethod(name=GET,path="/dontUseOnParameter") + public void b08(@Response(usePartSerializer=false) Value<String> value) { + value.set("foo"); + } + + } + + @Response(usePartSerializer=true) + public static class B03 { + @Override + public String toString() {return "foo";} + } + + @Response(usePartSerializer=false) + public static class B04 { + @Override + public String toString() {return "foo";} + } + + @Response(usePartSerializer=true) + public static class B05 extends Exception { + @Override + public String toString() {return "foo";} + } + + @Response(usePartSerializer=false) + public static class B06 extends Exception { + @Override + public String toString() {return "foo";} + } + + static MockRest b = MockRest.create(B.class); + + + @Test + public void b01_useOnMethod() throws Exception { + b.get("/useOnMethod").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void b02_dontUseOnMethod() throws Exception { + b.get("/dontUseOnMethod").execute().assertStatus(200).assertBody("foo"); + } + + @Test + public void b03_useOnClass() throws Exception { + b.get("/useOnClass").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void b04_dontUseOnClass() throws Exception { + b.get("/dontUseOnClass").execute().assertStatus(200).assertBody("foo"); + } + + @Test + public void b05_useOnThrown() throws Exception { + b.get("/useOnThrown").execute().assertStatus(500).assertBody("xfoox"); + } + + @Test + public void b06_dontUseOnThrown() throws Exception { + b.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("HTTP 500: Internal Server Error"); + } + + @Test + public void b07_useOnParameter() throws Exception { + b.get("/useOnParameter").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void b08_dontUseOnParameter() throws Exception { + b.get("/dontUseOnParameter").execute().assertStatus(200).assertBody("foo"); + } + + //----------------------------------------------------------------------------------------------------------------- + // @Response(partSerializer) + //----------------------------------------------------------------------------------------------------------------- + + @RestResource + public static class C { + + @Response(partSerializer=XPartSerializer.class) + @RestMethod(name=GET,path="/useOnMethod") + public String c01() { + return "foo"; + } + + @Response + @RestMethod(name=GET,path="/dontUseOnMethod") + public String c02() { + return "foo"; + } + + @RestMethod(name=GET,path="/useOnClass") + public C03 c03() { + return new C03(); + } + + @RestMethod(name=GET,path="/dontUseOnClass") + public C04 c04() { + return new C04(); + } + + @RestMethod(name=GET,path="/useOnThrown") + public String c05() throws C05 { + throw new C05(); + } + + @RestMethod(name=GET,path="/dontUseOnThrown") + public String c06() throws C06 { + throw new C06(); + } + + @RestMethod(name=GET,path="/useOnParameter") + public void c07(@Response(partSerializer=XPartSerializer.class) Value<String> value) { + value.set("foo"); + } + + @RestMethod(name=GET,path="/dontUseOnParameter") + public void c08(@Response Value<String> value) { + value.set("foo"); + } + + } + + @Response(partSerializer=XPartSerializer.class) + public static class C03 { + @Override + public String toString() {return "foo";} + } + + @Response + public static class C04 { + @Override + public String toString() {return "foo";} + } + + @Response(partSerializer=XPartSerializer.class) + public static class C05 extends Exception { + @Override + public String toString() {return "foo";} + } + + @Response + public static class C06 extends Exception { + @Override + public String toString() {return "foo";} + } + + static MockRest c = MockRest.create(C.class); + + + @Test + public void c01_useOnMethod() throws Exception { + c.get("/useOnMethod").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void c02_dontUseOnMethod() throws Exception { + c.get("/dontUseOnMethod").execute().assertStatus(200).assertBody("foo"); + } + + @Test + public void c03_useOnClass() throws Exception { + c.get("/useOnClass").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void c04_dontUseOnClass() throws Exception { + c.get("/dontUseOnClass").execute().assertStatus(200).assertBody("foo"); + } + + @Test + public void c05_useOnThrown() throws Exception { + c.get("/useOnThrown").execute().assertStatus(500).assertBody("xfoox"); + } + + @Test + public void c06_dontUseOnThrown() throws Exception { + c.get("/dontUseOnThrown").execute().assertStatus(500).assertBodyContains("HTTP 500: Internal Server Error"); + } + + @Test + public void c07_useOnParameter() throws Exception { + c.get("/useOnParameter").execute().assertStatus(200).assertBody("xfoox"); + } + + @Test + public void c08_dontUseOnParameter() throws Exception { + c.get("/dontUseOnParameter").execute().assertStatus(200).assertBody("foo"); + } + + //----------------------------------------------------------------------------------------------------------------- + // @Response(partSerializer) with schemas + //----------------------------------------------------------------------------------------------------------------- + + @RestResource + public static class D { + + @Response(schema=@Schema(collectionFormat="pipes"),usePartSerializer=true) + @RestMethod(name=GET,path="/useOnMethod") + public String[] d01() { + return new String[]{"foo","bar"}; + } + + @RestMethod(name=GET,path="/useOnClass") + public D02 d02() { + return new D02(); + } + + @RestMethod(name=GET,path="/useOnThrown") + public String d03() throws D03 { + throw new D03(); + } + + @RestMethod(name=GET,path="/useOnParameter") + public void d04(@Response(schema=@Schema(collectionFormat="pipes"),usePartSerializer=true) Value<String[]> value) { + value.set(new String[]{"foo","bar"}); + } + + @Response(schema=@Schema(type="string",format="byte"),usePartSerializer=true) + @RestMethod(name=GET,path="/useOnMethodBytes") + public byte[] d05() { + return "foo".getBytes(); + } + + @RestMethod(name=GET,path="/useOnClassBytes") + public D06 d06() { + return new D06(); + } + + @RestMethod(name=GET,path="/useOnThrownBytes") + public String d07() throws D07 { + throw new D07(); + } + + + @RestMethod(name=GET,path="/useOnParameterBytes") + public void d08(@Response(schema=@Schema(type="string",format="byte"),usePartSerializer=true) Value<byte[]> value) { + value.set("foo".getBytes()); + } + + } + + @Response(schema=@Schema(type="array",collectionFormat="pipes"),usePartSerializer=true) + public static class D02 { + public String[] toStringArray() { + return new String[]{"foo","bar"}; + } + } + + @Response(schema=@Schema(type="array",collectionFormat="pipes"),usePartSerializer=true) + public static class D03 extends Exception { + public String[] toStringArray() { + return new String[]{"foo","bar"}; + } + } + + @Response(schema=@Schema(format="byte"),usePartSerializer=true) + public static class D06 { + public byte[] toBytes() { + return "foo".getBytes(); + } + } + + @Response(schema=@Schema(format="byte"),usePartSerializer=true) + public static class D07 extends Exception { + public byte[] toBytes() { + return "foo".getBytes(); + } + } + + static MockRest d = MockRest.create(D.class); + + + @Test + public void d01_useOnMethod() throws Exception { + d.get("/useOnMethod").execute().assertStatus(200).assertBody("foo|bar"); + } + + @Test + public void d02_useOnClass() throws Exception { + d.get("/useOnClass").execute().assertStatus(200).assertBody("foo|bar"); + } + + @Test + public void d03_useOnThrown() throws Exception { + d.get("/useOnThrown").execute().assertStatus(500).assertBody("foo|bar"); + } + + @Test + public void d04_useOnParameter() throws Exception { + d.get("/useOnParameter").execute().assertStatus(200).assertBody("foo|bar"); + } + + @Test + public void d05_useOnMethodBytes() throws Exception { + d.get("/useOnMethodBytes").execute().assertStatus(200).assertBody("Zm9v"); + } + + @Test + public void d06_useOnClassBytes() throws Exception { + d.get("/useOnClassBytes").execute().assertStatus(200).assertBody("Zm9v"); + } + + @Test + public void d07_useOnThrownBytes() throws Exception { + d.get("/useOnThrownBytes").execute().assertStatus(500).assertBody("Zm9v"); + } + + @Test + public void d08_useOnParameterBytes() throws Exception { + d.get("/useOnParameterBytes").execute().assertStatus(200).assertBody("Zm9v"); + } + + //================================================================================================================= // @Response on POJO //=================================================================================================================