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 58db5a9 REST API refactoring. 58db5a9 is described below commit 58db5a98ac7103643f6587337722870500db6368 Author: JamesBognar <james.bog...@salesforce.com> AuthorDate: Tue Jan 26 09:59:38 2021 -0500 REST API refactoring. --- .../java/org/apache/juneau/rest/RestContext.java | 663 +++++++--- .../org/apache/juneau/rest/SwaggerProvider.java | 1320 ++++++++++++++++++++ .../apache/juneau/rest/SwaggerProviderBuilder.java | 115 ++ 3 files changed, 1958 insertions(+), 140 deletions(-) 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 d70c08e..f3a5e06 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 @@ -18,8 +18,10 @@ import static org.apache.juneau.internal.ObjectUtils.*; import static org.apache.juneau.internal.IOUtils.*; import static org.apache.juneau.internal.StringUtils.*; import static org.apache.juneau.rest.HttpRuntimeException.*; +import static org.apache.juneau.rest.logging.RestLoggingDetail.*; import static org.apache.juneau.Enablement.*; import static java.util.Collections.*; +import static java.util.logging.Level.*; import static java.util.Arrays.*; import java.io.*; @@ -63,6 +65,7 @@ import org.apache.juneau.rest.converters.*; import org.apache.juneau.rest.logging.*; import org.apache.juneau.rest.params.*; import org.apache.juneau.http.exception.*; +import org.apache.juneau.http.header.*; import org.apache.juneau.rest.reshandlers.*; import org.apache.juneau.rest.util.*; import org.apache.juneau.rest.vars.*; @@ -2893,6 +2896,34 @@ public class RestContext extends BeanContext { public static final String REST_staticFilesDefault = PREFIX + ".staticFilesDefault.o"; /** + * Configuration property: Swagger provider class. + * + * <h5 class='section'>Property:</h5> + * <ul class='spaced-list'> + * <li><b>ID:</b> {@link org.apache.juneau.rest.RestContext#REST_swaggerProviderClass REST_swaggerProviderClass} + * <li><b>Name:</b> <js>"RestContext.swaggerProviderClass.c"</js> + * <li><b>Data type:</b> {@link org.apache.juneau.rest.SwaggerProvider} + * <li><b>Default:</b> {@link org.apache.juneau.rest.SwaggerProvider} + * <li><b>Session property:</b> <jk>false</jk> + * <li><b>Annotations:</b> + * <ul> + * <li class='ja'>{@link org.apache.juneau.rest.annotation.Rest#infoProvider()} + * </ul> + * <li><b>Methods:</b> + * <ul> + * <li class='jm'>{@link org.apache.juneau.rest.RestContextBuilder#infoProvider(Class)} + * <li class='jm'>{@link org.apache.juneau.rest.RestContextBuilder#infoProvider(RestInfoProvider)} + * </ul> + * + * <h5 class='section'>Description:</h5> + * <p> + * The default static file finder. + * <p> + * This setting is inherited from the parent context. + */ + public static final String REST_swaggerProviderClass = PREFIX + ".swaggerProviderClass.c"; + + /** * Configuration property: Supported accept media types. * * <h5 class='section'>Property:</h5> @@ -3421,6 +3452,7 @@ public class RestContext extends BeanContext { private final StackTraceStore stackTraceStore; private final Logger logger; private final RestInfoProvider infoProvider; + private final SwaggerProvider swaggerProvider; private final HttpException initException; private final RestContext parentContext; final BeanFactory rootBeanFactory; @@ -3503,9 +3535,9 @@ public class RestContext extends BeanContext { parentContext = builder.parentContext; ClassInfo rci = ClassInfo.ofProxy(r); - rootBeanFactory = createRootBeanFactory(r); + rootBeanFactory = createBeanFactory(r); - beanFactory = createBeanFactory(r); + beanFactory = BeanFactory.of(rootBeanFactory, r); beanFactory.addBean(BeanFactory.class, beanFactory); beanFactory.addBean(RestContext.class, this); beanFactory.addBean(Object.class, r); @@ -3597,10 +3629,11 @@ public class RestContext extends BeanContext { preCallMethods = createPreCallMethods(r).stream().map(this::toRestMethodInvoker).toArray(RestMethodInvoker[]:: new); postCallMethods = createPostCallMethods(r).stream().map(this::toRestMethodInvoker).toArray(RestMethodInvoker[]:: new); - restMethods = createRestMethods(r).build(); - restChildren = createRestChildren(r).build(); + restMethods = createRestMethods(r); + restChildren = createRestChildren(r); infoProvider = createInfoProvider(r, beanFactory); + swaggerProvider = createSwaggerProvider(r, beanFactory); } catch (HttpException e) { _initException = e; @@ -3622,9 +3655,85 @@ public class RestContext extends BeanContext { } /** + * Instantiates the bean factory for this REST resource. + * + * <p> + * The bean factory is typically used for passing in injected beans into REST contexts and for storing beans + * created by the REST context. + * + * <p> + * Instantiates based on the following logic: + * <ul> + * <li>Returns the resource class itself if it's an instance of {@link BeanFactory}. + * <li>Looks for {@link #REST_beanFactory} value set via any of the following: + * <ul> + * <li>{@link RestContextBuilder#beanFactory(Class)}/{@link RestContextBuilder#beanFactory(BeanFactory)} + * <li>{@link Rest#beanFactory()}. + * </ul> + * <li>Instantiates a new {@link BeanFactory}. + * Uses the parent context's root bean factory as the parent bean factory if this is a child resource. + * </ul> + * + * <p> + * Your REST class can also implement a create method called <c>createBeanFactory()</c> to instantiate your own + * bean factory. + * + * <h5 class='figure'>Example:</h5> + * <p class='bpcode w800'> + * <ja>@Rest</ja> + * <jk>public class</jk> MyRestClass { + * + * <jk>public</jk> BeanFactory createBeanFactory(Optional<BeanFactory> <jv>parentBeanFactory</jv>) <jk>throws</jk> Exception { + * <jc>// Create your own bean factory here.</jc> + * } + * } + * </p> + * + * <p> + * The <c>createBeanFactory()</c> method can be static or non-static can contain any of the following arguments: + * <ul> + * <li><c>{@link Optional}<{@link BeanFactory}></c> - The parent root bean factory if this is a child resource. + * </ul> + * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_beanFactory} + * </ul> + * + * @param resource The REST resource object. + * @return The bean factory for this REST resource. + * @throws Exception If bean factory could not be instantiated. + */ + protected BeanFactory createBeanFactory(Object resource) throws Exception { + + BeanFactory x = null; + + if (resource instanceof BeanFactory) + x = (BeanFactory)resource; + + if (x == null && parentContext != null) + x = parentContext.rootBeanFactory; + + if (x == null) + x = getInstanceProperty(REST_beanFactory, BeanFactory.class, null, x); + + x = BeanFactory + .of(x, resource) + .addBean(BeanFactory.class, x) + .beanCreateMethodFinder(BeanFactory.class, resource) + .find("createBeanFactory") + .withDefault(x) + .run(); + + return x; + } + + /** * Instantiates the file finder for this REST resource. * * <p> + * The file finder is used to retrieve localized files from the classpath. + * + * <p> * Instantiates based on the following logic: * <ul> * <li>Returns the resource class itself is an instance of {@link FileFinder}. @@ -3633,23 +3742,44 @@ public class RestContext extends BeanContext { * <li>{@link RestContextBuilder#fileFinder(Class)}/{@link RestContextBuilder#fileFinder(FileFinder)} * <li>{@link Rest#fileFinder()}. * </ul> - * <li>Looks for a static or non-static <c>createFileFinder()</> method that returns {@link FileFinder} on the - * resource class with any of the following arguments: - * <ul> - * <li>{@link RestContext} - * <li>{@link BeanFactory} - * <li>Any {@doc RestInjection injected beans}. - * </ul> - * <li>Resolves it via the bean factory registered in this context (including any Spring beans). + * <li>Resolves it via the {@link #createBeanFactory(Object) bean factory} registered in this context (including Spring beans if using SpringRestServlet). * <li>Looks for value in {@link #REST_fileFinderDefault} setting. - * <li>Instantiates a {@link BasicFileFinder}. + * <li>Instantiates via {@link #createFileFinderBuilder(Object, BeanFactory)}. + * </ul> + * + * <p> + * Your REST class can also implement a create method called <c>createFileFinder()</c> to instantiate your own + * file finder. + * + * <h5 class='figure'>Example:</h5> + * <p class='bpcode w800'> + * <ja>@Rest</ja> + * <jk>public class</jk> MyRestClass { + * + * <jk>public</jk> FileFinder createFileFinder() <jk>throws</jk> Exception { + * <jc>// Create your own file finder here.</jc> + * } + * } + * </p> + * + * <p> + * The <c>createFileFinder()</c> method can be static or non-static can contain any of the following arguments: + * <ul> + * <li>{@link FileFinder} - The file finder that would have been returned by this method. + * <li>{@link FileFinderBuilder} - The file finder returned by {@link #createFileFinderBuilder(Object,BeanFactory)}. + * <li>{@link RestContext} - This REST context. + * <li>{@link BeanFactory} - The bean factory of this REST context. + * <li>Any {@doc RestInjection injected bean} types. Use {@link Optional} arguments for beans that may not exist. + * </ul> + * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_fileFinder} * </ul> * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The file finder for this REST resource. * @throws Exception If file finder could not be instantiated. - * @seealso #REST_fileFinder */ protected FileFinder createFileFinder(Object resource, BeanFactory beanFactory) throws Exception { @@ -3668,7 +3798,7 @@ public class RestContext extends BeanContext { x = getInstanceProperty(REST_fileFinderDefault, FileFinder.class, null, beanFactory); if (x == null) - x = new BasicFileFinder(this); + x = createFileFinderBuilder(resource, beanFactory).build(); x = BeanFactory .of(beanFactory, resource) @@ -3682,6 +3812,39 @@ public class RestContext extends BeanContext { } /** + * Instantiates the file finder builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createFileFinder(Object, BeanFactory)} method. + * + * @param resource The REST resource object. + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The file finder builder for this REST resource. + * @throws Exception If file finder builder could not be instantiated. + */ + protected FileFinderBuilder createFileFinderBuilder(Object resource, BeanFactory beanFactory) throws Exception { + + FileFinderBuilder x = FileFinder + .create() + .dir("static") + .dir("htdocs") + .cp(getResourceClass(), "htdocs", true) + .cp(getResourceClass(), "/htdocs", true) + .caching(1_000_000) + .exclude("(?i).*\\.(class|properties)"); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(FileFinderBuilder.class, x) + .beanCreateMethodFinder(FileFinderBuilder.class, resource) + .find("createFileFinder") + .withDefault(x) + .run(); + + return x; + } + + /** * Instantiates the REST info provider for this REST resource. * * <p> @@ -3704,11 +3867,14 @@ public class RestContext extends BeanContext { * <li>Instantiates a {@link BasicRestInfoProvider}. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_infoProvider} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The info provider for this REST resource. * @throws Exception If info provider could not be instantiated. - * @seealso #REST_infoProvider */ protected RestInfoProvider createInfoProvider(Object resource, BeanFactory beanFactory) throws Exception { @@ -3762,11 +3928,14 @@ public class RestContext extends BeanContext { * <li>Instantiates a {@link BasicStaticFiles}. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_staticFiles} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The file finder for this REST resource. * @throws Exception If file finder could not be instantiated. - * @seealso #REST_staticFiles */ protected StaticFiles createStaticFiles(Object resource, BeanFactory beanFactory) throws Exception { @@ -3785,7 +3954,7 @@ public class RestContext extends BeanContext { x = getInstanceProperty(REST_staticFilesDefault, StaticFiles.class, null, beanFactory); if (x == null) - x = new BasicStaticFiles(this); + x = createStaticFilesBuilder(resource, beanFactory).build(); x = BeanFactory .of(beanFactory, resource) @@ -3799,6 +3968,40 @@ public class RestContext extends BeanContext { } /** + * Instantiates the static files builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createStaticFiles(Object, BeanFactory)} method. + * + * @param resource The REST resource object. + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The static files builder for this REST resource. + * @throws Exception If static files builder could not be instantiated. + */ + protected StaticFilesBuilder createStaticFilesBuilder(Object resource, BeanFactory beanFactory) throws Exception { + + StaticFilesBuilder x = StaticFiles + .create() + .dir("static") + .dir("htdocs") + .cp(getResourceClass(), "htdocs", true) + .cp(getResourceClass(), "/htdocs", true) + .caching(1_000_000) + .exclude("(?i).*\\.(class|properties)") + .headers(CacheControl.of("max-age=86400, public")); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(StaticFilesBuilder.class, x) + .beanCreateMethodFinder(StaticFilesBuilder.class, resource) + .find("createStaticFiles") + .withDefault(x) + .run(); + + return x; + } + + /** * Instantiates the call logger this REST resource. * * <p> @@ -3823,11 +4026,14 @@ public class RestContext extends BeanContext { * <li>Instantiates a {@link BasicFileFinder}. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_callLogger} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The file finder for this REST resource. * @throws Exception If file finder could not be instantiated. - * @seealso #REST_callLogger */ protected RestLogger createCallLogger(Object resource, BeanFactory beanFactory) throws Exception { @@ -3846,7 +4052,7 @@ public class RestContext extends BeanContext { x = getInstanceProperty(REST_callLoggerDefault, RestLogger.class, null, beanFactory); if (x == null) - x = new BasicRestLogger(this); + x = createCallLoggerBuilder(resource, beanFactory).build(); x = BeanFactory .of(beanFactory, resource) @@ -3860,112 +4066,49 @@ public class RestContext extends BeanContext { } /** - * Instantiates the bean factory for this REST resource. + * Instantiates the call logger builder for this REST resource. * * <p> - * Instantiates based on the following logic: - * <ul> - * <li>Returns the resource class itself is an instance of {@link BeanFactory}. - * <li>Looks for {@link #REST_beanFactory} value set via any of the following: - * <ul> - * <li>{@link RestContextBuilder#beanFactory(Class)}/{@link RestContextBuilder#beanFactory(BeanFactory)} - * <li>{@link Rest#beanFactory()}. - * </ul> - * <li>Looks for a static or non-static <c>beanFactory()</> method that returns {@link BeanFactory} on the - * resource class with any of the following arguments: - * <ul> - * <li>{@link RestContext} - * <li>{@link BeanFactory} - The parent resource bean factory if this is a child. - * <li>Any {@doc RestInjection injected beans}. - * </ul> - * <li>Resolves it via the bean factory registered in this context. - * <li>Instantiates a {@link BeanFactory}. - * </ul> + * Allows subclasses to intercept and modify the builder used by the {@link #createCallLogger(Object, BeanFactory)} method. * * @param resource The REST resource object. - * @return The bean factory for this REST resource. - * @throws Exception If bean factory could not be instantiated. - * @seealso #REST_beanFactory - */ - protected BeanFactory createBeanFactory(Object resource) throws Exception { - - BeanFactory x = null; - - if (resource instanceof BeanFactory) - x = (BeanFactory)resource; - - BeanFactory bf = createRootBeanFactory(resource) - .addBean(RestContext.class, this) - .addBean(BeanFactory.class, parentContext == null ? null : parentContext.rootBeanFactory) - .addBean(PropertyStore.class, getPropertyStore()) - .addBean(Object.class, resource); - - if (x == null) - x = getInstanceProperty(REST_beanFactory, BeanFactory.class, null, bf); - - if (x == null) - x = bf; - - x = bf - .beanCreateMethodFinder(BeanFactory.class, resource) - .find("createBeanFactory") - .withDefault(x) - .run(); - - return x; - } - - /** - * Instantiates the root bean factory for this REST resource. - * - * <p> - * The root bean factory is the factory used for passing in injected beans. - * Beans created by this context are not added to this factory. - * - * <p> - * Instantiates based on the following logic: - * <ul> - * <li>Returns the resource class itself is an instance of {@link BeanFactory}. - * <li>Looks for {@link #REST_beanFactory} value set via any of the following: - * <ul> - * <li>{@link RestContextBuilder#beanFactory(Class)}/{@link RestContextBuilder#beanFactory(BeanFactory)} - * <li>{@link Rest#beanFactory()}. - * </ul> - * <li>Looks for a static or non-static <c>beanFactory()</> method that returns {@link BeanFactory} on the - * resource class with any of the following arguments: - * <ul> - * <li>{@link RestContext} - * <li>{@link BeanFactory} - The parent resource bean factory if this is a child. - * </ul> - * <li>Resolves it via the bean factory registered in this context. - * <li>Instantiates a {@link BeanFactory}. - * </ul> - * - * @param resource The REST resource object. - * @return The bean factory for this REST resource. - * @throws Exception If bean factory could not be instantiated. - * @seealso #REST_beanFactory + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The call logger builder for this REST resource. + * @throws Exception If call logger builder could not be instantiated. */ - protected BeanFactory createRootBeanFactory(Object resource) throws Exception { - - BeanFactory x = null; - - if (resource instanceof BeanFactory) - x = (BeanFactory)resource; - - BeanFactory parent = parentContext == null ? null : parentContext.rootBeanFactory; - BeanFactory bf = BeanFactory.of(parent, resource); - bf.addBean(BeanFactory.class, bf); - - if (x == null) - x = getInstanceProperty(REST_beanFactory, BeanFactory.class, null, bf); + protected RestLoggerBuilder createCallLoggerBuilder(Object resource, BeanFactory beanFactory) throws Exception { - if (x == null) - x = bf; + RestLoggerBuilder x = RestLogger + .create() + .normalRules( // Rules when debugging is not enabled. + RestLogger.createRule() // Log 500+ errors with status-line and header information. + .statusFilter(a -> a >= 500) + .level(SEVERE) + .requestDetail(HEADER) + .responseDetail(HEADER) + .build(), + RestLogger.createRule() // Log 400-500 errors with just status-line information. + .statusFilter(a -> a >= 400) + .level(WARNING) + .requestDetail(STATUS_LINE) + .responseDetail(STATUS_LINE) + .build() + ) + .debugRules( // Rules when debugging is enabled. + RestLogger.createRule() // Log everything with full details. + .level(SEVERE) + .requestDetail(ENTITY) + .responseDetail(ENTITY) + .build() + ) + .logger(getLogger()) + .stackTraceStore(getStackTraceStore()); - x = bf - .beanCreateMethodFinder(BeanFactory.class, resource) - .find("createBeanFactory") + x = BeanFactory + .of(beanFactory, resource) + .addBean(RestLoggerBuilder.class, x) + .beanCreateMethodFinder(RestLoggerBuilder.class, resource) + .find("createCallLogger") .withDefault(x) .run(); @@ -3994,11 +4137,14 @@ public class RestContext extends BeanContext { * <li>Instantiates a <c>ResponseHandler[0]</c>. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_responseHandlers} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The response handlers for this REST resource. * @throws Exception If response handlers could not be instantiated. - * @seealso #REST_responseHandlers */ protected ResponseHandlerList createResponseHandlers(Object resource, BeanFactory beanFactory) throws Exception { @@ -4042,12 +4188,15 @@ public class RestContext extends BeanContext { * <li>Instantiates a <c>Serializer[0]</c>. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_serializers} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @param ps The property store to apply to all serialiers. * @return The serializers for this REST resource. * @throws Exception If serializers could not be instantiated. - * @seealso #REST_serializers */ protected SerializerGroup createSerializers(Object resource, BeanFactory beanFactory, PropertyStore ps) throws Exception { @@ -4102,12 +4251,15 @@ public class RestContext extends BeanContext { * <li>Instantiates a <c>Parser[0]</c>. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_parsers} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @param ps The property store to apply to all serialiers. * @return The parsers for this REST resource. * @throws Exception If parsers could not be instantiated. - * @seealso #REST_parsers */ protected ParserGroup createParsers(Object resource, BeanFactory beanFactory, PropertyStore ps) throws Exception { @@ -4163,11 +4315,14 @@ public class RestContext extends BeanContext { * <li>Instantiates an {@link OpenApiSerializer}. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_partSerializer} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The HTTP part serializer for this REST resource. * @throws Exception If serializer could not be instantiated. - * @seealso #REST_partSerializer */ protected HttpPartSerializer createPartSerializer(Object resource, BeanFactory beanFactory) throws Exception { @@ -4219,11 +4374,14 @@ public class RestContext extends BeanContext { * <li>Instantiates an {@link OpenApiSerializer}. * </ul> * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_partParser} + * </ul> + * * @param resource The REST resource object. * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The HTTP part parser for this REST resource. * @throws Exception If parser could not be instantiated. - * @seealso #REST_partParser */ protected HttpPartParser createPartParser(Object resource, BeanFactory beanFactory) throws Exception { @@ -4272,7 +4430,6 @@ public class RestContext extends BeanContext { * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The REST method parameter resolvers for this REST resource. * @throws Exception If parameter resolvers could not be instantiated. - * @seealso #REST_paramResolvers */ @SuppressWarnings("unchecked") protected RestParamList createRestParams(Object resource, BeanFactory beanFactory) throws Exception { @@ -4344,7 +4501,6 @@ public class RestContext extends BeanContext { * @param beanFactory The bean factory to use for retrieving and creating beans. * @return The REST method parameter resolvers for this REST resource. * @throws Exception If parameter resolvers could not be instantiated. - * @seealso #REST_paramResolvers */ @SuppressWarnings("unchecked") protected RestParamList createHookMethodParams(Object resource, BeanFactory beanFactory) throws Exception { @@ -4446,14 +4602,10 @@ public class RestContext extends BeanContext { * @throws Exception If JSON schema generator could not be instantiated. */ protected JsonSchemaGenerator createJsonSchemaGenerator(Object resource, BeanFactory beanFactory) throws Exception { - JsonSchemaGenerator x = beanFactory.getBean(JsonSchemaGenerator.class).orElse(null); if (x == null) - x = JsonSchemaGenerator - .create() - .apply(getPropertyStore()) - .build(); + x = createJsonSchemaGeneratorBuilder(resource, beanFactory).build(); x = BeanFactory .of(beanFactory, resource) @@ -4467,6 +4619,116 @@ public class RestContext extends BeanContext { } /** + * Instantiates the JSON-schema generator builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createJsonSchemaGenerator(Object, BeanFactory)} method. + * + * @param resource The REST resource object. + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The JSON-schema generator builder for this REST resource. + * @throws Exception If JSON-schema generator builder could not be instantiated. + */ + protected JsonSchemaGeneratorBuilder createJsonSchemaGeneratorBuilder(Object resource, BeanFactory beanFactory) throws Exception { + JsonSchemaGeneratorBuilder x = JsonSchemaGenerator + .create() + .apply(getPropertyStore()); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(JsonSchemaGeneratorBuilder.class, x) + .beanCreateMethodFinder(JsonSchemaGeneratorBuilder.class, resource) + .find("createJsonSchemaGenerator") + .withDefault(x) + .run(); + + return x; + } + + /** + * Instantiates the REST info provider for this REST resource. + * + * <p> + * Instantiates based on the following logic: + * <ul> + * <li>Returns the resource class itself is an instance of {@link RestInfoProvider}. + * <li>Looks for {@link #REST_infoProvider} value set via any of the following: + * <ul> + * <li>{@link RestContextBuilder#infoProvider(Class)}/{@link RestContextBuilder#infoProvider(RestInfoProvider)} + * <li>{@link Rest#infoProvider()}. + * </ul> + * <li>Looks for a static or non-static <c>createInfoProvider()</> method that returns {@link RestInfoProvider} on the + * resource class with any of the following arguments: + * <ul> + * <li>{@link RestContext} + * <li>{@link BeanFactory} + * <li>Any {@doc RestInjection injected beans}. + * </ul> + * <li>Resolves it via the bean factory registered in this context. + * <li>Instantiates a {@link BasicRestInfoProvider}. + * </ul> + * + * <ul class='seealso'> + * <li class='jf'>{@link #REST_infoProvider} + * </ul> + * + * @param resource The REST resource object. + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The info provider for this REST resource. + * @throws Exception If info provider could not be instantiated. + */ + protected SwaggerProvider createSwaggerProvider(Object resource, BeanFactory beanFactory) throws Exception { + SwaggerProvider x = beanFactory.getBean(SwaggerProvider.class).orElse(null); + + if (x == null) + x = createSwaggerProviderBuilder(resource, beanFactory).build(); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(SwaggerProvider.class, x) + .beanCreateMethodFinder(SwaggerProvider.class, resource) + .find("createSwaggerProvider") + .withDefault(x) + .run(); + + return x; + } + + /** + * Instantiates the REST API builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createSwaggerProvider(Object, BeanFactory)} method. + * + * @param resource The REST resource object. + * @param beanFactory The bean factory to use for retrieving and creating beans. + * @return The REST API builder for this REST resource. + * @throws Exception If REST API builder could not be instantiated. + */ + protected SwaggerProviderBuilder createSwaggerProviderBuilder(Object resource, BeanFactory beanFactory) throws Exception { + + SwaggerProviderBuilder x = SwaggerProvider + .create() + .beanFactory(beanFactory) + .fileFinder(getFileFinder()) + .messages(getMessages()) + .varResolver(getVarResolver()) + .jsonSchemaGenerator(createJsonSchemaGenerator(resource, beanFactory)) + .implClass(getClassProperty(REST_swaggerProviderClass, SwaggerProvider.class, SwaggerProvider.class)); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(SwaggerProviderBuilder.class, x) + .beanCreateMethodFinder(SwaggerProviderBuilder.class, resource) + .find("createSwaggerProvider") + .withDefault(x) + .run(); + + return x; + + } + + /** * Instantiates the variable resolver for this REST resource. * * <p> @@ -4723,21 +4985,58 @@ public class RestContext extends BeanContext { * @throws Exception An error occurred. */ protected Messages createMessages(Object resource) throws Exception { + + Messages x = createMessagesBuilder(resource).build(); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(Messages.class, x) + .beanCreateMethodFinder(Messages.class, resource) + .find("createMessages") + .withDefault(x) + .run(); + + return x; + } + + /** + * Instantiates the Messages builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createMessages(Object)} method. + * + * @param resource The REST resource object. + * @return The messages builder for this REST resource. + * @throws Exception If messages builder could not be instantiated. + */ + protected MessagesBuilder createMessagesBuilder(Object resource) throws Exception { + Tuple2<Class<?>,String>[] mbl = getInstanceArrayProperty(REST_messages, Tuple2.class); - Messages msgs = null; + MessagesBuilder x = null; + for (int i = mbl.length-1; i >= 0; i--) { Class<?> c = firstNonNull(mbl[i].getA(), resource.getClass()); String value = mbl[i].getB(); if (isJsonObject(value,true)) { - MessagesString x = SimpleJson.DEFAULT.read(value, MessagesString.class); - msgs = Messages.create(c).name(x.name).baseNames(split(x.baseNames, ',')).locale(x.locale).parent(msgs).build(); + MessagesString ms = SimpleJson.DEFAULT.read(value, MessagesString.class); + x = Messages.create(c).name(ms.name).baseNames(split(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build()); } else { - msgs = Messages.create(c).name(value).parent(msgs).build(); + x = Messages.create(c).name(value).parent(x == null ? null : x.build()); } } - if (msgs == null) - msgs = Messages.create(resource.getClass()).build(); - return msgs; + + if (x == null) + x = Messages.create(resource.getClass()); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(MessagesBuilder.class, x) + .beanCreateMethodFinder(MessagesBuilder.class, resource) + .find("createMessages") + .withDefault(x) + .run(); + + return x; } private static class MessagesString { @@ -4753,7 +5052,33 @@ public class RestContext extends BeanContext { * @return The builder for the {@link RestMethods} object. * @throws Exception An error occurred. */ - protected RestMethodsBuilder createRestMethods(Object resource) throws Exception { + protected RestMethods createRestMethods(Object resource) throws Exception { + + RestMethods x = createRestMethodsBuilder(resource).build(); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(RestMethods.class, x) + .beanCreateMethodFinder(RestMethods.class, resource) + .find("createRestMethods") + .withDefault(x) + .run(); + + return x; + } + + /** + * Instantiates the REST methods builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createRestMethods(Object)} method. + * + * @param resource The REST resource object. + * @return The REST methods builder for this REST resource. + * @throws Exception If REST methods builder could not be instantiated. + */ + protected RestMethodsBuilder createRestMethodsBuilder(Object resource) throws Exception { + RestMethodsBuilder x = RestMethods .create() .beanFactory(rootBeanFactory) @@ -4810,6 +5135,14 @@ public class RestContext extends BeanContext { } } + x = BeanFactory + .of(beanFactory, resource) + .addBean(RestMethodsBuilder.class, x) + .beanCreateMethodFinder(RestMethodsBuilder.class, resource) + .find("createRestMethods") + .withDefault(x) + .run(); + return x; } @@ -4820,7 +5153,33 @@ public class RestContext extends BeanContext { * @return The builder for the {@link RestChildren} object. * @throws Exception An error occurred. */ - protected RestChildrenBuilder createRestChildren(Object resource) throws Exception { + protected RestChildren createRestChildren(Object resource) throws Exception { + + RestChildren x = createRestChildrenBuilder(resource).build(); + + x = BeanFactory + .of(beanFactory, resource) + .addBean(RestChildren.class, x) + .beanCreateMethodFinder(RestChildren.class, resource) + .find("createRestChildren") + .withDefault(x) + .run(); + + return x; + } + + /** + * Instantiates the REST children builder for this REST resource. + * + * <p> + * Allows subclasses to intercept and modify the builder used by the {@link #createRestChildren(Object)} method. + * + * @param resource The REST resource object. + * @return The REST children builder for this REST resource. + * @throws Exception If REST children builder could not be instantiated. + */ + protected RestChildrenBuilder createRestChildrenBuilder(Object resource) throws Exception { + RestChildrenBuilder x = RestChildren .create() .beanFactory(rootBeanFactory) @@ -4865,6 +5224,15 @@ public class RestContext extends BeanContext { x.add(cc); } + + x = BeanFactory + .of(beanFactory, resource) + .addBean(RestChildrenBuilder.class, x) + .beanCreateMethodFinder(RestChildrenBuilder.class, resource) + .find("createRestChildren") + .withDefault(x) + .run(); + return x; } @@ -5177,6 +5545,21 @@ public class RestContext extends BeanContext { } /** + * Returns the Swagger provider used by this resource. + * + * <ul class='seealso'> + * <li class='jf'>{@link RestContext#REST_swaggerProviderClass} + * </ul> + * + * @return + * The information provider for this resource. + * <br>Never <jk>null</jk>. + */ + public SwaggerProvider getSwaggerProvider() { + return swaggerProvider; + } + + /** * Returns the resource object. * * <p> diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProvider.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProvider.java new file mode 100644 index 0000000..ea01244 --- /dev/null +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProvider.java @@ -0,0 +1,1320 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * +// * with the License. You may obtain a copy of the License at * +// * * +// * http://www.apache.org/licenses/LICENSE-2.0 * +// * * +// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * +// * specific language governing permissions and limitations under the License. * +// *************************************************************************************************************************** +package org.apache.juneau.rest; + +import static org.apache.juneau.internal.ObjectUtils.*; +import static org.apache.juneau.internal.StringUtils.*; +import static org.apache.juneau.internal.StringUtils.isEmpty; +import static org.apache.juneau.rest.RestParamType.*; + +import java.io.*; +import java.lang.reflect.*; +import java.lang.reflect.Method; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.collections.*; +import org.apache.juneau.cp.*; +import org.apache.juneau.dto.swagger.*; +import org.apache.juneau.http.*; +import org.apache.juneau.http.annotation.*; +import org.apache.juneau.http.annotation.Contact; +import org.apache.juneau.http.annotation.License; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.jsonschema.*; +import org.apache.juneau.jsonschema.annotation.*; +import org.apache.juneau.jsonschema.annotation.Items; +import org.apache.juneau.jsonschema.annotation.Tag; +import org.apache.juneau.marshall.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.reflect.*; +import org.apache.juneau.rest.annotation.*; +import org.apache.juneau.rest.util.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; + +/** + * Interface for retrieving Swagger on a REST resource. + */ +public class SwaggerProvider { + + /** + * Creator. + * + * @return A new builder for this object. + */ + public static SwaggerProviderBuilder create() { + return new SwaggerProviderBuilder(); + } + + private final VarResolverSession vr; + private final JsonParser jp = JsonParser.create().ignoreUnknownBeanProperties().build(); + private final JsonSchemaGeneratorSession js; + private final Messages messages; + private final FileFinder fileFinder; + + /** + * Constructor. + * + * @param builder The builder containing the settings for this Swagger provider. + */ + public SwaggerProvider(SwaggerProviderBuilder builder) { + BeanFactory bf = builder.beanFactory; + + this.vr = firstNonNull(builder.varResolver, bf.getBean(VarResolver.class).orElse(VarResolver.DEFAULT)).createSession(); + this.js = firstNonNull(builder.jsonSchemaGenerator, bf.getBean(JsonSchemaGenerator.class).orElse(JsonSchemaGenerator.DEFAULT)).createSession(); + this.messages = builder.messages; + this.fileFinder = builder.fileFinder; + } + + /** + * Returns the Swagger associated with the specified {@link Rest}-annotated class. + * + * @param context The context of the {@link Rest}-annotated class. + * @param locale The request locale. + * @return A new {@link Swagger} object. + * @throws Exception If an error occurred producing the Swagger. + */ + public Swagger getSwagger(RestContext context, Locale locale) throws Exception { + + Class<?> c = context.getResourceClass(); + ClassInfo rci = ClassInfo.of(c); + + FileFinder ff = fileFinder != null ? fileFinder : FileFinder.create().cp(c,null,false).build(); + Messages mb = messages != null ? messages : Messages.create(c).build(); + + InputStream is = ff.getStream(rci.getSimpleName() + ".json", locale).orElse(null); + + // Load swagger JSON from classpath. + OMap omSwagger = SimpleJson.DEFAULT.read(is, OMap.class); + if (omSwagger == null) + omSwagger = new OMap(); + + // Combine it with @Rest(swagger) + for (Rest rr : rci.getAnnotations(Rest.class)) { + + OMap sInfo = omSwagger.getMap("info", true); + + sInfo + .appendSkipEmpty("title", + firstNonEmpty( + sInfo.getString("title"), + resolve(rr.title()) + ) + ) + .appendSkipEmpty("description", + firstNonEmpty( + sInfo.getString("description"), + resolve(rr.description()) + ) + ); + + ResourceSwagger r = rr.swagger(); + + omSwagger.append(parseMap(r.value(), "@ResourceSwagger(value) on class {0}", c)); + + if (! ResourceSwaggerAnnotation.empty(r)) { + OMap info = omSwagger.getMap("info", true); + + info + .appendSkipEmpty("title", resolve(r.title())) + .appendSkipEmpty("description", resolve(r.description())) + .appendSkipEmpty("version", resolve(r.version())) + .appendSkipEmpty("termsOfService", resolve(r.termsOfService())) + .appendSkipEmpty("contact", + merge( + info.getMap("contact"), + toMap(r.contact(), "@ResourceSwagger(contact) on class {0}", c) + ) + ) + .appendSkipEmpty("license", + merge( + info.getMap("license"), + toMap(r.license(), "@ResourceSwagger(license) on class {0}", c) + ) + ); + } + + omSwagger + .appendSkipEmpty("externalDocs", + merge( + omSwagger.getMap("externalDocs"), + toMap(r.externalDocs(), "@ResourceSwagger(externalDocs) on class {0}", c) + ) + ) + .appendSkipEmpty("tags", + merge( + omSwagger.getList("tags"), + toList(r.tags(), "@ResourceSwagger(tags) on class {0}", c) + ) + ); + } + + omSwagger.appendSkipEmpty("externalDocs", parseMap(mb.findFirstString("externalDocs"), "Messages/externalDocs on class {0}", c)); + + OMap info = omSwagger.getMap("info", true); + + info + .appendSkipEmpty("title", resolve(mb.findFirstString("title"))) + .appendSkipEmpty("description", resolve(mb.findFirstString("description"))) + .appendSkipEmpty("version", resolve(mb.findFirstString("version"))) + .appendSkipEmpty("termsOfService", resolve(mb.findFirstString("termsOfService"))) + .appendSkipEmpty("contact", parseMap(mb.findFirstString("contact"), "Messages/contact on class {0}", c)) + .appendSkipEmpty("license", parseMap(mb.findFirstString("license"), "Messages/license on class {0}", c)); + + if (info.isEmpty()) + omSwagger.remove("info"); + + OList + produces = omSwagger.getList("produces", true), + consumes = omSwagger.getList("consumes", true); + if (consumes.isEmpty()) + consumes.addAll(context.getConsumes()); + if (produces.isEmpty()) + produces.addAll(context.getProduces()); + + Map<String,OMap> tagMap = new LinkedHashMap<>(); + if (omSwagger.containsKey("tags")) { + for (OMap om : omSwagger.getList("tags").elements(OMap.class)) { + String name = om.getString("name"); + if (name == null) + throw new SwaggerException(null, "Tag definition found without name in swagger JSON."); + tagMap.put(name, om); + } + } + + String s = mb.findFirstString("tags"); + if (s != null) { + for (OMap m : parseListOrCdl(s, "Messages/tags on class {0}", c).elements(OMap.class)) { + String name = m.getString("name"); + if (name == null) + throw new SwaggerException(null, "Tag definition found without name in resource bundle on class {0}", c) ; + if (tagMap.containsKey(name)) + tagMap.get(name).putAll(m); + else + tagMap.put(name, m); + } + } + + // Load our existing bean definitions into our session. + OMap definitions = omSwagger.getMap("definitions", true); + for (String defId : definitions.keySet()) + js.addBeanDef(defId, new OMap(definitions.getMap(defId))); + + // Iterate through all the @RestMethod methods. + for (RestMethodContext sm : context.getMethodContexts()) { + + BeanSession bs = sm.createBeanSession(); + + Method m = sm.method; + MethodInfo mi = MethodInfo.of(m); + RestMethod rm = mi.getLastAnnotation(RestMethod.class); + String mn = m.getName(); + + // Get the operation from the existing swagger so far. + OMap op = getOperation(omSwagger, sm.getPathPattern(), sm.getHttpMethod().toLowerCase()); + + // Add @RestMethod(swagger) + MethodSwagger ms = rm.swagger(); + + op.append(parseMap(ms.value(), "@MethodSwagger(value) on class {0} method {1}", c, m)); + op.appendSkipEmpty("operationId", + firstNonEmpty( + resolve(ms.operationId()), + op.getString("operationId"), + mn + ) + ); + op.appendSkipEmpty("summary", + firstNonEmpty( + resolve(ms.summary()), + resolve(mb.findFirstString(mn + ".summary")), + op.getString("summary"), + resolve(rm.summary()) + ) + ); + op.appendSkipEmpty("description", + firstNonEmpty( + resolve(ms.description()), + resolve(mb.findFirstString(mn + ".description")), + op.getString("description"), + resolve(rm.description()) + ) + ); + op.appendSkipEmpty("deprecated", + firstNonEmpty( + resolve(ms.deprecated()), + (m.getAnnotation(Deprecated.class) != null || m.getDeclaringClass().getAnnotation(Deprecated.class) != null) ? "true" : null + ) + ); + op.appendSkipEmpty("tags", + merge( + parseListOrCdl(mb.findFirstString(mn + ".tags"), "Messages/tags on class {0} method {1}", c, m), + parseListOrCdl(ms.tags(), "@MethodSwagger(tags) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("schemes", + merge( + parseListOrCdl(mb.findFirstString(mn + ".schemes"), "Messages/schemes on class {0} method {1}", c, m), + parseListOrCdl(ms.schemes(), "@MethodSwagger(schemes) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("consumes", + firstNonEmpty( + parseListOrCdl(mb.findFirstString(mn + ".consumes"), "Messages/consumes on class {0} method {1}", c, m), + parseListOrCdl(ms.consumes(), "@MethodSwagger(consumes) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("produces", + firstNonEmpty( + parseListOrCdl(mb.findFirstString(mn + ".produces"), "Messages/produces on class {0} method {1}", c, m), + parseListOrCdl(ms.produces(), "@MethodSwagger(produces) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("parameters", + merge( + parseList(mb.findFirstString(mn + ".parameters"), "Messages/parameters on class {0} method {1}", c, m), + parseList(ms.parameters(), "@MethodSwagger(parameters) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("responses", + merge( + parseMap(mb.findFirstString(mn + ".responses"), "Messages/responses on class {0} method {1}", c, m), + parseMap(ms.responses(), "@MethodSwagger(responses) on class {0} method {1}", c, m) + ) + ); + op.appendSkipEmpty("externalDocs", + merge( + op.getMap("externalDocs"), + parseMap(mb.findFirstString(mn + ".externalDocs"), "Messages/externalDocs on class {0} method {1}", c, m), + toMap(ms.externalDocs(), "@MethodSwagger(externalDocs) on class {0} method {1}", c, m) + ) + ); + + if (op.containsKey("tags")) + for (String tag : op.getList("tags").elements(String.class)) + if (! tagMap.containsKey(tag)) + tagMap.put(tag, OMap.of("name", tag)); + + OMap paramMap = new OMap(); + if (op.containsKey("parameters")) + for (OMap param : op.getList("parameters").elements(OMap.class)) + paramMap.put(param.getString("in") + '.' + ("body".equals(param.getString("in")) ? "body" : param.getString("name")), param); + + // Finally, look for parameters defined on method. + for (ParamInfo mpi : mi.getParams()) { + + ClassInfo pt = mpi.getParameterType(); + Type type = pt.innerType(); + + if (mpi.hasAnnotation(Body.class) || pt.hasAnnotation(Body.class)) { + OMap param = paramMap.getMap(BODY + ".body", true).a("in", BODY); + for (Body a : mpi.getAnnotations(Body.class)) + merge(param, a); + for (Body a : pt.getAnnotations(Body.class)) + merge(param, a); + param.putIfAbsent("required", true); + param.appendSkipEmpty("schema", getSchema(param.getMap("schema"), type, bs)); + addBodyExamples(sm, param, false, type, locale); + + } else if (mpi.hasAnnotation(Query.class) || pt.hasAnnotation(Query.class)) { + String name = null; + for (Query a : mpi.getAnnotations(Query.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + for (Query a : pt.getAnnotations(Query.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + OMap param = paramMap.getMap(QUERY + "." + name, true).a("name", name).a("in", QUERY); + for (Query a : mpi.getAnnotations(Query.class)) + merge(param, a); + for (Query a : pt.getAnnotations(Query.class)) + merge(param, a); + mergePartSchema(param, getSchema(param.getMap("schema"), type, bs)); + addParamExample(sm, param, QUERY, type); + + } else if (mpi.hasAnnotation(FormData.class) || pt.hasAnnotation(FormData.class)) { + String name = null; + for (FormData a : mpi.getAnnotations(FormData.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + for (FormData a : pt.getAnnotations(FormData.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + OMap param = paramMap.getMap(FORM_DATA + "." + name, true).a("name", name).a("in", FORM_DATA); + for (FormData a : mpi.getAnnotations(FormData.class)) + merge(param, a); + for (FormData a : pt.getAnnotations(FormData.class)) + merge(param, a); + mergePartSchema(param, getSchema(param.getMap("schema"), type, bs)); + addParamExample(sm, param, FORM_DATA, type); + + } else if (mpi.hasAnnotation(Header.class) || pt.hasAnnotation(Header.class)) { + String name = null; + for (Header a : mpi.getAnnotations(Header.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + for (Header a : pt.getAnnotations(Header.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + OMap param = paramMap.getMap(HEADER + "." + name, true).a("name", name).a("in", HEADER); + for (Header a : mpi.getAnnotations(Header.class)) + merge(param, a); + for (Header a : pt.getAnnotations(Header.class)) + merge(param, a); + mergePartSchema(param, getSchema(param.getMap("schema"), type, bs)); + addParamExample(sm, param, HEADER, type); + + } else if (mpi.hasAnnotation(Path.class) || pt.hasAnnotation(Path.class)) { + String name = null; + for (Path a : mpi.getAnnotations(Path.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + for (Path a : pt.getAnnotations(Path.class)) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + OMap param = paramMap.getMap(PATH + "." + name, true).a("name", name).a("in", PATH); + for (Path a : mpi.getAnnotations(Path.class)) + merge(param, a); + for (Path a : pt.getAnnotations(Path.class)) + merge(param, a); + mergePartSchema(param, getSchema(param.getMap("schema"), type, bs)); + addParamExample(sm, param, PATH, type); + param.putIfAbsent("required", true); + } + } + + if (! paramMap.isEmpty()) + op.put("parameters", paramMap.values()); + + OMap responses = op.getMap("responses", true); + + for (ClassInfo eci : mi.getExceptionTypes()) { + if (eci.hasAnnotation(Response.class)) { + List<Response> la = eci.getAnnotations(Response.class); + Set<Integer> codes = getCodes(la, 500); + for (Response a : la) { + for (Integer code : codes) { + OMap om = responses.getMap(String.valueOf(code), true); + merge(om, a); + if (! om.containsKey("schema")) + om.appendSkipEmpty("schema", getSchema(om.getMap("schema"), eci.inner(), bs)); + } + } + for (MethodInfo ecmi : eci.getAllMethodsParentFirst()) { + ResponseHeader a = ecmi.getLastAnnotation(ResponseHeader.class); + if (a == null) + a = ecmi.getReturnType().unwrap(Value.class,Optional.class).getLastAnnotation(ResponseHeader.class); + if (a != null && ! isMulti(a)) { + String ha = a.name(); + for (Integer code : codes) { + OMap header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(ha, true); + merge(header, a); + mergePartSchema(header, getSchema(header, ecmi.getReturnType().innerType(), bs)); + } + } + } + } + } + + if (mi.hasAnnotation(Response.class) || mi.getReturnType().unwrap(Value.class,Optional.class).hasAnnotation(Response.class)) { + List<Response> la = mi.getAnnotations(Response.class); + Set<Integer> codes = getCodes(la, 200); + for (Response a : la) { + for (Integer code : codes) { + OMap om = responses.getMap(String.valueOf(code), true); + merge(om, a); + if (! om.containsKey("schema")) + om.appendSkipEmpty("schema", getSchema(om.getMap("schema"), m.getGenericReturnType(), bs)); + addBodyExamples(sm, om, true, m.getGenericReturnType(), locale); + } + } + if (mi.getReturnType().hasAnnotation(Response.class)) { + for (MethodInfo ecmi : mi.getReturnType().getAllMethodsParentFirst()) { + if (ecmi.hasAnnotation(ResponseHeader.class)) { + ResponseHeader a = ecmi.getLastAnnotation(ResponseHeader.class); + String ha = a.name(); + if (! isMulti(a)) { + for (Integer code : codes) { + OMap header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(ha, true); + merge(header, a); + mergePartSchema(header, getSchema(header, ecmi.getReturnType().innerType(), bs)); + } + } + } + } + } + } else if (m.getGenericReturnType() != void.class) { + OMap om = responses.getMap("200", true); + if (! om.containsKey("schema")) + om.appendSkipEmpty("schema", getSchema(om.getMap("schema"), m.getGenericReturnType(), bs)); + addBodyExamples(sm, om, true, m.getGenericReturnType(), locale); + } + + // Finally, look for @ResponseHeader parameters defined on method. + for (ParamInfo mpi : mi.getParams()) { + + ClassInfo pt = mpi.getParameterType(); + + if (mpi.hasAnnotation(ResponseHeader.class) || pt.hasAnnotation(ResponseHeader.class)) { + List<ResponseHeader> la = AList.of(mpi.getAnnotations(ResponseHeader.class)).a(pt.getAnnotations(ResponseHeader.class)); + Set<Integer> codes = getCodes2(la, 200); + String name = null; + for (ResponseHeader a : la) + name = firstNonEmpty(a.name(), a.n(), a.value(), name); + Type type = mpi.getParameterType().innerType(); + for (ResponseHeader a : la) { + if (! isMulti(a)) { + for (Integer code : codes) { + OMap header = responses.getMap(String.valueOf(code), true).getMap("headers", true).getMap(name, true); + merge(header, a); + mergePartSchema(header, getSchema(header, Value.getParameterType(type), bs)); + } + } + } + + } else if (mpi.hasAnnotation(Response.class) || pt.hasAnnotation(Response.class)) { + List<Response> la = AList.of(mpi.getAnnotations(Response.class)).a(pt.getAnnotations(Response.class)); + Set<Integer> codes = getCodes(la, 200); + Type type = mpi.getParameterType().innerType(); + for (Response a : la) { + for (Integer code : codes) { + OMap response = responses.getMap(String.valueOf(code), true); + merge(response, a); + } + } + type = Value.getParameterType(type); + if (type != null) { + for (String code : responses.keySet()) { + OMap om = responses.getMap(code); + if (! om.containsKey("schema")) + om.appendSkipEmpty("schema", getSchema(om.getMap("schema"), type, bs)); + } + } + } + } + + // Add default response descriptions. + for (Map.Entry<String,Object> e : responses.entrySet()) { + String key = e.getKey(); + OMap val = responses.getMap(key); + if (StringUtils.isDecimal(key)) + val.appendIf(false, true, true, "description", RestUtils.getHttpResponseText(Integer.parseInt(key))); + } + + if (responses.isEmpty()) + op.remove("responses"); + else + op.put("responses", new TreeMap<>(responses)); + + if (! op.containsKey("consumes")) { + List<MediaType> mConsumes = sm.supportedContentTypes; + if (! mConsumes.equals(consumes)) + op.put("consumes", mConsumes); + } + + if (! op.containsKey("produces")) { + List<MediaType> mProduces = sm.supportedAcceptTypes; + if (! mProduces.equals(produces)) + op.put("produces", mProduces); + } + } + + if (js.getBeanDefs() != null) + for (Map.Entry<String,OMap> e : js.getBeanDefs().entrySet()) + definitions.put(e.getKey(), fixSwaggerExtensions(e.getValue())); + if (definitions.isEmpty()) + omSwagger.remove("definitions"); + + if (! tagMap.isEmpty()) + omSwagger.put("tags", tagMap.values()); + + if (consumes.isEmpty()) + omSwagger.remove("consumes"); + if (produces.isEmpty()) + omSwagger.remove("produces"); + +// try { +// if (! omSwagger.isEmpty()) +// assertNoEmpties(omSwagger); +// } catch (SwaggerException e1) { +// System.err.println(omSwagger.toString(SimpleJsonSerializer.DEFAULT_READABLE)); +// throw e1; +// } + + try { + String swaggerJson = SimpleJsonSerializer.DEFAULT_READABLE.toString(omSwagger); +// System.err.println(swaggerJson); + return jp.parse(swaggerJson, Swagger.class); + } catch (Exception e) { + throw new RestServletException(e, "Error detected in swagger."); + } + } + //================================================================================================================= + // Utility methods + //================================================================================================================= + + private boolean isMulti(ResponseHeader h) { + if ("*".equals(h.name()) || "*".equals(h.value())) + return true; + return false; + } + + private OMap resolve(OMap om) throws ParseException { + OMap om2 = null; + if (om.containsKey("_value")) { + om = om.modifiable(); + om2 = parseMap(om.remove("_value")); + } else { + om2 = new OMap(); + } + for (Map.Entry<String,Object> e : om.entrySet()) { + Object val = e.getValue(); + if (val instanceof OMap) { + val = resolve((OMap)val); + } else if (val instanceof OList) { + val = resolve((OList) val); + } else if (val instanceof String) { + val = resolve(val.toString()); + } + om2.put(e.getKey(), val); + } + return om2; + } + + private OList resolve(OList om) throws ParseException { + OList ol2 = new OList(); + for (Object val : om) { + if (val instanceof OMap) { + val = resolve((OMap)val); + } else if (val instanceof OList) { + val = resolve((OList) val); + } else if (val instanceof String) { + val = resolve(val.toString()); + } + ol2.add(val); + } + return ol2; + } + + private String resolve(String[]...s) { + for (String[] ss : s) { + if (ss.length != 0) + return resolve(joinnl(ss)); + } + return null; + } + + private String resolve(String s) { + if (s == null) + return null; + return vr.resolve(s.trim()); + } + + private OMap parseMap(String[] o, String location, Object...args) throws ParseException { + if (o.length == 0) + return OMap.EMPTY_MAP; + try { + return parseMap(o); + } catch (ParseException e) { + throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args); + } + } + + private OMap parseMap(String o, String location, Object...args) throws ParseException { + try { + return parseMap(o); + } catch (ParseException e) { + throw new SwaggerException(e, "Malformed swagger JSON object encountered in " + location + ".", args); + } + } + + private OMap parseMap(Object o) throws ParseException { + if (o == null) + return null; + if (o instanceof String[]) + o = joinnl((String[])o); + if (o instanceof String) { + String s = o.toString(); + if (s.isEmpty()) + return null; + s = resolve(s); + if ("IGNORE".equalsIgnoreCase(s)) + return OMap.of("ignore", true); + if (! isJsonObject(s, true)) + s = "{" + s + "}"; + return OMap.ofJson(s); + } + if (o instanceof OMap) + return (OMap)o; + throw new SwaggerException(null, "Unexpected data type ''{0}''. Expected OMap or String.", o.getClass().getName()); + } + + private OList parseList(Object o, String location, Object...locationArgs) throws ParseException { + try { + if (o == null) + return null; + String s = (o instanceof String[] ? joinnl((String[])o) : o.toString()); + if (s.isEmpty()) + return null; + s = resolve(s); + if (! isJsonArray(s, true)) + s = "[" + s + "]"; + return OList.ofJson(s); + } catch (ParseException e) { + throw new SwaggerException(e, "Malformed swagger JSON array encountered in "+location+".", locationArgs); + } + } + + private OList parseListOrCdl(Object o, String location, Object...locationArgs) throws ParseException { + try { + if (o == null) + return null; + String s = (o instanceof String[] ? joinnl((String[])o) : o.toString()); + if (s.isEmpty()) + return null; + s = resolve(s); + return StringUtils.parseListOrCdl(s); + } catch (ParseException e) { + throw new SwaggerException(e, "Malformed swagger JSON array encountered in "+location+".", locationArgs); + } + } + + private OMap newMap(OMap om, String[] value, String location, Object...locationArgs) throws ParseException { + if (value.length == 0) + return om == null ? new OMap() : om; + OMap om2 = parseMap(joinnl(value), location, locationArgs); + if (om == null) + return om2; + return om.append(om2); + } + + private OMap merge(OMap...maps) { + OMap m = maps[0]; + for (int i = 1; i < maps.length; i++) { + if (maps[i] != null) { + if (m == null) + m = new OMap(); + m.putAll(maps[i]); + } + } + return m; + } + + private OList merge(OList...lists) { + OList l = lists[0]; + for (int i = 1; i < lists.length; i++) { + if (lists[i] != null) { + if (l == null) + l = new OList(); + l.addAll(lists[i]); + } + } + return l; + } + + @SafeVarargs + private final <T> T firstNonEmpty(T...t) { + return ObjectUtils.firstNonEmpty(t); + } + + private OMap toMap(ExternalDocs a, String location, Object...locationArgs) throws ParseException { + if (ExternalDocsAnnotation.empty(a)) + return null; + OMap om = newMap(new OMap(), a.value(), location, locationArgs) + .appendSkipEmpty("description", resolve(joinnl(a.description()))) + .appendSkipEmpty("url", resolve(a.url())); + return nullIfEmpty(om); + } + + private OMap toMap(Contact a, String location, Object...locationArgs) throws ParseException { + if (ContactAnnotation.empty(a)) + return null; + OMap om = newMap(new OMap(), a.value(), location, locationArgs) + .appendSkipEmpty("name", resolve(a.name())) + .appendSkipEmpty("url", resolve(a.url())) + .appendSkipEmpty("email", resolve(a.email())); + return nullIfEmpty(om); + } + + private OMap toMap(License a, String location, Object...locationArgs) throws ParseException { + if (LicenseAnnotation.empty(a)) + return null; + OMap om = newMap(new OMap(), a.value(), location, locationArgs) + .appendSkipEmpty("name", resolve(a.name())) + .appendSkipEmpty("url", resolve(a.url())); + return nullIfEmpty(om); + } + + private OMap toMap(Tag a, String location, Object...locationArgs) throws ParseException { + OMap om = newMap(new OMap(), a.value(), location, locationArgs); + om + .appendSkipEmpty("name", resolve(a.name())) + .appendSkipEmpty("description", resolve(joinnl(a.description()))) + .appendSkipNull("externalDocs", merge(om.getMap("externalDocs"), toMap(a.externalDocs(), location, locationArgs))); + return nullIfEmpty(om); + } + + private OList toList(Tag[] aa, String location, Object...locationArgs) throws ParseException { + if (aa.length == 0) + return null; + OList ol = new OList(); + for (Tag a : aa) + ol.add(toMap(a, location, locationArgs)); + return nullIfEmpty(ol); + } + + private OMap getSchema(OMap schema, Type type, BeanSession bs) throws Exception { + + if (type == Swagger.class) + return null; + + schema = newMap(schema); + + ClassMeta<?> cm = bs.getClassMeta(type); + + if (schema.getBoolean("ignore", false)) + return null; + + if (schema.containsKey("type") || schema.containsKey("$ref")) + return schema; + + OMap om = fixSwaggerExtensions(schema.append(js.getSchema(cm))); + + return nullIfEmpty(om); + } + + /** + * Replaces non-standard JSON-Schema attributes with standard Swagger attributes. + */ + private OMap fixSwaggerExtensions(OMap om) { + om + .appendSkipNull("discriminator", om.remove("x-discriminator")) + .appendSkipNull("readOnly", om.remove("x-readOnly")) + .appendSkipNull("xml", om.remove("x-xml")) + .appendSkipNull("externalDocs", om.remove("x-externalDocs")) + .appendSkipNull("example", om.remove("x-example")); + return nullIfEmpty(om); + } + + private void addBodyExamples(RestMethodContext sm, OMap piri, boolean response, Type type, Locale locale) throws Exception { + + String sex = piri.getString("x-example"); + + if (sex == null) { + OMap schema = resolveRef(piri.getMap("schema")); + if (schema != null) + sex = schema.getString("example", schema.getString("x-example")); + } + + if (isEmpty(sex)) + return; + + Object example = null; + if (isJson(sex)) { + example = jp.parse(sex, type); + } else { + ClassMeta<?> cm = js.getClassMeta(type); + if (cm.hasStringMutater()) { + example = cm.getStringMutater().mutate(sex); + } + } + + String examplesKey = response ? "examples" : "x-examples"; // Parameters don't have an examples attribute. + + OMap examples = piri.getMap(examplesKey); + if (examples == null) + examples = new OMap(); + + List<MediaType> mediaTypes = response ? sm.getSerializers().getSupportedMediaTypes() : sm.getParsers().getSupportedMediaTypes(); + + for (MediaType mt : mediaTypes) { + if (mt != MediaType.HTML) { + Serializer s2 = sm.getSerializers().getSerializer(mt); + if (s2 != null) { + SerializerSessionArgs args = + SerializerSessionArgs + .create() + .locale(locale) + .mediaType(mt) + .useWhitespace(true) + ; + try { + String eVal = s2.createSession(args).serializeToString(example); + examples.put(s2.getPrimaryMediaType().toString(), eVal); + } catch (Exception e) { + System.err.println("Could not serialize to media type ["+mt+"]: " + e.getLocalizedMessage()); // NOT DEBUG + } + } + } + } + + if (! examples.isEmpty()) + piri.put(examplesKey, examples); + } + + private void addParamExample(RestMethodContext sm, OMap piri, RestParamType in, Type type) throws Exception { + + String s = piri.getString("x-example"); + + if (isEmpty(s)) + return; + + OMap examples = piri.getMap("x-examples"); + if (examples == null) + examples = new OMap(); + + String paramName = piri.getString("name"); + + if (in == QUERY) + s = "?" + urlEncodeLax(paramName) + "=" + urlEncodeLax(s); + else if (in == FORM_DATA) + s = paramName + "=" + s; + else if (in == HEADER) + s = paramName + ": " + s; + else if (in == PATH) + s = sm.getPathPattern().replace("{"+paramName+"}", urlEncodeLax(s)); + + examples.put("example", s); + + if (! examples.isEmpty()) + piri.put("x-examples", examples); + } + + + private OMap resolveRef(OMap m) { + if (m == null) + return null; + if (m.containsKey("$ref") && js.getBeanDefs() != null) { + String ref = m.getString("$ref"); + if (ref.startsWith("#/definitions/")) + return js.getBeanDefs().get(ref.substring(14)); + } + return m; + } + + private OMap getOperation(OMap om, String path, String httpMethod) { + if (! om.containsKey("paths")) + om.put("paths", new OMap()); + om = om.getMap("paths"); + if (! om.containsKey(path)) + om.put(path, new OMap()); + om = om.getMap(path); + if (! om.containsKey(httpMethod)) + om.put(httpMethod, new OMap()); + return om.getMap(httpMethod); + } + + private static OMap newMap(OMap om) { + if (om == null) + return new OMap(); + return om.modifiable(); + } + + private OMap merge(OMap om, Body a) throws ParseException { + if (BodyAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.value().length > 0) + om.putAll(parseMap(a.value())); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipEmpty("x-examples", parseMap(a.examples()), parseMap(a.exs())) + .appendSkipFalse("required", a.required() || a.r()) + .appendSkipEmpty("schema", merge(om.getMap("schema"), a.schema())) + ; + } + + private OMap merge(OMap om, Query a) throws ParseException { + if (QueryAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipFalse("allowEmptyValue", a.allowEmptyValue() || a.aev()) + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipFalse("required", a.required() || a.r()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + ; + } + + private OMap merge(OMap om, FormData a) throws ParseException { + if (FormDataAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipFalse("allowEmptyValue", a.allowEmptyValue() || a.aev()) + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipFalse("required", a.required()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + ; + } + + private OMap merge(OMap om, Header a) throws ParseException { + if (HeaderAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipFalse("required", a.required() || a.r()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + ; + } + + private OMap merge(OMap om, Path a) throws ParseException { + if (PathAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + ; + } + + private OMap merge(OMap om, Schema a) throws ParseException { + if (SchemaAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.value().length > 0) + om.putAll(parseMap(a.value())); + return om + .appendSkipEmpty("additionalProperties", toOMap(a.additionalProperties())) + .appendSkipEmpty("allOf", joinnl(a.allOf())) + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("discriminator", a.discriminator()) + .appendSkipEmpty("description", resolve(a.description()), resolve(a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example()), resolve(a.ex())) + .appendSkipEmpty("examples", parseMap(a.examples()), parseMap(a.exs())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("externalDocs", merge(om.getMap("externalDocs"), a.externalDocs())) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("ignore", a.ignore() ? "true" : null) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipMinusOne("maxProperties", a.maxProperties(), a.maxp()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipMinusOne("minProperties", a.minProperties(), a.minp()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipEmpty("properties", toOMap(a.properties())) + .appendSkipFalse("readOnly", a.readOnly() || a.ro()) + .appendSkipFalse("required", a.required() || a.r()) + .appendSkipEmpty("title", a.title()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + .appendSkipEmpty("xml", joinnl(a.xml())) + .appendSkipEmpty("$ref", a.$ref()) + ; + } + + private OMap merge(OMap om, ExternalDocs a) throws ParseException { + if (ExternalDocsAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.value().length > 0) + om.putAll(parseMap(a.value())); + return om + .appendSkipEmpty("description", resolve(a.description())) + .appendSkipEmpty("url", a.url()) + ; + } + + private OMap merge(OMap om, Items a) throws ParseException { + if (ItemsAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.value().length > 0) + om.putAll(parseMap(a.value())); + return om + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipEmpty("$ref", a.$ref()) + ; + } + + private OMap merge(OMap om, SubItems a) throws ParseException { + if (SubItemsAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.value().length > 0) + om.putAll(parseMap(a.value())); + return om + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", toOMap(a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + .appendSkipEmpty("$ref", a.$ref()) + ; + } + + private OMap merge(OMap om, Response a) throws ParseException { + if (ResponseAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipEmpty("examples", parseMap(a.examples()), parseMap(a.exs())) + .appendSkipEmpty("headers", merge(om.getMap("headers"), a.headers())) + .appendSkipEmpty("schema", merge(om.getMap("schema"), a.schema())) + ; + } + + private OMap merge(OMap om, ResponseHeader[] a) throws ParseException { + if (a.length == 0) + return om; + om = newMap(om); + for (ResponseHeader aa : a) { + String name = StringUtils.firstNonEmpty(aa.name(), aa.value()); + if (isEmpty(name)) + throw new RuntimeException("@ResponseHeader used without name or value."); + om.getMap(name, true).putAll(merge(null, aa)); + } + return om; + } + + private OMap merge(OMap om, ResponseHeader a) throws ParseException { + if (ResponseHeaderAnnotation.empty(a)) + return om; + om = newMap(om); + if (a.api().length > 0) + om.putAll(parseMap(a.api())); + return om + .appendSkipEmpty("collectionFormat", a.collectionFormat(), a.cf()) + .appendSkipEmpty("default", joinnl(a._default(), a.df())) + .appendSkipEmpty("description", resolve(a.description(), a.d())) + .appendSkipEmpty("enum", toSet(a._enum()), toSet(a.e())) + .appendSkipEmpty("x-example", resolve(a.example(), a.ex())) + .appendSkipFalse("exclusiveMaximum", a.exclusiveMaximum() || a.emax()) + .appendSkipFalse("exclusiveMinimum", a.exclusiveMinimum() || a.emin()) + .appendSkipEmpty("format", a.format(), a.f()) + .appendSkipEmpty("items", merge(om.getMap("items"), a.items())) + .appendSkipEmpty("maximum", a.maximum(), a.max()) + .appendSkipMinusOne("maxItems", a.maxItems(), a.maxi()) + .appendSkipMinusOne("maxLength", a.maxLength(), a.maxl()) + .appendSkipEmpty("minimum", a.minimum(), a.min()) + .appendSkipMinusOne("minItems", a.minItems(), a.mini()) + .appendSkipMinusOne("minLength", a.minLength(), a.minl()) + .appendSkipEmpty("multipleOf", a.multipleOf(), a.mo()) + .appendSkipEmpty("pattern", a.pattern(), a.p()) + .appendSkipEmpty("type", a.type(), a.t()) + .appendSkipFalse("uniqueItems", a.uniqueItems() || a.ui()) + .appendSkipEmpty("$ref", a.$ref()) + ; + } + + private OMap mergePartSchema(OMap param, OMap schema) { + if (schema != null) { + param + .appendIf(false, true, true, "collectionFormat", schema.remove("collectionFormat")) + .appendIf(false, true, true, "default", schema.remove("default")) + .appendIf(false, true, true, "description", schema.remove("enum")) + .appendIf(false, true, true, "enum", schema.remove("enum")) + .appendIf(false, true, true, "x-example", schema.remove("x-example")) + .appendIf(false, true, true, "exclusiveMaximum", schema.remove("exclusiveMaximum")) + .appendIf(false, true, true, "exclusiveMinimum", schema.remove("exclusiveMinimum")) + .appendIf(false, true, true, "format", schema.remove("format")) + .appendIf(false, true, true, "items", schema.remove("items")) + .appendIf(false, true, true, "maximum", schema.remove("maximum")) + .appendIf(false, true, true, "maxItems", schema.remove("maxItems")) + .appendIf(false, true, true, "maxLength", schema.remove("maxLength")) + .appendIf(false, true, true, "minimum", schema.remove("minimum")) + .appendIf(false, true, true, "minItems", schema.remove("minItems")) + .appendIf(false, true, true, "minLength", schema.remove("minLength")) + .appendIf(false, true, true, "multipleOf", schema.remove("multipleOf")) + .appendIf(false, true, true, "pattern", schema.remove("pattern")) + .appendIf(false, true, true, "required", schema.remove("required")) + .appendIf(false, true, true, "type", schema.remove("type")) + .appendIf(false, true, true, "uniqueItems", schema.remove("uniqueItems")); + + if ("object".equals(param.getString("type")) && ! schema.isEmpty()) + param.put("schema", schema); + } + + return param; + } + + + + private OMap toOMap(String[] ss) throws ParseException { + if (ss.length == 0) + return null; + String s = joinnl(ss); + if (s.isEmpty()) + return null; + if (! isJsonObject(s, true)) + s = "{" + s + "}"; + s = resolve(s); + return OMap.ofJson(s); + } + + private Set<String> toSet(String[] ss) throws ParseException { + if (ss.length == 0) + return null; + String s = joinnl(ss); + if (s.isEmpty()) + return null; + s = resolve(s); + Set<String> set = ASet.of(); + for (Object o : StringUtils.parseListOrCdl(s)) + set.add(o.toString()); + return set; + } + + static String joinnl(String[]...s) { + for (String[] ss : s) { + if (ss.length != 0) + return StringUtils.joinnl(ss).trim(); + } + return ""; + } + + private static Set<Integer> getCodes(List<Response> la, Integer def) { + Set<Integer> codes = new TreeSet<>(); + for (Response a : la) { + for (int i : a.value()) + codes.add(i); + for (int i : a.code()) + codes.add(i); + } + if (codes.isEmpty() && def != null) + codes.add(def); + return codes; + } + + private static Set<Integer> getCodes2(List<ResponseHeader> la, Integer def) { + Set<Integer> codes = new TreeSet<>(); + for (ResponseHeader a : la) { + for (int i : a.code()) + codes.add(i); + } + if (codes.isEmpty() && def != null) + codes.add(def); + return codes; + } + + private static OMap nullIfEmpty(OMap m) { + return (m == null || m.isEmpty() ? null : m); + } + + private static OList nullIfEmpty(OList l) { + return (l == null || l.isEmpty() ? null : l); + } +} diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProviderBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProviderBuilder.java new file mode 100644 index 0000000..f8d89f6 --- /dev/null +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/SwaggerProviderBuilder.java @@ -0,0 +1,115 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * +// * with the License. You may obtain a copy of the License at * +// * * +// * http://www.apache.org/licenses/LICENSE-2.0 * +// * * +// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * +// * specific language governing permissions and limitations under the License. * +// *************************************************************************************************************************** +package org.apache.juneau.rest; + +import static org.apache.juneau.rest.HttpRuntimeException.*; + +import org.apache.juneau.cp.*; +import org.apache.juneau.http.exception.*; +import org.apache.juneau.jsonschema.*; +import org.apache.juneau.svl.*; + +/** + * Builder class for {@link SwaggerProvider} objects. + */ +public class SwaggerProviderBuilder { + + private Class<? extends SwaggerProvider> implClass; + + BeanFactory beanFactory; + Class<?> resourceClass; + VarResolver varResolver; + JsonSchemaGenerator jsonSchemaGenerator; + Messages messages; + FileFinder fileFinder; + + /** + * Creates a new {@link SwaggerProvider} object from this builder. + * + * @return A new {@link SwaggerProvider} object. + */ + public SwaggerProvider build() { + try { + Class<? extends SwaggerProvider> ic = implClass == null ? SwaggerProvider.class : implClass; + return BeanFactory.of(beanFactory).addBean(SwaggerProviderBuilder.class, this).createBean(ic); + } catch (Exception e) { + throw toHttpException(e, InternalServerError.class); + } + } + + /** + * Specifies the bean factory to use for instantiating the {@link SwaggerProvider} object. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder beanFactory(BeanFactory value) { + this.beanFactory = value; + return this; + } + + /** + * Specifies the variable resolver to use for the {@link SwaggerProvider} object. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder varResolver(VarResolver value) { + this.varResolver = value; + return this; + } + + /** + * Specifies the JSON-schema generator to use for the {@link SwaggerProvider} object. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder jsonSchemaGenerator(JsonSchemaGenerator value) { + this.jsonSchemaGenerator = value; + return this; + } + + /** + * Specifies the messages to use for the {@link SwaggerProvider} object. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder messages(Messages value) { + this.messages = value; + return this; + } + + /** + * Specifies the file-finder to use for the {@link SwaggerProvider} object. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder fileFinder(FileFinder value) { + this.fileFinder = value; + return this; + } + + /** + * Specifies a subclass of {@link SwaggerProvider} to create when the {@link #build()} method is called. + * + * @param value The new value for this setting. + * @return This object (for method chaining). + */ + public SwaggerProviderBuilder implClass(Class<? extends SwaggerProvider> value) { + this.implClass = value; + return this; + } +}