From f7fb6827e2ef19565894a3eb96aca55c13056394 Mon Sep 17 00:00:00 2001 From: joshua bauer Date: Mon, 22 Oct 2018 10:27:25 -0700 Subject: [PATCH] Initial OpenAPI 3 support. --- conf/logback-test.xml | 117 -- conf/logback.xml | 18 +- pom.xml | 196 +- .../sinistral/proteus/ProteusApplication.java | 1 + .../proteus/server/tools/oas/Reader.java | 1829 +++++++++++++++++ .../server/tools/oas/ServerModelResolver.java | 166 ++ .../tools/oas/ServerParameterExtension.java | 323 +++ .../{ => tools}/swagger/AnnotationHelper.java | 3 +- .../server/{ => tools}/swagger/Reader.java | 2 +- .../swagger/ServerParameterExtension.java | 2 +- .../proteus/services/OpenAPIService.java | 444 ++++ .../proteus/services/SwaggerService.java | 81 +- .../{ => server/tools}/swagger/index.html | 0 .../tools/swagger}/proteus-logo.svg | 0 .../{ => server/tools}/swagger/redoc.html | 0 .../tools}/swagger/swagger-ui-bundle.js | 0 .../swagger/swagger-ui-standalone-preset.js | 0 .../{ => server/tools}/swagger/swagger-ui.css | 0 .../{ => server/tools}/swagger/swagger-ui.js | 0 .../swagger/themes/theme-feeling-blue.css | 0 .../tools}/swagger/themes/theme-flattop.css | 0 .../tools}/swagger/themes/theme-material.css | 0 .../tools}/swagger/themes/theme-monokai.css | 0 .../tools}/swagger/themes/theme-muted.css | 0 .../tools}/swagger/themes/theme-newspaper.css | 0 .../tools}/swagger/themes/theme-outline.css | 0 src/main/resources/reference.conf | 52 +- .../proteus/test/controllers/Tests.java | 34 +- .../proteus/test/server/DefaultServer.java | 5 + .../test/server/TestControllerEndpoints.java | 8 + src/test/resources/application.conf | 39 +- src/test/resources/logback-test.xml | 36 + 32 files changed, 3073 insertions(+), 283 deletions(-) delete mode 100644 conf/logback-test.xml create mode 100644 src/main/java/io/sinistral/proteus/server/tools/oas/Reader.java create mode 100644 src/main/java/io/sinistral/proteus/server/tools/oas/ServerModelResolver.java create mode 100644 src/main/java/io/sinistral/proteus/server/tools/oas/ServerParameterExtension.java rename src/main/java/io/sinistral/proteus/server/{ => tools}/swagger/AnnotationHelper.java (98%) rename src/main/java/io/sinistral/proteus/server/{ => tools}/swagger/Reader.java (99%) rename src/main/java/io/sinistral/proteus/server/{ => tools}/swagger/ServerParameterExtension.java (97%) create mode 100644 src/main/java/io/sinistral/proteus/services/OpenAPIService.java rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/index.html (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools/swagger}/proteus-logo.svg (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/redoc.html (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/swagger-ui-bundle.js (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/swagger-ui-standalone-preset.js (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/swagger-ui.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/swagger-ui.js (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-feeling-blue.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-flattop.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-material.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-monokai.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-muted.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-newspaper.css (100%) rename src/main/resources/io/sinistral/proteus/{ => server/tools}/swagger/themes/theme-outline.css (100%) diff --git a/conf/logback-test.xml b/conf/logback-test.xml deleted file mode 100644 index 0be7a5f..0000000 --- a/conf/logback-test.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - true - - %date{ISO8601} %highlight(%-5level) [%boldCyan(%logger)] - [%boldYellow(%method %F) ] - %boldWhite(%message) %n %red(%ex) - - - - - - - - - - - - - - - - - - - io.sinistral.proteus.services - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/conf/logback.xml b/conf/logback.xml index e5e2656..7f7c34d 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -11,36 +11,32 @@ - - + - - + + + - + - - - - + - - + diff --git a/pom.xml b/pom.xml index 98f1584..3327465 100644 --- a/pom.xml +++ b/pom.xml @@ -33,9 +33,11 @@ 1.8 1.8 UTF-8 - 3.0.0 - 2.0.13.Final - 2.9.6 + 3.0.0 + 2.0.13.Final + 2.9.6 + 2.0.6-SNAPSHOT + 1.5.21 @@ -119,7 +121,6 @@ maven-surefire-plugin 2.20.1 - org.apache.maven.surefire @@ -188,7 +189,7 @@ io.rest-assured rest-assured - 3.0.2 + 3.1.1 test @@ -199,38 +200,38 @@ test - - io.undertow undertow-core - ${version.undertow} + ${undertow.version} - - + + + + ch.qos.logback + logback-classic + 1.2.3 + + + org.slf4j + slf4j-api + + + + + javax.ws.rs javax.ws.rs-api 2.1.1 - - com.fasterxml.woodstox - woodstox-core - 5.1.0 - - - - com.squareup - javapoet - 1.8.0 - com.google.inject guice 4.1.0 + net.openhft compiler @@ -243,6 +244,12 @@ + + com.squareup + javapoet + 1.8.0 + + com.google.guava guava @@ -254,90 +261,97 @@ config 1.3.1 + + + org.yaml + snakeyaml + 1.23 + + + + commons-io + commons-io + 2.6 + + + + org.apache.httpcomponents + httpcore + 4.4.6 + + + + org.slf4j + slf4j-api + 1.7.25 + + + org.slf4j + slf4j-ext + 1.7.25 + + + org.fusesource.jansi + jansi + 1.15 + + + + com.fasterxml.woodstox + woodstox-core + 5.1.0 + + com.fasterxml.jackson.core jackson-annotations - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.core jackson-core - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.dataformat jackson-dataformat-xml - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.dataformat jackson-dataformat-yaml - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.module jackson-module-afterburner - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jdk8 - ${version.jackson} + ${jackson.version} com.fasterxml.jackson.core jackson-databind - ${version.jackson} + ${jackson.version} - - - ch.qos.logback - logback-classic - 1.2.3 - - - org.slf4j - slf4j-api - - - - - - - + io.swagger swagger-annotations - 1.5.12 + ${swagger.version} io.swagger swagger-core - 1.5.12 - - - org.slf4j - slf4j-api - - - - - io.swagger - swagger-parser - 1.0.28 + ${swagger.version} org.slf4j @@ -348,34 +362,47 @@ io.swagger swagger-jaxrs - 1.5.12 - - - org.apache.httpcomponents - httpcore - 4.4.6 + ${swagger.version} + - org.slf4j - slf4j-api - 1.7.25 + io.swagger.core.v3 + swagger-annotations + ${openapi.version} + - org.slf4j - slf4j-ext - 1.7.25 + io.swagger.core.v3 + swagger-models + ${openapi.version} + - org.fusesource.jansi - jansi - 1.15 + io.swagger.core.v3 + swagger-jaxrs2 + ${openapi.version} + - org.reactivestreams - reactive-streams - 1.0.0.final + io.swagger.core.v3 + swagger-integration + ${openapi.version} + + + + + sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + true + + + false + + + ossrh @@ -385,6 +412,7 @@ ossrh https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://oss.sonatype.org/content/groups/public/io/sinistral/proteus-core diff --git a/src/main/java/io/sinistral/proteus/ProteusApplication.java b/src/main/java/io/sinistral/proteus/ProteusApplication.java index 5ff2c6d..43b4413 100644 --- a/src/main/java/io/sinistral/proteus/ProteusApplication.java +++ b/src/main/java/io/sinistral/proteus/ProteusApplication.java @@ -165,6 +165,7 @@ public void healthy() for(ListenerInfo info : undertow.getListenerInfo()) { + log.debug("listener info: " + info); SocketAddress address = info.getAddress(); if(address != null) diff --git a/src/main/java/io/sinistral/proteus/server/tools/oas/Reader.java b/src/main/java/io/sinistral/proteus/server/tools/oas/Reader.java new file mode 100644 index 0000000..35a4c65 --- /dev/null +++ b/src/main/java/io/sinistral/proteus/server/tools/oas/Reader.java @@ -0,0 +1,1829 @@ +/** + * + */ +package io.sinistral.proteus.server.tools.oas; + +/** + * @author jbauer + */ + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Application; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import io.sinistral.proteus.server.ServerResponse; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.util.AnnotationsUtils; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.ParameterProcessor; +import io.swagger.v3.core.util.PathUtils; +import io.swagger.v3.core.util.ReflectionUtils; +import io.swagger.v3.jaxrs2.OperationParser; +import io.swagger.v3.jaxrs2.ReaderListener; +import io.swagger.v3.jaxrs2.ResolvedParameter; +import io.swagger.v3.jaxrs2.SecurityParser; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtension; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtensions; +import io.swagger.v3.jaxrs2.util.ReaderUtils; +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.servers.Server; +import io.swagger.v3.oas.integration.ContextUtils; +import io.swagger.v3.oas.integration.SwaggerConfiguration; +import io.swagger.v3.oas.integration.api.OpenAPIConfiguration; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.callbacks.Callback; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.tags.Tag; +import io.undertow.server.HttpServerExchange; + +public class Reader extends io.swagger.v3.jaxrs2.Reader +{ + private static final Logger LOGGER = LoggerFactory.getLogger(Reader.class); + + public static final String DEFAULT_MEDIA_TYPE_VALUE = "*/*"; + public static final String DEFAULT_DESCRIPTION = "default response"; + + protected OpenAPIConfiguration config; + + private Application application; + private OpenAPI openAPI; + private Components components; + private Paths paths; + private Set openApiTags; + + private static final String GET_METHOD = "get"; + private static final String POST_METHOD = "post"; + private static final String PUT_METHOD = "put"; + private static final String DELETE_METHOD = "delete"; + private static final String PATCH_METHOD = "patch"; + private static final String TRACE_METHOD = "trace"; + private static final String HEAD_METHOD = "head"; + private static final String OPTIONS_METHOD = "options"; + + public Reader() + { + this.openAPI = new OpenAPI(); + paths = new Paths(); + openApiTags = new LinkedHashSet<>(); + components = new Components(); + + } + + public Reader(OpenAPI openAPI) + { + this(); + setConfiguration(new SwaggerConfiguration().openAPI(openAPI)); + } + + public Reader(OpenAPIConfiguration openApiConfiguration) + { + this(); + setConfiguration(openApiConfiguration); + } + + public OpenAPI getOpenAPI() + { + return openAPI; + } + + /** + * Scans a single class for Swagger annotations - does not invoke + * ReaderListeners + */ + public OpenAPI read(Class cls) + { + return read(cls, resolveApplicationPath(), null, false, null, null, new LinkedHashSet(), new ArrayList(), new HashSet>()); + } + + /** + * Scans a set of classes for both ReaderListeners and OpenAPI annotations. + * All found listeners will + * be instantiated before any of the classes are scanned for OpenAPI + * annotations - so they can be invoked + * accordingly. + * @param classes + * a set of classes to scan + * @return the generated OpenAPI definition + */ + public OpenAPI read(Set> classes) + { + Set> sortedClasses = new TreeSet<>(new Comparator>() + { + @Override + public int compare(Class class1, Class class2) + { + if (class1.equals(class2)) + { + return 0; + } + else if (class1.isAssignableFrom(class2)) + { + return -1; + } + else if (class2.isAssignableFrom(class1)) + { + return 1; + } + return class1.getName().compareTo(class2.getName()); + } + }); + sortedClasses.addAll(classes); + + Map, ReaderListener> listeners = new HashMap<>(); + + for (Class cls : sortedClasses) + { + if (ReaderListener.class.isAssignableFrom(cls) && !listeners.containsKey(cls)) + { + try + { + listeners.put(cls, (ReaderListener) cls.newInstance()); + } catch (Exception e) + { + LOGGER.error("Failed to create ReaderListener", e); + } + } + } + + for (ReaderListener listener : listeners.values()) + { + try + { + listener.beforeScan(this, openAPI); + } catch (Exception e) + { + LOGGER.error("Unexpected error invoking beforeScan listener [" + listener.getClass().getName() + "]", e); + } + } + + for (Class cls : sortedClasses) + { + read(cls, resolveApplicationPath(), null, false, null, null, new LinkedHashSet(), new ArrayList(), new HashSet>()); + } + + for (ReaderListener listener : listeners.values()) + { + try + { + listener.afterScan(this, openAPI); + } catch (Exception e) + { + LOGGER.error("Unexpected error invoking afterScan listener [" + listener.getClass().getName() + "]", e); + } + } + return openAPI; + } + + @Override + public void setConfiguration(OpenAPIConfiguration openApiConfiguration) + { + if (openApiConfiguration != null) + { + this.config = ContextUtils.deepCopy(openApiConfiguration); + if (openApiConfiguration.getOpenAPI() != null) + { + this.openAPI = this.config.getOpenAPI(); + if (this.openAPI.getComponents() != null) + { + this.components = this.openAPI.getComponents(); + } + } + } + } + + public OpenAPI read(Set> classes, Map resources) + { + return read(classes); + } + + protected String resolveApplicationPath() + { + if (application != null) + { + Class applicationToScan = this.application.getClass(); + ApplicationPath applicationPath; + // search up in the hierarchy until we find one with the annotation, + // this is needed because for example Weld proxies will not have the + // annotation and the right class will be the superClass + while ((applicationPath = applicationToScan.getAnnotation(ApplicationPath.class)) == null && !applicationToScan.getSuperclass().equals(Application.class)) + { + applicationToScan = applicationToScan.getSuperclass(); + } + + if (applicationPath != null) + { + if (StringUtils.isNotBlank(applicationPath.value())) + { + return applicationPath.value(); + } + } + // look for inner application, e.g. ResourceConfig + try + { + Application innerApp = application; + Method m = application.getClass().getMethod("getApplication", null); + while (m != null) + { + Application retrievedApp = (Application) m.invoke(innerApp, null); + if (retrievedApp == null) + { + break; + } + if (retrievedApp.getClass().equals(innerApp.getClass())) + { + break; + } + innerApp = retrievedApp; + applicationPath = innerApp.getClass().getAnnotation(ApplicationPath.class); + if (applicationPath != null) + { + if (StringUtils.isNotBlank(applicationPath.value())) + { + return applicationPath.value(); + } + } + m = innerApp.getClass().getMethod("getApplication", null); + } + } catch (NoSuchMethodException e) + { + // no inner application found + } catch (Exception e) + { + // no inner application found + } + } + return ""; + } + + public OpenAPI read(Class cls, + String parentPath, + String parentMethod, + boolean isSubresource, + RequestBody parentRequestBody, + ApiResponses parentResponses, + Set parentTags, + List parentParameters, + Set> scannedResources) + { + + Hidden hidden = cls.getAnnotation(Hidden.class); + // class path + final javax.ws.rs.Path apiPath = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Path.class); + + if (hidden != null) + { // || (apiPath == null && !isSubresource)) { + return openAPI; + } + + io.swagger.v3.oas.annotations.responses.ApiResponse[] classResponses = ReflectionUtils + .getRepeatableAnnotationsArray(cls, io.swagger.v3.oas.annotations.responses.ApiResponse.class); + + List apiSecurityScheme = ReflectionUtils + .getRepeatableAnnotations(cls, io.swagger.v3.oas.annotations.security.SecurityScheme.class); + List apiSecurityRequirements = ReflectionUtils + .getRepeatableAnnotations(cls, io.swagger.v3.oas.annotations.security.SecurityRequirement.class); + + ExternalDocumentation apiExternalDocs = ReflectionUtils.getAnnotation(cls, ExternalDocumentation.class); + io.swagger.v3.oas.annotations.tags.Tag[] apiTags = ReflectionUtils.getRepeatableAnnotationsArray(cls, io.swagger.v3.oas.annotations.tags.Tag.class); + io.swagger.v3.oas.annotations.servers.Server[] apiServers = ReflectionUtils.getRepeatableAnnotationsArray(cls, io.swagger.v3.oas.annotations.servers.Server.class); + + javax.ws.rs.Consumes classConsumes = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Consumes.class); + javax.ws.rs.Produces classProduces = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Produces.class); + + // OpenApiDefinition + OpenAPIDefinition openAPIDefinition = ReflectionUtils.getAnnotation(cls, OpenAPIDefinition.class); + + if (openAPIDefinition != null) + { + + // info + AnnotationsUtils.getInfo(openAPIDefinition.info()).ifPresent(info -> openAPI.setInfo(info)); + + // OpenApiDefinition security requirements + SecurityParser + .getSecurityRequirements(openAPIDefinition.security()) + .ifPresent(s -> openAPI.setSecurity(s)); + // + // OpenApiDefinition external docs + AnnotationsUtils + .getExternalDocumentation(openAPIDefinition.externalDocs()) + .ifPresent(docs -> openAPI.setExternalDocs(docs)); + + // OpenApiDefinition tags + AnnotationsUtils + .getTags(openAPIDefinition.tags(), false) + .ifPresent(tags -> openApiTags.addAll(tags)); + + // OpenApiDefinition servers + AnnotationsUtils.getServers(openAPIDefinition.servers()).ifPresent(servers -> openAPI.setServers(servers)); + + // OpenApiDefinition extensions + if (openAPIDefinition.extensions().length > 0) + { + openAPI.setExtensions(AnnotationsUtils + .getExtensions(openAPIDefinition.extensions())); + } + + } + + // class security schemes + if (apiSecurityScheme != null) + { + for (io.swagger.v3.oas.annotations.security.SecurityScheme securitySchemeAnnotation : apiSecurityScheme) + { + Optional securityScheme = SecurityParser.getSecurityScheme(securitySchemeAnnotation); + if (securityScheme.isPresent()) + { + Map securitySchemeMap = new HashMap<>(); + if (StringUtils.isNotBlank(securityScheme.get().key)) + { + securitySchemeMap.put(securityScheme.get().key, securityScheme.get().securityScheme); + if (components.getSecuritySchemes() != null && components.getSecuritySchemes().size() != 0) + { + components.getSecuritySchemes().putAll(securitySchemeMap); + } + else + { + components.setSecuritySchemes(securitySchemeMap); + } + } + } + } + } + + // class security requirements + List classSecurityRequirements = new ArrayList<>(); + if (apiSecurityRequirements != null) + { + Optional> requirementsObject = SecurityParser.getSecurityRequirements( + apiSecurityRequirements.toArray( + new io.swagger.v3.oas.annotations.security.SecurityRequirement[apiSecurityRequirements + .size()])); + if (requirementsObject.isPresent()) + { + classSecurityRequirements = requirementsObject.get(); + } + } + + // class tags, consider only name to add to class operations + final Set classTags = new LinkedHashSet<>(); + if (apiTags != null) + { + AnnotationsUtils + .getTags(apiTags, false).ifPresent(tags -> tags + .stream() + .map(t -> t.getName()) + .forEach(t -> classTags.add(t))); + } + + // parent tags + if (isSubresource) + { + if (parentTags != null) + { + classTags.addAll(parentTags); + } + } + + // servers + final List classServers = new ArrayList<>(); + if (apiServers != null) + { + AnnotationsUtils.getServers(apiServers).ifPresent(servers -> classServers.addAll(servers)); + } + + // class external docs + Optional classExternalDocumentation = AnnotationsUtils.getExternalDocumentation(apiExternalDocs); + + JavaType classType = TypeFactory.defaultInstance().constructType(cls); + BeanDescription bd = Json.mapper().getSerializationConfig().introspect(classType); + + final List globalParameters = new ArrayList<>(); + + // look for constructor-level annotated properties + globalParameters.addAll(ReaderUtils.collectConstructorParameters(cls, components, classConsumes, null)); + + // look for field-level annotated properties + globalParameters.addAll(ReaderUtils.collectFieldParameters(cls, components, classConsumes, null)); + + // iterate class methods + Method methods[] = cls.getMethods(); + for (Method method : methods) + { + if (isOperationHidden(method)) + { + continue; + } + AnnotatedMethod annotatedMethod = bd.findMethod(method.getName(), method.getParameterTypes()); + javax.ws.rs.Produces methodProduces = ReflectionUtils.getAnnotation(method, javax.ws.rs.Produces.class); + javax.ws.rs.Consumes methodConsumes = ReflectionUtils.getAnnotation(method, javax.ws.rs.Consumes.class); + + if (ReflectionUtils.isOverriddenMethod(method, cls)) + { + continue; + } + + javax.ws.rs.Path methodPath = ReflectionUtils.getAnnotation(method, javax.ws.rs.Path.class); + + String operationPath = ReaderUtils.getPath(apiPath, methodPath, parentPath, isSubresource); + + // skip if path is the same as parent, e.g. for @ApplicationPath + // annotated application + // extending resource config. + if (ignoreOperationPath(operationPath, parentPath) && !isSubresource) + { + continue; + } + + Map regexMap = new LinkedHashMap<>(); + operationPath = PathUtils.parsePath(operationPath, regexMap); + if (operationPath != null) + { + if (config != null && ReaderUtils.isIgnored(operationPath, config)) + { + continue; + } + + final Class subResource = getSubResourceWithJaxRsSubresourceLocatorSpecs(method); + + String httpMethod = ReaderUtils.extractOperationMethod(method, OpenAPIExtensions.chain()); + httpMethod = (httpMethod == null && isSubresource) ? parentMethod : httpMethod; + + if (StringUtils.isBlank(httpMethod) && subResource == null) + { + continue; + } + else if (StringUtils.isBlank(httpMethod) && subResource != null) + { + Type returnType = method.getGenericReturnType(); + if (shouldIgnoreClass(returnType.getTypeName()) && !returnType.equals(subResource)) + { + continue; + } + } + + io.swagger.v3.oas.annotations.Operation apiOperation = ReflectionUtils.getAnnotation(method, io.swagger.v3.oas.annotations.Operation.class); + JsonView jsonViewAnnotation = ReflectionUtils.getAnnotation(method, JsonView.class); + if (apiOperation != null && apiOperation.ignoreJsonView()) + { + jsonViewAnnotation = null; + } + + Operation operation = parseMethod( + method, + globalParameters, + methodProduces, + classProduces, + methodConsumes, + classConsumes, + classSecurityRequirements, + classExternalDocumentation, + classTags, + classServers, + isSubresource, + parentRequestBody, + parentResponses, + jsonViewAnnotation, + classResponses); + if (operation != null) + { + + List operationParameters = new ArrayList<>(); + List formParameters = new ArrayList<>(); + Annotation[][] paramAnnotations = getParameterAnnotations(method); + if (annotatedMethod == null) + { // annotatedMethod not null only when method with 0-2 + // parameters + Type[] genericParameterTypes = method.getGenericParameterTypes(); + for (int i = 0; i < genericParameterTypes.length; i++) + { + final Type type = TypeFactory.defaultInstance().constructType(genericParameterTypes[i], cls); + io.swagger.v3.oas.annotations.Parameter paramAnnotation = AnnotationsUtils + .getAnnotation(io.swagger.v3.oas.annotations.Parameter.class, paramAnnotations[i]); + Type paramType = ParameterProcessor.getParameterType(paramAnnotation, true); + if (paramType == null) + { + paramType = type; + } + else + { + if (!(paramType instanceof Class)) + { + paramType = type; + } + } + ResolvedParameter resolvedParameter = getParameters( + paramType, Arrays.asList(paramAnnotations[i]), operation, classConsumes, methodConsumes, + jsonViewAnnotation); + for (Parameter p : resolvedParameter.parameters) + { + operationParameters.add(p); + } + if (resolvedParameter.requestBody != null) + { + processRequestBody( + resolvedParameter.requestBody, + operation, + methodConsumes, + classConsumes, + operationParameters, + paramAnnotations[i], + type, + jsonViewAnnotation); + } + else if (resolvedParameter.formParameter != null) + { + // collect params to use together as request + // Body + formParameters.add(resolvedParameter.formParameter); + } + } + } + else + { + for (int i = 0; i < annotatedMethod.getParameterCount(); i++) + { + AnnotatedParameter param = annotatedMethod.getParameter(i); + final Type type = TypeFactory.defaultInstance().constructType(param.getParameterType(), cls); + io.swagger.v3.oas.annotations.Parameter paramAnnotation = AnnotationsUtils + .getAnnotation(io.swagger.v3.oas.annotations.Parameter.class, paramAnnotations[i]); + Type paramType = ParameterProcessor.getParameterType(paramAnnotation, true); + if (paramType == null) + { + paramType = type; + } + else + { + if (!(paramType instanceof Class)) + { + paramType = type; + } + } + ResolvedParameter resolvedParameter = getParameters( + paramType, Arrays.asList(paramAnnotations[i]), operation, classConsumes, methodConsumes, + jsonViewAnnotation); + for (Parameter p : resolvedParameter.parameters) + { + operationParameters.add(p); + } + if (resolvedParameter.requestBody != null) + { + processRequestBody( + resolvedParameter.requestBody, + operation, + methodConsumes, + classConsumes, + operationParameters, + paramAnnotations[i], + type, + jsonViewAnnotation); + } + else if (resolvedParameter.formParameter != null) + { + // collect params to use together as request + // Body + formParameters.add(resolvedParameter.formParameter); + } + } + } + // if we have form parameters, need to merge them into + // single schema and use as request body.. + if (formParameters.size() > 0) + { + Schema mergedSchema = new ObjectSchema(); + for (Parameter formParam : formParameters) + { + mergedSchema.addProperties(formParam.getName(), formParam.getSchema()); + } + Parameter merged = new Parameter().schema(mergedSchema); + processRequestBody( + merged, + operation, + methodConsumes, + classConsumes, + operationParameters, + new Annotation[0], + null, + jsonViewAnnotation); + + } + if (operationParameters.size() > 0) + { + for (Parameter operationParameter : operationParameters) + { + operation.addParametersItem(operationParameter); + } + } + + // if subresource, merge parent parameters + if (parentParameters != null) + { + for (Parameter parentParameter : parentParameters) + { + operation.addParametersItem(parentParameter); + } + } + + if (subResource != null && !scannedResources.contains(subResource)) + { + scannedResources.add(subResource); + read( + subResource, operationPath, httpMethod, true, operation.getRequestBody(), operation.getResponses(), classTags, operation.getParameters(), + scannedResources); + // remove the sub resource so that it can visit it later + // in another path + // but we have a room for optimization in the future to + // reuse the scanned result + // by caching the scanned resources in the reader + // instance to avoid actual scanning + // the the resources again + scannedResources.remove(subResource); + // don't proceed with root resource operation, as it's + // handled by subresource + continue; + } + + final Iterator chain = OpenAPIExtensions.chain(); + if (chain.hasNext()) + { + final OpenAPIExtension extension = chain.next(); + extension.decorateOperation(operation, method, chain); + } + + PathItem pathItemObject; + if (openAPI.getPaths() != null && openAPI.getPaths().get(operationPath) != null) + { + pathItemObject = openAPI.getPaths().get(operationPath); + } + else + { + pathItemObject = new PathItem(); + } + + if (StringUtils.isBlank(httpMethod)) + { + continue; + } + setPathItemOperation(pathItemObject, httpMethod, operation); + + paths.addPathItem(operationPath, pathItemObject); + if (openAPI.getPaths() != null) + { + this.paths.putAll(openAPI.getPaths()); + } + + openAPI.setPaths(this.paths); + + } + } + } + + // if no components object is defined in openApi instance passed by + // client, set openAPI.components to resolved components (if not empty) + if (!isEmptyComponents(components) && openAPI.getComponents() == null) + { + openAPI.setComponents(components); + } + + // add tags from class to definition tags + AnnotationsUtils + .getTags(apiTags, true).ifPresent(tags -> openApiTags.addAll(tags)); + + if (!openApiTags.isEmpty()) + { + Set tagsSet = new LinkedHashSet<>(); + if (openAPI.getTags() != null) + { + for (Tag tag : openAPI.getTags()) + { + if (tagsSet.stream().noneMatch(t -> t.getName().equals(tag.getName()))) + { + tagsSet.add(tag); + } + } + } + for (Tag tag : openApiTags) + { + if (tagsSet.stream().noneMatch(t -> t.getName().equals(tag.getName()))) + { + tagsSet.add(tag); + } + } + openAPI.setTags(new ArrayList<>(tagsSet)); + } + + return openAPI; + } + + public static Annotation[][] getParameterAnnotations(Method method) + { + + Annotation[][] methodAnnotations = method.getParameterAnnotations(); + + java.lang.reflect.Parameter[] params = method.getParameters(); + + for (int i = 0; i < params.length; i++) + { + Annotation[] paramAnnotations = methodAnnotations[i]; + + if (!params[i].getType().isAssignableFrom(io.sinistral.proteus.server.ServerRequest.class) && !params[i].getType().getName().startsWith("io.undertow")) + { + String annotationStrings = Arrays.stream(paramAnnotations).map(a -> a.annotationType().getName()).collect(Collectors.joining(" ")); + + LOGGER.debug("\nparameter: " + params[i] + " | name: " + params[i].getName() + " type: " + params[i].getType() + " -> " + annotationStrings); + + if (paramAnnotations.length == 0) + { + final String parameterName = params[i].getName(); + + LOGGER.debug("creating query parameter for " + parameterName); + + QueryParam queryParam = new QueryParam() + { + + @Override + public String value() + { + return parameterName; + } + + @Override + public Class annotationType() + { + return QueryParam.class; + } + }; + + methodAnnotations[i] = new Annotation[] { queryParam }; + } + } + + } + + Method overriddenmethod = ReflectionUtils.getOverriddenMethod(method); + + if (overriddenmethod != null) + { + Annotation[][] overriddenAnnotations = overriddenmethod + .getParameterAnnotations(); + + for (int i = 0; i < methodAnnotations.length; i++) + { + List types = new ArrayList<>(); + for (int j = 0; j < methodAnnotations[i].length; j++) + { + types.add(methodAnnotations[i][j].annotationType()); + } + for (int j = 0; j < overriddenAnnotations[i].length; j++) + { + if (!types.contains(overriddenAnnotations[i][j] + .annotationType())) + { + methodAnnotations[i] = ArrayUtils.add( + methodAnnotations[i], + overriddenAnnotations[i][j]); + } + } + + } + } + return methodAnnotations; + } + + protected Content processContent(Content content, Schema schema, Consumes methodConsumes, Consumes classConsumes) + { + if (content == null) + { + content = new Content(); + } + if (methodConsumes != null) + { + for (String value : methodConsumes.value()) + { + setMediaTypeToContent(schema, content, value); + } + } + else if (classConsumes != null) + { + for (String value : classConsumes.value()) + { + setMediaTypeToContent(schema, content, value); + } + } + else + { + setMediaTypeToContent(schema, content, DEFAULT_MEDIA_TYPE_VALUE); + } + return content; + } + + protected void processRequestBody( Parameter requestBodyParameter, Operation operation, + Consumes methodConsumes, Consumes classConsumes, + List operationParameters, + Annotation[] paramAnnotations, Type type, + JsonView jsonViewAnnotation) + { + + io.swagger.v3.oas.annotations.parameters.RequestBody requestBodyAnnotation = getRequestBody(Arrays.asList(paramAnnotations)); + if (requestBodyAnnotation != null) + { + Optional optionalRequestBody = OperationParser.getRequestBody(requestBodyAnnotation, classConsumes, methodConsumes, components, jsonViewAnnotation); + if (optionalRequestBody.isPresent()) + { + RequestBody requestBody = optionalRequestBody.get(); + if (StringUtils.isBlank(requestBody.get$ref()) && + (requestBody.getContent() == null || requestBody.getContent().isEmpty())) + { + if (requestBodyParameter.getSchema() != null) + { + Content content = processContent(requestBody.getContent(), requestBodyParameter.getSchema(), methodConsumes, classConsumes); + requestBody.setContent(content); + } + } + else if (StringUtils.isBlank(requestBody.get$ref()) && + requestBody.getContent() != null && + !requestBody.getContent().isEmpty()) + { + if (requestBodyParameter.getSchema() != null) + { + for (MediaType mediaType : requestBody.getContent().values()) + { + if (mediaType.getSchema() == null) + { + if (requestBodyParameter.getSchema() == null) + { + mediaType.setSchema(new Schema()); + } + else + { + mediaType.setSchema(requestBodyParameter.getSchema()); + } + } + if (StringUtils.isBlank(mediaType.getSchema().getType())) + { + mediaType.getSchema().setType(requestBodyParameter.getSchema().getType()); + } + } + } + } + operation.setRequestBody(requestBody); + } + } + else + { + if (operation.getRequestBody() == null) + { + boolean isRequestBodyEmpty = true; + RequestBody requestBody = new RequestBody(); + if (StringUtils.isNotBlank(requestBodyParameter.get$ref())) + { + requestBody.set$ref(requestBodyParameter.get$ref()); + isRequestBodyEmpty = false; + } + if (StringUtils.isNotBlank(requestBodyParameter.getDescription())) + { + requestBody.setDescription(requestBodyParameter.getDescription()); + isRequestBodyEmpty = false; + } + if (Boolean.TRUE.equals(requestBodyParameter.getRequired())) + { + requestBody.setRequired(requestBodyParameter.getRequired()); + isRequestBodyEmpty = false; + } + + if (requestBodyParameter.getSchema() != null) + { + Content content = processContent(null, requestBodyParameter.getSchema(), methodConsumes, classConsumes); + requestBody.setContent(content); + isRequestBodyEmpty = false; + } + if (!isRequestBodyEmpty) + { + // requestBody.setExtensions(extensions); + operation.setRequestBody(requestBody); + } + } + } + } + + private io.swagger.v3.oas.annotations.parameters.RequestBody getRequestBody(List annotations) + { + if (annotations == null) + { + return null; + } + for (Annotation a : annotations) + { + if (a instanceof io.swagger.v3.oas.annotations.parameters.RequestBody) + { + return (io.swagger.v3.oas.annotations.parameters.RequestBody) a; + } + } + return null; + } + + private void setMediaTypeToContent(Schema schema, Content content, String value) + { + MediaType mediaTypeObject = new MediaType(); + mediaTypeObject.setSchema(schema); + content.addMediaType(value, mediaTypeObject); + } + + public Operation parseMethod( + Method method, + List globalParameters, + JsonView jsonViewAnnotation) + { + JavaType classType = TypeFactory.defaultInstance().constructType(method.getDeclaringClass()); + return parseMethod( + classType.getClass(), + method, + globalParameters, + null, + null, + null, + null, + new ArrayList<>(), + Optional.empty(), + new HashSet<>(), + new ArrayList<>(), + false, + null, + null, + jsonViewAnnotation, + null); + } + + public Operation parseMethod( + Method method, + List globalParameters, + Produces methodProduces, + Produces classProduces, + Consumes methodConsumes, + Consumes classConsumes, + List classSecurityRequirements, + Optional classExternalDocs, + Set classTags, + List classServers, + boolean isSubresource, + RequestBody parentRequestBody, + ApiResponses parentResponses, + JsonView jsonViewAnnotation, + io.swagger.v3.oas.annotations.responses.ApiResponse[] classResponses) + { + JavaType classType = TypeFactory.defaultInstance().constructType(method.getDeclaringClass()); + return parseMethod( + classType.getClass(), + method, + globalParameters, + methodProduces, + classProduces, + methodConsumes, + classConsumes, + classSecurityRequirements, + classExternalDocs, + classTags, + classServers, + isSubresource, + parentRequestBody, + parentResponses, + jsonViewAnnotation, + classResponses); + } + + private Operation parseMethod( + Class cls, + Method method, + List globalParameters, + Produces methodProduces, + Produces classProduces, + Consumes methodConsumes, + Consumes classConsumes, + List classSecurityRequirements, + Optional classExternalDocs, + Set classTags, + List classServers, + boolean isSubresource, + RequestBody parentRequestBody, + ApiResponses parentResponses, + JsonView jsonViewAnnotation, + io.swagger.v3.oas.annotations.responses.ApiResponse[] classResponses) + { + + if (Arrays.stream(method.getParameters()).filter(p -> p.getType().isAssignableFrom(HttpServerExchange.class)).count() > 0L) + { + return null; + } + + Operation operation = new Operation(); + + io.swagger.v3.oas.annotations.Operation apiOperation = ReflectionUtils.getAnnotation(method, io.swagger.v3.oas.annotations.Operation.class); + + List apiSecurity = ReflectionUtils + .getRepeatableAnnotations(method, io.swagger.v3.oas.annotations.security.SecurityRequirement.class); + + List apiCallbacks = ReflectionUtils + .getRepeatableAnnotations(method, io.swagger.v3.oas.annotations.callbacks.Callback.class); + + List apiServers = ReflectionUtils.getRepeatableAnnotations(method, Server.class); + + List apiTags = ReflectionUtils.getRepeatableAnnotations(method, io.swagger.v3.oas.annotations.tags.Tag.class); + + List apiParameters = ReflectionUtils.getRepeatableAnnotations(method, io.swagger.v3.oas.annotations.Parameter.class); + + List apiResponses = ReflectionUtils + .getRepeatableAnnotations(method, io.swagger.v3.oas.annotations.responses.ApiResponse.class); + io.swagger.v3.oas.annotations.parameters.RequestBody apiRequestBody = ReflectionUtils.getAnnotation(method, io.swagger.v3.oas.annotations.parameters.RequestBody.class); + + ExternalDocumentation apiExternalDocumentation = ReflectionUtils.getAnnotation(method, ExternalDocumentation.class); + + // callbacks + Map callbacks = new LinkedHashMap<>(); + + if (apiCallbacks != null) + { + for (io.swagger.v3.oas.annotations.callbacks.Callback methodCallback : apiCallbacks) + { + Map currentCallbacks = getCallbacks(methodCallback, methodProduces, classProduces, methodConsumes, classConsumes, jsonViewAnnotation); + callbacks.putAll(currentCallbacks); + } + } + if (callbacks.size() > 0) + { + operation.setCallbacks(callbacks); + } + + // security + classSecurityRequirements.forEach(operation::addSecurityItem); + if (apiSecurity != null) + { + Optional> requirementsObject = SecurityParser.getSecurityRequirements(apiSecurity.toArray( + new io.swagger.v3.oas.annotations.security.SecurityRequirement[apiSecurity + .size()])); + if (requirementsObject.isPresent()) + { + requirementsObject.get().stream() + .filter(r -> operation.getSecurity() == null || !operation.getSecurity().contains(r)) + .forEach(operation::addSecurityItem); + } + } + + // servers + if (classServers != null) + { + classServers.forEach(operation::addServersItem); + } + + if (apiServers != null) + { + AnnotationsUtils.getServers(apiServers.toArray(new Server[apiServers.size()])).ifPresent(servers -> servers.forEach(operation::addServersItem)); + } + + // external docs + AnnotationsUtils.getExternalDocumentation(apiExternalDocumentation).ifPresent(operation::setExternalDocs); + + // method tags + if (apiTags != null) + { + apiTags.stream() + .filter(t -> operation.getTags() == null || (operation.getTags() != null && !operation.getTags().contains(t.name()))) + .map(t -> t.name()) + .forEach(operation::addTagsItem); + AnnotationsUtils.getTags(apiTags.toArray(new io.swagger.v3.oas.annotations.tags.Tag[apiTags.size()]), true).ifPresent(tags -> openApiTags.addAll(tags)); + } + + // parameters + if (globalParameters != null) + { + for (Parameter globalParameter : globalParameters) + { + operation.addParametersItem(globalParameter); + } + } + if (apiParameters != null) + { + + getParametersListFromAnnotation( + apiParameters.toArray(new io.swagger.v3.oas.annotations.Parameter[apiParameters.size()]), + classConsumes, + methodConsumes, + operation, + jsonViewAnnotation).ifPresent(p -> p.forEach(operation::addParametersItem)); + } + + // RequestBody in Method + if (apiRequestBody != null && operation.getRequestBody() == null) + { + OperationParser.getRequestBody(apiRequestBody, classConsumes, methodConsumes, components, jsonViewAnnotation).ifPresent( + operation::setRequestBody); + } + + // operation id + if (StringUtils.isBlank(operation.getOperationId())) + { + operation.setOperationId(getOperationId(method.getName())); + } + + // classResponses + if (classResponses != null && classResponses.length > 0) + { + OperationParser.getApiResponses( + classResponses, + classProduces, + methodProduces, + components, + jsonViewAnnotation) + .ifPresent(responses -> + { + if (operation.getResponses() == null) + { + operation.setResponses(responses); + } + else + { + responses.forEach(operation.getResponses()::addApiResponse); + } + }); + } + + if (apiOperation != null) + { + setOperationObjectFromApiOperationAnnotation(operation, apiOperation, methodProduces, classProduces, methodConsumes, classConsumes, jsonViewAnnotation); + } + + // apiResponses + if (apiResponses != null && apiResponses.size() > 0) + { + OperationParser.getApiResponses( + apiResponses.toArray(new io.swagger.v3.oas.annotations.responses.ApiResponse[apiResponses.size()]), + classProduces, + methodProduces, + components, + jsonViewAnnotation) + .ifPresent(responses -> + { + if (operation.getResponses() == null) + { + operation.setResponses(responses); + } + else + { + responses.forEach(operation.getResponses()::addApiResponse); + } + }); + } + + // class tags after tags defined as field of @Operation + if (classTags != null) + { + classTags.stream() + .filter(t -> operation.getTags() == null || (operation.getTags() != null && !operation.getTags().contains(t))) + .forEach(operation::addTagsItem); + } + + // external docs of class if not defined in annotation of method or as + // field of Operation annotation + if (operation.getExternalDocs() == null) + { + classExternalDocs.ifPresent(operation::setExternalDocs); + } + + // if subresource, merge parent requestBody + if (isSubresource && parentRequestBody != null) + { + if (operation.getRequestBody() == null) + { + operation.requestBody(parentRequestBody); + } + else + { + Content content = operation.getRequestBody().getContent(); + if (content == null) + { + content = parentRequestBody.getContent(); + operation.getRequestBody().setContent(content); + } + else if (parentRequestBody.getContent() != null) + { + for (String parentMediaType : parentRequestBody.getContent().keySet()) + { + if (content.get(parentMediaType) == null) + { + content.addMediaType(parentMediaType, parentRequestBody.getContent().get(parentMediaType)); + } + } + } + } + } + + // handle return type, add as response in case. + Type returnType = method.getGenericReturnType(); + final Class subResource = getSubResourceWithJaxRsSubresourceLocatorSpecs(method); + + if (!shouldIgnoreClass(returnType.getTypeName()) && !returnType.equals(subResource)) + { + LOGGER.debug("processing class " + returnType + " " + returnType.getTypeName()); + + JavaType classType = TypeFactory.defaultInstance().constructType(returnType); + + if (classType != null && classType.getRawClass() != null) + { + if (classType.getRawClass().isAssignableFrom(ServerResponse.class)) + { + if(classType.containedType(0) != null) + { + returnType = classType.containedType(0); + } + } + else if (classType.getRawClass().isAssignableFrom(CompletableFuture.class)) + { + Class futureCls = classType.containedType(0).getRawClass(); + + if (futureCls.isAssignableFrom(ServerResponse.class)) + { + final JavaType futureType = TypeFactory.defaultInstance().constructType(classType.containedType(0)); + returnType = futureType.containedType(0); + } + else + { + returnType = classType.containedType(0); + } + } + } + + ResolvedSchema resolvedSchema = ModelConverters.getInstance() + .resolveAsResolvedSchema(new AnnotatedType(returnType).resolveAsRef(true).jsonViewAnnotation(jsonViewAnnotation)); + + if (resolvedSchema.schema != null) + { + Schema returnTypeSchema = resolvedSchema.schema; + Content content = new Content(); + MediaType mediaType = new MediaType().schema(returnTypeSchema); + AnnotationsUtils.applyTypes(classProduces == null ? new String[0] : classProduces.value(), + methodProduces == null ? new String[0] : methodProduces.value(), content, mediaType); + if (operation.getResponses() == null) + { + operation.responses( + new ApiResponses()._default( + new ApiResponse().description(DEFAULT_DESCRIPTION) + .content(content))); + } + if (operation.getResponses().getDefault() != null && + StringUtils.isBlank(operation.getResponses().getDefault().get$ref())) + { + if (operation.getResponses().getDefault().getContent() == null) + { + operation.getResponses().getDefault().content(content); + } + else + { + for (String key : operation.getResponses().getDefault().getContent().keySet()) + { + if (operation.getResponses().getDefault().getContent().get(key).getSchema() == null) + { + operation.getResponses().getDefault().getContent().get(key).setSchema(returnTypeSchema); + } + } + } + } + Map schemaMap = resolvedSchema.referencedSchemas; + if (schemaMap != null) + { + schemaMap.forEach((key, schema) -> components.addSchemas(key, schema)); + } + + } + } + if (operation.getResponses() == null || operation.getResponses().isEmpty()) + { + Content content = new Content(); + MediaType mediaType = new MediaType(); + AnnotationsUtils.applyTypes(classProduces == null ? new String[0] : classProduces.value(), + methodProduces == null ? new String[0] : methodProduces.value(), content, mediaType); + + ApiResponse apiResponseObject = new ApiResponse().description(DEFAULT_DESCRIPTION).content(content); + operation.setResponses(new ApiResponses()._default(apiResponseObject)); + } + + return operation; + } + + private boolean shouldIgnoreClass(String className) + { + if (StringUtils.isBlank(className)) + { + return true; + } + boolean ignore = false; + ignore = ignore || className.startsWith("javax.ws.rs."); + ignore = ignore || className.equalsIgnoreCase("void"); + ignore = ignore || className.startsWith("io.undertow"); + ignore = ignore || className.startsWith("java.lang.Void"); + return ignore; + } + + private Map getCallbacks( + io.swagger.v3.oas.annotations.callbacks.Callback apiCallback, + Produces methodProduces, + Produces classProduces, + Consumes methodConsumes, + Consumes classConsumes, + JsonView jsonViewAnnotation) + { + Map callbackMap = new HashMap<>(); + if (apiCallback == null) + { + return callbackMap; + } + + Callback callbackObject = new Callback(); + if (StringUtils.isNotBlank(apiCallback.ref())) + { + callbackObject.set$ref(apiCallback.ref()); + callbackMap.put(apiCallback.name(), callbackObject); + return callbackMap; + } + PathItem pathItemObject = new PathItem(); + for (io.swagger.v3.oas.annotations.Operation callbackOperation : apiCallback.operation()) + { + Operation callbackNewOperation = new Operation(); + setOperationObjectFromApiOperationAnnotation( + callbackNewOperation, + callbackOperation, + methodProduces, + classProduces, + methodConsumes, + classConsumes, + jsonViewAnnotation); + setPathItemOperation(pathItemObject, callbackOperation.method(), callbackNewOperation); + } + + callbackObject.addPathItem(apiCallback.callbackUrlExpression(), pathItemObject); + callbackMap.put(apiCallback.name(), callbackObject); + + return callbackMap; + } + + private void setPathItemOperation(PathItem pathItemObject, String method, Operation operation) + { + switch (method) + { + case POST_METHOD: + pathItemObject.post(operation); + break; + case GET_METHOD: + pathItemObject.get(operation); + break; + case DELETE_METHOD: + pathItemObject.delete(operation); + break; + case PUT_METHOD: + pathItemObject.put(operation); + break; + case PATCH_METHOD: + pathItemObject.patch(operation); + break; + case TRACE_METHOD: + pathItemObject.trace(operation); + break; + case HEAD_METHOD: + pathItemObject.head(operation); + break; + case OPTIONS_METHOD: + pathItemObject.options(operation); + break; + default: + // Do nothing here + break; + } + } + + private void setOperationObjectFromApiOperationAnnotation( + Operation operation, + io.swagger.v3.oas.annotations.Operation apiOperation, + Produces methodProduces, + Produces classProduces, + Consumes methodConsumes, + Consumes classConsumes, + JsonView jsonViewAnnotation) + { + if (StringUtils.isNotBlank(apiOperation.summary())) + { + operation.setSummary(apiOperation.summary()); + } + if (StringUtils.isNotBlank(apiOperation.description())) + { + operation.setDescription(apiOperation.description()); + } + if (StringUtils.isNotBlank(apiOperation.operationId())) + { + operation.setOperationId(getOperationId(apiOperation.operationId())); + } + if (apiOperation.deprecated()) + { + operation.setDeprecated(apiOperation.deprecated()); + } + + ReaderUtils.getStringListFromStringArray(apiOperation.tags()).ifPresent(tags -> + { + tags.stream() + .filter(t -> operation.getTags() == null || (operation.getTags() != null && !operation.getTags().contains(t))) + .forEach(operation::addTagsItem); + }); + + if (operation.getExternalDocs() == null) + { // if not set in root annotation + AnnotationsUtils.getExternalDocumentation(apiOperation.externalDocs()).ifPresent(operation::setExternalDocs); + } + + OperationParser.getApiResponses(apiOperation.responses(), classProduces, methodProduces, components, jsonViewAnnotation).ifPresent(responses -> + { + if (operation.getResponses() == null) + { + operation.setResponses(responses); + } + else + { + responses.forEach(operation.getResponses()::addApiResponse); + } + }); + AnnotationsUtils.getServers(apiOperation.servers()).ifPresent(servers -> servers.forEach(operation::addServersItem)); + + getParametersListFromAnnotation( + apiOperation.parameters(), + classConsumes, + methodConsumes, + operation, + jsonViewAnnotation).ifPresent(p -> p.forEach(operation::addParametersItem)); + + // security + Optional> requirementsObject = SecurityParser.getSecurityRequirements(apiOperation.security()); + if (requirementsObject.isPresent()) + { + requirementsObject.get().stream() + .filter(r -> operation.getSecurity() == null || !operation.getSecurity().contains(r)) + .forEach(operation::addSecurityItem); + } + + // RequestBody in Operation + if (apiOperation != null && apiOperation.requestBody() != null && operation.getRequestBody() == null) + { + OperationParser.getRequestBody(apiOperation.requestBody(), classConsumes, methodConsumes, components, jsonViewAnnotation).ifPresent( + requestBodyObject -> operation + .setRequestBody( + requestBodyObject)); + } + + // Extensions in Operation + if (apiOperation.extensions().length > 0) + { + Map extensions = AnnotationsUtils.getExtensions(apiOperation.extensions()); + if (extensions != null) + { + for (String ext : extensions.keySet()) + { + operation.addExtension(ext, extensions.get(ext)); + } + } + } + } + + protected String getOperationId(String operationId) + { + boolean operationIdUsed = existOperationId(operationId); + String operationIdToFind = null; + int counter = 0; + while (operationIdUsed) + { + operationIdToFind = String.format("%s_%d", operationId, ++counter); + operationIdUsed = existOperationId(operationIdToFind); + } + if (operationIdToFind != null) + { + operationId = operationIdToFind; + } + return operationId; + } + + private boolean existOperationId(String operationId) + { + if (openAPI == null) + { + return false; + } + if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) + { + return false; + } + for (PathItem path : openAPI.getPaths().values()) + { + String pathOperationId = extractOperationIdFromPathItem(path); + if (operationId.equalsIgnoreCase(pathOperationId)) + { + return true; + } + + } + return false; + } + + protected Optional> getParametersListFromAnnotation(io.swagger.v3.oas.annotations.Parameter[] parameters, Consumes classConsumes, Consumes methodConsumes, + Operation operation, JsonView jsonViewAnnotation) + { + if (parameters == null) + { + return Optional.empty(); + } + List parametersObject = new ArrayList<>(); + for (io.swagger.v3.oas.annotations.Parameter parameter : parameters) + { + + ResolvedParameter resolvedParameter = getParameters( + ParameterProcessor.getParameterType(parameter), Collections.singletonList(parameter), operation, classConsumes, + methodConsumes, jsonViewAnnotation); + parametersObject.addAll(resolvedParameter.parameters); + } + if (parametersObject.size() == 0) + { + return Optional.empty(); + } + return Optional.of(parametersObject); + } + + protected ResolvedParameter getParameters( Type type, List annotations, Operation operation, javax.ws.rs.Consumes classConsumes, + javax.ws.rs.Consumes methodConsumes, JsonView jsonViewAnnotation) + { + + final Iterator chain = OpenAPIExtensions.chain(); + if (!chain.hasNext()) + { + return new ResolvedParameter(); + } + LOGGER.debug("getParameters for {}", type); + Set typesToSkip = new HashSet<>(); + final OpenAPIExtension extension = chain.next(); + LOGGER.debug("trying extension {}", extension); + + final ResolvedParameter extractParametersResult = extension + .extractParameters(annotations, type, typesToSkip, components, classConsumes, methodConsumes, true, jsonViewAnnotation, chain); + return extractParametersResult; + } + + private String extractOperationIdFromPathItem(PathItem path) + { + if (path.getGet() != null) + { + return path.getGet().getOperationId(); + } + else if (path.getPost() != null) + { + return path.getPost().getOperationId(); + } + else if (path.getPut() != null) + { + return path.getPut().getOperationId(); + } + else if (path.getDelete() != null) + { + return path.getDelete().getOperationId(); + } + else if (path.getOptions() != null) + { + return path.getOptions().getOperationId(); + } + else if (path.getHead() != null) + { + return path.getHead().getOperationId(); + } + else if (path.getPatch() != null) + { + return path.getPatch().getOperationId(); + } + return ""; + } + + private boolean isEmptyComponents(Components components) + { + if (components == null) + { + return true; + } + if (components.getSchemas() != null && components.getSchemas().size() > 0) + { + return false; + } + if (components.getSecuritySchemes() != null && components.getSecuritySchemes().size() > 0) + { + return false; + } + if (components.getCallbacks() != null && components.getCallbacks().size() > 0) + { + return false; + } + if (components.getExamples() != null && components.getExamples().size() > 0) + { + return false; + } + if (components.getExtensions() != null && components.getExtensions().size() > 0) + { + return false; + } + if (components.getHeaders() != null && components.getHeaders().size() > 0) + { + return false; + } + if (components.getLinks() != null && components.getLinks().size() > 0) + { + return false; + } + if (components.getParameters() != null && components.getParameters().size() > 0) + { + return false; + } + if (components.getRequestBodies() != null && components.getRequestBodies().size() > 0) + { + return false; + } + if (components.getResponses() != null && components.getResponses().size() > 0) + { + return false; + } + + return true; + } + + protected boolean isOperationHidden(Method method) + { + io.swagger.v3.oas.annotations.Operation apiOperation = ReflectionUtils.getAnnotation(method, io.swagger.v3.oas.annotations.Operation.class); + if (apiOperation != null && apiOperation.hidden()) + { + return true; + } + Hidden hidden = method.getAnnotation(Hidden.class); + if (hidden != null) + { + return true; + } + if (config != null && !Boolean.TRUE.equals(config.isReadAllResources()) && apiOperation == null) + { + return true; + } + return false; + } + + public void setApplication(Application application) + { + this.application = application; + } + + protected boolean ignoreOperationPath(String path, String parentPath) + { + + if (StringUtils.isBlank(path) && StringUtils.isBlank(parentPath)) + { + return true; + } + else if (StringUtils.isNotBlank(path) && StringUtils.isBlank(parentPath)) + { + return false; + } + else if (StringUtils.isBlank(path) && StringUtils.isNotBlank(parentPath)) + { + return false; + } + if (parentPath != null && !"".equals(parentPath) && !"/".equals(parentPath)) + { + if (!parentPath.startsWith("/")) + { + parentPath = "/" + parentPath; + } + if (parentPath.endsWith("/")) + { + parentPath = parentPath.substring(0, parentPath.length() - 1); + } + } + if (path != null && !"".equals(path) && !"/".equals(path)) + { + if (!path.startsWith("/")) + { + path = "/" + path; + } + if (path.endsWith("/")) + { + path = path.substring(0, path.length() - 1); + } + } + if (path.equals(parentPath)) + { + return true; + } + return false; + } + + protected Class getSubResourceWithJaxRsSubresourceLocatorSpecs(Method method) + { + final Class rawType = method.getReturnType(); + final Class type; + if (Class.class.equals(rawType)) + { + type = getClassArgument(method.getGenericReturnType()); + if (type == null) + { + return null; + } + } + else + { + type = rawType; + } + + if (method.getAnnotation(javax.ws.rs.Path.class) != null) + { + if (ReaderUtils.extractOperationMethod(method, null) == null) + { + return type; + } + } + return null; + } + + private static Class getClassArgument(Type cls) + { + if (cls instanceof ParameterizedType) + { + final ParameterizedType parameterized = (ParameterizedType) cls; + final Type[] args = parameterized.getActualTypeArguments(); + if (args.length != 1) + { + LOGGER.error("Unexpected class definition: {}", cls); + return null; + } + final Type first = args[0]; + if (first instanceof Class) + { + return (Class) first; + } + else + { + return null; + } + } + else + { + LOGGER.error("Unknown class definition: {}", cls); + return null; + } + } +} diff --git a/src/main/java/io/sinistral/proteus/server/tools/oas/ServerModelResolver.java b/src/main/java/io/sinistral/proteus/server/tools/oas/ServerModelResolver.java new file mode 100644 index 0000000..806a5a8 --- /dev/null +++ b/src/main/java/io/sinistral/proteus/server/tools/oas/ServerModelResolver.java @@ -0,0 +1,166 @@ +/** + * + */ +package io.sinistral.proteus.server.tools.oas; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import io.sinistral.proteus.server.ServerResponse; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverter; +import io.swagger.v3.core.converter.ModelConverterContext; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.media.Schema; + +/** + * @author jbauer + */ +public class ServerModelResolver extends io.swagger.v3.core.jackson.ModelResolver +{ + + private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ServerModelResolver.class.getCanonicalName()); + + + public ServerModelResolver() + { + super(io.swagger.v3.core.util.Json.mapper()); + } + + /** + * @param mapper + */ + public ServerModelResolver(ObjectMapper mapper) + { + super(mapper); + + } + + /* + * (non-Javadoc) + * @see io.swagger.v3.core.jackson.ModelResolver#resolve(io.swagger.v3.core. + * converter.AnnotatedType, + * io.swagger.v3.core.converter.ModelConverterContext, java.util.Iterator) + */ + @Override + public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context, Iterator next) + { + + JavaType classType = TypeFactory.defaultInstance().constructType(annotatedType.getType()); + + Class rawClass = classType.getRawClass(); + + JavaType resolvedType = classType; + + if (rawClass != null && !resolvedType.isPrimitive()) + { + if (rawClass.isAssignableFrom(ServerResponse.class)) + { + resolvedType = classType.containedType(0); + } + else if (rawClass.isAssignableFrom(CompletableFuture.class)) + { + Class futureCls = classType.containedType(0).getRawClass(); + + if (futureCls.isAssignableFrom(ServerResponse.class)) + { + System.out.println("class is assignable from ServerResponse"); + final JavaType futureType = TypeFactory.defaultInstance().constructType(classType.containedType(0)); + resolvedType = futureType.containedType(0); + } + else + { + System.out.println("class is NOT assignable from ServerResponse"); + resolvedType = classType.containedType(0); + } + } + + if(resolvedType != null) + { + if (resolvedType.getTypeName().contains("ByteBuffer")) + { + resolvedType = TypeFactory.defaultInstance().constructFromCanonical(java.io.File.class.getName()); + } + + if (resolvedType.getTypeName().contains("java.nio.file.Path")) + { + resolvedType = TypeFactory.defaultInstance().constructFromCanonical(java.io.File.class.getName()); + } + +// if (resolvedType.getTypeName().contains("java.lang.Void")) +// { +// resolvedType = TypeFactory.defaultInstance().constructFromCanonical(java.lang.Void.class.getName()); +// } + + annotatedType.setType(resolvedType); + + System.out.println("resolvedType out " + resolvedType); + } + + } + + try + { + log.info("Processing " + annotatedType + " " + classType + " " + annotatedType.getName()); + + return super.resolve(annotatedType, context, next); + + } catch (Exception e) + { + log.error("Error processing " + annotatedType + " " + classType + " " + annotatedType.getName(), e); + return null; + } + + } + + /* + * (non-Javadoc) + * @see + * io.swagger.v3.core.jackson.ModelResolver#resolveRequiredProperties(com. + * fasterxml.jackson.databind.introspect.Annotated, + * java.lang.annotation.Annotation[], + * io.swagger.v3.oas.annotations.media.Schema) + */ + @Override + protected List resolveRequiredProperties(Annotated a, Annotation[] annotations, io.swagger.v3.oas.annotations.media.Schema schema) + { + // TODO Auto-generated method stub + return super.resolveRequiredProperties(a, annotations, schema); + } + + /* + * (non-Javadoc) + * @see + * io.swagger.v3.core.jackson.ModelResolver#shouldIgnoreClass(java.lang. + * reflect.Type) + */ + @Override + protected boolean shouldIgnoreClass(Type type) + { + //System.out.println("should ignore " + type); + + JavaType classType = TypeFactory.defaultInstance().constructType(type); + + String canonicalName = classType.toCanonical(); + + if (canonicalName.startsWith("io.undertow") || canonicalName.startsWith("org.xnio") || canonicalName.equals("io.sinistral.proteus.server.ServerRequest") || canonicalName.contains(java.lang.Void.class.getName())) + { + return true; + } + + // TODO Auto-generated method stub + return super.shouldIgnoreClass(type); + } + +} diff --git a/src/main/java/io/sinistral/proteus/server/tools/oas/ServerParameterExtension.java b/src/main/java/io/sinistral/proteus/server/tools/oas/ServerParameterExtension.java new file mode 100644 index 0000000..45e1ab1 --- /dev/null +++ b/src/main/java/io/sinistral/proteus/server/tools/oas/ServerParameterExtension.java @@ -0,0 +1,323 @@ +/** + * + */ +package io.sinistral.proteus.server.tools.oas; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.CookieParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +import org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.AnnotationMap; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.ParameterProcessor; +import io.swagger.v3.jaxrs2.ResolvedParameter; +import io.swagger.v3.jaxrs2.ext.AbstractOpenAPIExtension; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtension; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtensions; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.parameters.Parameter; + +/** + * @author jbauer + * + */ + +public class ServerParameterExtension extends AbstractOpenAPIExtension { + private static String QUERY_PARAM = "query"; + private static String HEADER_PARAM = "header"; + private static String COOKIE_PARAM = "cookie"; + private static String PATH_PARAM = "path"; + private static String FORM_PARAM = "form"; + + final ObjectMapper mapper = Json.mapper(); + + @Override + public ResolvedParameter extractParameters(List annotations, + Type type, + Set typesToSkip, + Components components, + javax.ws.rs.Consumes classConsumes, + javax.ws.rs.Consumes methodConsumes, + boolean includeRequestBody, + JsonView jsonViewAnnotation, + Iterator chain) { + + if (shouldIgnoreType(type, typesToSkip)) { + return new ResolvedParameter(); + } + + JavaType javaType = constructType(type); + + boolean isRequired = true; + + if(_isOptionalType(javaType)) + { + isRequired = false; + } + + List parameters = new ArrayList<>(); + Parameter parameter = null; + ResolvedParameter extractParametersResult = new ResolvedParameter(); + + + for (Annotation annotation : annotations) { + if (annotation instanceof QueryParam) { + QueryParam param = (QueryParam) annotation; + Parameter qp = new Parameter(); + qp.setIn(QUERY_PARAM); + qp.setName(param.value()); + parameter = qp; + } else if (annotation instanceof PathParam) { + PathParam param = (PathParam) annotation; + Parameter pp = new Parameter(); + pp.setIn(PATH_PARAM); + pp.setName(param.value()); + parameter = pp; + } else if (annotation instanceof MatrixParam) { + MatrixParam param = (MatrixParam) annotation; + Parameter pp = new Parameter(); + pp.setIn(PATH_PARAM); + pp.setStyle(Parameter.StyleEnum.MATRIX); + pp.setName(param.value()); + parameter = pp; + } else if (annotation instanceof HeaderParam) { + HeaderParam param = (HeaderParam) annotation; + Parameter pp = new Parameter(); + pp.setIn(HEADER_PARAM); + pp.setName(param.value()); + parameter = pp; + } else if (annotation instanceof CookieParam) { + CookieParam param = (CookieParam) annotation; + Parameter pp = new Parameter(); + pp.setIn(COOKIE_PARAM); + pp.setName(param.value()); + parameter = pp; + } else if (annotation instanceof io.swagger.v3.oas.annotations.Parameter) { + if (((io.swagger.v3.oas.annotations.Parameter) annotation).hidden()) { + extractParametersResult.parameters = parameters; + return extractParametersResult; + } + if (parameter == null) { + Parameter pp = new Parameter(); + parameter = pp; + } + } else { + if (handleAdditionalAnnotation(parameters, annotation, type, typesToSkip, classConsumes, methodConsumes, components, includeRequestBody, jsonViewAnnotation)) { + + extractParametersResult.parameters.addAll(parameters); + } + } + } + + + + if (parameter != null && StringUtils.isNotBlank(parameter.getIn())) { + parameter.setRequired(isRequired); + parameters.add(parameter); + } else if (includeRequestBody) { + Parameter unknownParameter = ParameterProcessor.applyAnnotations( + null, + type, + annotations, + components, + classConsumes == null ? new String[0] : classConsumes.value(), + methodConsumes == null ? new String[0] : methodConsumes.value(), jsonViewAnnotation); + if (unknownParameter != null) { + + if (StringUtils.isNotBlank(unknownParameter.getIn()) && !"form".equals(unknownParameter.getIn())) { + extractParametersResult.parameters.add(unknownParameter); + } else if ("form".equals(unknownParameter.getIn())) { + unknownParameter.setIn(null); + extractParametersResult.formParameter = unknownParameter; + } else { // return as request body + extractParametersResult.requestBody = unknownParameter; + + } + } + } + for (Parameter p : parameters) { + + + Parameter processedParameter = ParameterProcessor.applyAnnotations( + p, + type, + annotations, + components, + classConsumes == null ? new String[0] : classConsumes.value(), + methodConsumes == null ? new String[0] : methodConsumes.value(), + jsonViewAnnotation); + + if (processedParameter != null) { + processedParameter.setRequired(isRequired); + extractParametersResult.parameters.add(processedParameter); + } + } + return extractParametersResult; + } + + protected boolean _isOptionalType(JavaType propType) { + return Arrays.asList("com.google.common.base.Optional", "java.util.Optional") + .contains(propType.getRawClass().getCanonicalName()); + } + + + /** + * Adds additional annotation processing support + * + * @param parameters + * @param annotation + * @param type + * @param typesToSkip + */ + + private boolean handleAdditionalAnnotation(List parameters, Annotation annotation, + final Type type, Set typesToSkip, javax.ws.rs.Consumes classConsumes, + javax.ws.rs.Consumes methodConsumes, Components components, boolean includeRequestBody, JsonView jsonViewAnnotation) { + boolean processed = false; + if (BeanParam.class.isAssignableFrom(annotation.getClass())) { + + + // Use Jackson's logic for processing Beans + JavaType javaType = constructType(type); + + final BeanDescription beanDesc = mapper.getSerializationConfig().introspect(javaType); + final List properties = beanDesc.findProperties(); + +// if(extracted.size() == 0) +// { +// System.out.println("Unable to find parameters..."); +// +// Parameter processedParam = ParameterProcessor.applyAnnotations( +// p, +// paramType, +// paramAnnotations, +// components, +// classConsumes == null ? new String[0] : classConsumes.value(), +// methodConsumes == null ? new String[0] : methodConsumes.value(), +// jsonViewAnnotation); +// } + + for (final BeanPropertyDefinition propDef : properties) { + final AnnotatedField field = propDef.getField(); + final AnnotatedMethod setter = propDef.getSetter(); + final AnnotatedMethod getter = propDef.getGetter(); + final List paramAnnotations = new ArrayList(); + final Iterator extensions = OpenAPIExtensions.chain(); + Type paramType = null; + + // Gather the field's details + if (field != null) { + paramType = field.getType(); + + AnnotationMap annotationMap = field.getAllAnnotations(); + + for (final Annotation fieldAnnotation : annotationMap.annotations()) { + if (!paramAnnotations.contains(fieldAnnotation)) { + paramAnnotations.add(fieldAnnotation); + } + } + } + + // Gather the setter's details but only the ones we need + if (setter != null) { + // Do not set the param class/type from the setter if the values are already identified + if (paramType == null) { + // paramType will stay null if there is no parameter + paramType = setter.getParameterType(0); + } + + AnnotationMap annotationMap = setter.getAllAnnotations(); + + for (final Annotation fieldAnnotation : annotationMap.annotations()) { + if (!paramAnnotations.contains(fieldAnnotation)) { + paramAnnotations.add(fieldAnnotation); + } + } + } + + // Gather the getter's details but only the ones we need + if (getter != null) { + // Do not set the param class/type from the getter if the values are already identified + if (paramType == null) { + paramType = getter.getType(); + } + + AnnotationMap annotationMap = getter.getAllAnnotations(); + + for (final Annotation fieldAnnotation : annotationMap.annotations()) { + if (!paramAnnotations.contains(fieldAnnotation)) { + paramAnnotations.add(fieldAnnotation); + } + } + } + + if (paramType == null) { + continue; + } + + + // Re-process all Bean fields and let the default swagger-jaxrs/swagger-jersey-jaxrs processors do their thing + List extracted = + extensions.next().extractParameters( + paramAnnotations, + paramType, + typesToSkip, + components, + classConsumes, + methodConsumes, + includeRequestBody, + jsonViewAnnotation, + extensions).parameters; + + + + + for (Parameter p : extracted) { + Parameter processedParam = ParameterProcessor.applyAnnotations( + p, + paramType, + paramAnnotations, + components, + classConsumes == null ? new String[0] : classConsumes.value(), + methodConsumes == null ? new String[0] : methodConsumes.value(), + jsonViewAnnotation); + if (processedParam != null) { + + System.out.println("added new parameters: " + processedParam); + parameters.add(processedParam); + } + } + + processed = true; + } + } + return processed; + } + + @Override + protected boolean shouldIgnoreClass(Class cls) { + return cls.getName().startsWith("javax.ws.rs.") || cls.getName().startsWith("io.undertow"); + + } +} diff --git a/src/main/java/io/sinistral/proteus/server/swagger/AnnotationHelper.java b/src/main/java/io/sinistral/proteus/server/tools/swagger/AnnotationHelper.java similarity index 98% rename from src/main/java/io/sinistral/proteus/server/swagger/AnnotationHelper.java rename to src/main/java/io/sinistral/proteus/server/tools/swagger/AnnotationHelper.java index 50d7990..33997ed 100644 --- a/src/main/java/io/sinistral/proteus/server/swagger/AnnotationHelper.java +++ b/src/main/java/io/sinistral/proteus/server/tools/swagger/AnnotationHelper.java @@ -1,7 +1,7 @@ /** * */ -package io.sinistral.proteus.server.swagger; +package io.sinistral.proteus.server.tools.swagger; import java.lang.annotation.Annotation; import java.lang.reflect.Parameter; @@ -136,7 +136,6 @@ else if( formParam != null ) { return parameter.getName(); } - } @Override diff --git a/src/main/java/io/sinistral/proteus/server/swagger/Reader.java b/src/main/java/io/sinistral/proteus/server/tools/swagger/Reader.java similarity index 99% rename from src/main/java/io/sinistral/proteus/server/swagger/Reader.java rename to src/main/java/io/sinistral/proteus/server/tools/swagger/Reader.java index b22653e..9d13929 100644 --- a/src/main/java/io/sinistral/proteus/server/swagger/Reader.java +++ b/src/main/java/io/sinistral/proteus/server/tools/swagger/Reader.java @@ -1,7 +1,7 @@ /** * */ -package io.sinistral.proteus.server.swagger; +package io.sinistral.proteus.server.tools.swagger; import java.lang.annotation.Annotation; import java.lang.reflect.Method; diff --git a/src/main/java/io/sinistral/proteus/server/swagger/ServerParameterExtension.java b/src/main/java/io/sinistral/proteus/server/tools/swagger/ServerParameterExtension.java similarity index 97% rename from src/main/java/io/sinistral/proteus/server/swagger/ServerParameterExtension.java rename to src/main/java/io/sinistral/proteus/server/tools/swagger/ServerParameterExtension.java index dbf1e70..8b51250 100644 --- a/src/main/java/io/sinistral/proteus/server/swagger/ServerParameterExtension.java +++ b/src/main/java/io/sinistral/proteus/server/tools/swagger/ServerParameterExtension.java @@ -1,7 +1,7 @@ /** * */ -package io.sinistral.proteus.server.swagger; +package io.sinistral.proteus.server.tools.swagger; import java.lang.annotation.Annotation; import java.lang.reflect.Type; diff --git a/src/main/java/io/sinistral/proteus/services/OpenAPIService.java b/src/main/java/io/sinistral/proteus/services/OpenAPIService.java new file mode 100644 index 0000000..a344046 --- /dev/null +++ b/src/main/java/io/sinistral/proteus/services/OpenAPIService.java @@ -0,0 +1,444 @@ + +package io.sinistral.proteus.services; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import javax.ws.rs.HttpMethod; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.typesafe.config.Config; + +import io.sinistral.proteus.server.endpoints.EndpointInfo; +import io.sinistral.proteus.server.tools.oas.Reader; +import io.sinistral.proteus.server.tools.oas.ServerModelResolver; +import io.sinistral.proteus.server.tools.oas.ServerParameterExtension; +import io.swagger.v3.jaxrs2.ext.OpenAPIExtensions; +import io.swagger.v3.jaxrs2.integration.JaxrsApplicationAndAnnotationScanner; +import io.swagger.v3.oas.integration.GenericOpenApiContext; +import io.swagger.v3.oas.integration.SwaggerConfiguration; +import io.swagger.v3.oas.integration.api.OpenApiContext; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RoutingHandler; +import io.undertow.util.Headers; +import io.undertow.util.Methods; + + +@Singleton +public class OpenAPIService extends BaseService implements Supplier +{ + + private static Logger log = LoggerFactory.getLogger(OpenAPIService.class.getCanonicalName()); + + protected final String resourcePathPrefix = "oas"; + + + @Inject + @Named("openapi.resourcePrefix") + protected String resourcePrefix; + + @Inject + @Named("openapi.basePath") + protected String basePath; + + @Inject + @Named("openapi.specFilename") + protected String specFilename; + + @Inject + @Named("openapi") + protected Config openAPIConfig; + + @Inject + @Named("application.name") + protected String applicationName; + + @Inject + @Named("openapi.port") + protected Integer port; + + @Inject + @Named("application.path") + protected String applicationPath; + + @Inject + protected RoutingHandler router; + + @Inject + @Named("registeredEndpoints") + protected Set registeredEndpoints; + + @Inject + @Named("registeredControllers") + protected Set> registeredControllers; + + @Inject + @Named("registeredHandlerWrappers") + protected Map registeredHandlerWrappers; + + protected ObjectMapper mapper = new ObjectMapper(); + + protected ObjectWriter writer = null; + + protected YAMLMapper yamlMapper = new YAMLMapper(); + + protected Path resourcePath = null; + + protected ClassLoader serviceClassLoader = null; + + protected OpenAPI openApi = null; + + protected String spec = null; + + protected String indexHTML = null; + + @SuppressWarnings("deprecation") + public OpenAPIService( ) + { + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true); + mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); + mapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH,true); + mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setSerializationInclusion(Include.NON_NULL); + + mapper.registerModule(new Jdk8Module()); + + + yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + yamlMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true); + yamlMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); + yamlMapper.configure(DeserializationFeature.EAGER_DESERIALIZER_FETCH,true); + yamlMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + yamlMapper.setSerializationInclusion(Include.NON_NULL); + + writer = yamlMapper.writerWithDefaultPrettyPrinter(); + writer = writer.without(SerializationFeature.WRITE_NULL_MAP_VALUES); + } + + + @SuppressWarnings("rawtypes") + public void generateSpec() throws Exception + { + + Set> classes = this.registeredControllers; + + OpenAPIExtensions.setExtensions(Collections.singletonList(new ServerParameterExtension())); + + OpenAPI openApi = new OpenAPI(); + + Info info = mapper.convertValue(openAPIConfig.getValue("info").unwrapped(), Info.class); + + openApi.setInfo(info); + + Map securitySchemes = mapper.convertValue(openAPIConfig.getValue("securitySchemes").unwrapped(), new TypeReference>(){}); + + if(openApi.getComponents() == null) + { + openApi.setComponents(new Components()); + } + + openApi.getComponents().setSecuritySchemes(securitySchemes); + + List servers = mapper.convertValue(openAPIConfig.getValue("servers").unwrapped(), new TypeReference>(){}); + + openApi.setServers(servers); + + SwaggerConfiguration config = new SwaggerConfiguration() + .resourceClasses(classes.stream().map( c -> c.getName()).collect(Collectors.toSet())) + .openAPI(openApi); + + + config.setModelConverterClassess(Collections.singleton(ServerModelResolver.class.getName())); + + OpenApiContext ctx = new GenericOpenApiContext() + .openApiConfiguration(config) + .openApiReader(new Reader(config)) + .openApiScanner(new JaxrsApplicationAndAnnotationScanner().openApiConfiguration(config)) + .init(); + + openApi = ctx.read(); + + this.openApi = openApi; + + this.spec = writer.writeValueAsString(openApi); + + } + + + public void generateHTML() + { + try + { + + try(InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream(resourcePrefix + "/index.html")) + { + byte[] templateBytes = IOUtils.toByteArray(templateInputStream); + + String templateString = new String(templateBytes,Charset.defaultCharset()); + + templateString = templateString.replaceAll("\\{\\{ basePath \\}\\}", basePath); + templateString = templateString.replaceAll("\\{\\{ title \\}\\}",applicationName + " Swagger UI"); + templateString = templateString.replaceAll("\\{\\{ filePath \\}\\}", basePath + ".yaml"); + + this.indexHTML = templateString; + } + + + URL url = this.getClass().getClassLoader().getResource(resourcePrefix); + + if( url.toExternalForm().contains("!") ) + { + log.debug("Copying OpenAPI resources..."); + + String jarPathString = url.toExternalForm().substring(0, url.toExternalForm().indexOf("!") ).replaceAll("file:", "").replaceAll("jar:", ""); + + File srcFile = new File(jarPathString); + + try(JarFile jarFile = new JarFile(srcFile, false)) + { + String appName = config.getString("application.name").replaceAll(" ", "_"); + + Path tmpDirParent = Files.createTempDirectory(appName); + + Path tmpDir = tmpDirParent.resolve("oas/"); + + if(tmpDir.toFile().exists()) + { + log.debug("Deleting existing OpenAPI directory at " + tmpDir); + + try + { + FileUtils.deleteDirectory(tmpDir.toFile()); + + } catch (java.lang.IllegalArgumentException e) + { + log.debug("Tmp directory is not a directory..."); + tmpDir.toFile().delete(); + } + } + + java.nio.file.Files.createDirectory( tmpDir ); + + this.resourcePath = tmpDir; + + jarFile.stream().filter( ze -> ze.getName().endsWith("js") || ze.getName().endsWith("css") || ze.getName().endsWith("map") || ze.getName().endsWith("html") ).forEach( ze -> { + + try + { + final InputStream entryInputStream = jarFile.getInputStream(ze); + + String filename = ze.getName().substring(resourcePrefix.length() + 1); + + Path entryFilePath = tmpDir.resolve(filename); + + java.nio.file.Files.createDirectories(entryFilePath.getParent()); + + java.nio.file.Files.copy(entryInputStream, entryFilePath,StandardCopyOption.REPLACE_EXISTING); + + } catch (Exception e) + { + log.error(e.getMessage() + " for entry " + ze.getName()); + } + }); + } + } + else + { + this.resourcePath = Paths.get(this.getClass().getClassLoader().getResource(this.resourcePrefix).toURI()); + this.serviceClassLoader = this.getClass().getClassLoader(); + } + + } catch (Exception e) + { + log.error(e.getMessage(),e); + } + } + + public RoutingHandler get() + { + + RoutingHandler router = new RoutingHandler(); + + /* + * YAML path + */ + + String pathTemplate = this.applicationPath + File.separator + this.specFilename ; + + //FileResourceManager resourceManager = new FileResourceManager(this.resourcePath.toFile(),1024); + + router.add(HttpMethod.GET, pathTemplate, new HttpHandler(){ + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception + { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, io.sinistral.proteus.server.MediaType.TEXT_YAML.contentType()); + + + try + { +// swaggerCopy.setHost(exchange.getHostAndPort()); +// +// spec = writer.writeValueAsString(swaggerCopy); + + } catch (Exception e) + { + log.error(e.getMessage(),e); + } + + exchange.getResponseSender().send(spec); + + } + + }); + + this.registeredEndpoints.add(EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName()).withMethod(Methods.GET).withProduces(io.sinistral.proteus.server.MediaType.TEXT_YAML.contentType()).build()); + + /* + pathTemplate = this.basePath; + + router.add(HttpMethod.GET, pathTemplate , new HttpHandler(){ + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception + { + + + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, MediaType.TEXT_HTML); + exchange.getResponseSender().send(indexHTML); + + } + + }); + + this.registeredEndpoints.add(EndpointInfo.builder().withConsumes(MediaType.WILDCARD).withProduces(MediaType.TEXT_HTML).withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName()).withMethod(Methods.GET).build()); + + + try + { + + pathTemplate = this.basePath + "/*"; + + router.add(HttpMethod.GET, pathTemplate, new ResourceHandler(resourceManager){ + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception + { + + String canonicalPath = CanonicalPathUtils.canonicalize((exchange.getRelativePath())); + + canonicalPath = canonicalPath.split(basePath)[1]; + + exchange.setRelativePath(canonicalPath); + + if(serviceClassLoader == null) + { + super.handleRequest(exchange); + } + else + { + canonicalPath = resourcePrefix + canonicalPath; + + try(final InputStream resourceInputStream = serviceClassLoader.getResourceAsStream( canonicalPath)) + { + + if(resourceInputStream == null) + { + ResponseCodeHandler.HANDLE_404.handleRequest(exchange); + return; + } + + byte[] resourceBytes = IOUtils.toByteArray(resourceInputStream); + + io.sinistral.proteus.server.MediaType mediaType = io.sinistral.proteus.server.MediaType.getByFileName(canonicalPath); + + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, mediaType.toString()); + + exchange.getResponseSender().send(ByteBuffer.wrap(resourceBytes)); + } + } + + } + + }); + + this.registeredEndpoints.add(EndpointInfo.builder().withConsumes(MediaType.WILDCARD).withProduces(MediaType.WILDCARD).withPathTemplate(pathTemplate).withControllerName(this.getClass().getSimpleName()).withMethod(Methods.GET).build()); + + + + } catch (Exception e) + { + log.error(e.getMessage(),e); + } + + */ + + return router; + } + + + + /* (non-Javadoc) + * @see com.google.common.util.concurrent.AbstractIdleService#startUp() + */ + @Override + protected void startUp() throws Exception + { + // TODO Auto-generated method stub + + + this.generateSpec(); + //this.generateHTML(); + + log.debug("\nOpenAPI Spec:\n" + writer.writeValueAsString(this.openApi)); + + router.addAll(this.get()); + } + + /* (non-Javadoc) + * @see com.google.common.util.concurrent.AbstractIdleService#shutDown() + */ + @Override + protected void shutDown() throws Exception + { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/io/sinistral/proteus/services/SwaggerService.java b/src/main/java/io/sinistral/proteus/services/SwaggerService.java index 054ae59..c3ff354 100644 --- a/src/main/java/io/sinistral/proteus/services/SwaggerService.java +++ b/src/main/java/io/sinistral/proteus/services/SwaggerService.java @@ -42,7 +42,7 @@ import io.sinistral.proteus.server.endpoints.EndpointInfo; import io.sinistral.proteus.server.security.MapIdentityManager; -import io.sinistral.proteus.server.swagger.ServerParameterExtension; +import io.sinistral.proteus.server.tools.swagger.ServerParameterExtension; import io.swagger.jaxrs.ext.SwaggerExtension; import io.swagger.jaxrs.ext.SwaggerExtensions; import io.swagger.models.Info; @@ -81,33 +81,33 @@ public class SwaggerService extends BaseService implements Supplier apiKeys = swaggerSecurity.getObjectList("apiKeys"); + List apiKeys = securityConfig.getObjectList("apiKeys"); for(ConfigObject apiKey : apiKeys) { @@ -280,9 +265,9 @@ public HttpHandler wrap(final HttpHandler handler) } } - if(swaggerSecurity.hasPath("basicRealms")) + if(securityConfig.hasPath("basicRealms")) { - List realms = swaggerSecurity.getObjectList("basicRealms"); + List realms = securityConfig.getObjectList("basicRealms"); for(ConfigObject realm : realms) { @@ -328,7 +313,7 @@ public HttpHandler wrap(final HttpHandler handler) } - this.reader = new io.sinistral.proteus.server.swagger.Reader(swagger); + this.reader = new io.sinistral.proteus.server.tools.swagger.Reader(swagger); classes.forEach( c -> this.reader.read(c)); @@ -359,7 +344,7 @@ public void generateSwaggerHTML() { - try(InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream(swaggerResourcePrefix + "/index.html")) + try(InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream(resourcePrefix + "/index.html")) { byte[] templateBytes = IOUtils.toByteArray(templateInputStream); @@ -367,32 +352,32 @@ public void generateSwaggerHTML() String themePath = "swagger-ui.css"; - if(!swaggerTheme.equals("default")) + if(!theme.equals("default")) { - themePath= "themes/theme-" + swaggerTheme + ".css"; + themePath= "themes/theme-" + theme + ".css"; } templateString = templateString.replaceAll("\\{\\{ themePath \\}\\}", themePath); - templateString = templateString.replaceAll("\\{\\{ swaggerBasePath \\}\\}", swaggerBasePath); + templateString = templateString.replaceAll("\\{\\{ swaggerBasePath \\}\\}", basePath); templateString = templateString.replaceAll("\\{\\{ title \\}\\}",applicationName + " Swagger UI"); - templateString = templateString.replaceAll("\\{\\{ swaggerFilePath \\}\\}", swaggerBasePath + ".json"); + templateString = templateString.replaceAll("\\{\\{ swaggerFilePath \\}\\}", basePath + ".json"); this.swaggerIndexHTML = templateString; } - try(InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream(swaggerResourcePrefix + "/redoc.html")) + try(InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream(resourcePrefix + "/redoc.html")) { byte[] templateBytes = IOUtils.toByteArray(templateInputStream); String templateString = new String(templateBytes,Charset.defaultCharset()); - templateString = templateString.replaceAll("\\{\\{ swaggerSpecPath \\}\\}", this.swaggerBasePath + ".json"); + templateString = templateString.replaceAll("\\{\\{ swaggerSpecPath \\}\\}", this.basePath + ".json"); templateString = templateString.replaceAll("\\{\\{ applicationName \\}\\}", applicationName); this.redocHTML = templateString; } - URL url = this.getClass().getClassLoader().getResource(swaggerResourcePrefix); + URL url = this.getClass().getClassLoader().getResource(resourcePrefix); if( url.toExternalForm().contains("!") ) { @@ -435,7 +420,7 @@ public void generateSwaggerHTML() { final InputStream entryInputStream = jarFile.getInputStream(ze); - String filename = ze.getName().substring(swaggerResourcePrefix.length() + 1); + String filename = ze.getName().substring(resourcePrefix.length() + 1); Path entryFilePath = swaggerTmpDir.resolve(filename); @@ -452,7 +437,7 @@ public void generateSwaggerHTML() } else { - this.swaggerResourcePath = Paths.get(this.getClass().getClassLoader().getResource(this.swaggerResourcePrefix).toURI()); + this.swaggerResourcePath = Paths.get(this.getClass().getClassLoader().getResource(this.resourcePrefix).toURI()); this.serviceClassLoader = this.getClass().getClassLoader(); } @@ -471,7 +456,7 @@ public RoutingHandler get() * JSON path */ - String pathTemplate = this.swaggerBasePath + ".json"; + String pathTemplate = this.basePath + ".json"; FileResourceManager resourceManager = new FileResourceManager(this.swaggerResourcePath.toFile(),1024); @@ -512,7 +497,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception * YAML path */ - pathTemplate = this.swaggerBasePath + ".yaml"; + pathTemplate = this.basePath + ".yaml"; router.add(HttpMethod.GET, pathTemplate, new HttpHandler(){ @@ -543,7 +528,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception this.registeredEndpoints.add(EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName("Swagger").withMethod(Methods.GET).withProduces(io.sinistral.proteus.server.MediaType.TEXT_YAML.contentType()).build()); - pathTemplate = this.swaggerBasePath + "/" + this.redocPath; + pathTemplate = this.basePath + "/" + this.redocPath; router.add(HttpMethod.GET,pathTemplate, new HttpHandler(){ @@ -561,7 +546,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception this.registeredEndpoints.add(EndpointInfo.builder().withConsumes("*/*").withPathTemplate(pathTemplate).withControllerName("Swagger").withMethod(Methods.GET).withProduces(MediaType.TEXT_HTML).build()); - pathTemplate = this.swaggerBasePath; + pathTemplate = this.basePath; router.add(HttpMethod.GET, pathTemplate , new HttpHandler(){ @@ -584,7 +569,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { - pathTemplate = this.swaggerBasePath + "/*"; + pathTemplate = this.basePath + "/*"; router.add(HttpMethod.GET, pathTemplate, new ResourceHandler(resourceManager){ @@ -594,7 +579,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception String canonicalPath = CanonicalPathUtils.canonicalize((exchange.getRelativePath())); - canonicalPath = canonicalPath.split(swaggerBasePath)[1]; + canonicalPath = canonicalPath.split(basePath)[1]; exchange.setRelativePath(canonicalPath); @@ -604,7 +589,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception } else { - canonicalPath = swaggerResourcePrefix + canonicalPath; + canonicalPath = resourcePrefix + canonicalPath; try(final InputStream resourceInputStream = serviceClassLoader.getResourceAsStream( canonicalPath)) { diff --git a/src/main/resources/io/sinistral/proteus/swagger/index.html b/src/main/resources/io/sinistral/proteus/server/tools/swagger/index.html similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/index.html rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/index.html diff --git a/src/main/resources/io/sinistral/proteus/proteus-logo.svg b/src/main/resources/io/sinistral/proteus/server/tools/swagger/proteus-logo.svg similarity index 100% rename from src/main/resources/io/sinistral/proteus/proteus-logo.svg rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/proteus-logo.svg diff --git a/src/main/resources/io/sinistral/proteus/swagger/redoc.html b/src/main/resources/io/sinistral/proteus/server/tools/swagger/redoc.html similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/redoc.html rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/redoc.html diff --git a/src/main/resources/io/sinistral/proteus/swagger/swagger-ui-bundle.js b/src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui-bundle.js similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/swagger-ui-bundle.js rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui-bundle.js diff --git a/src/main/resources/io/sinistral/proteus/swagger/swagger-ui-standalone-preset.js b/src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui-standalone-preset.js similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/swagger-ui-standalone-preset.js rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui-standalone-preset.js diff --git a/src/main/resources/io/sinistral/proteus/swagger/swagger-ui.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/swagger-ui.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/swagger-ui.js b/src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui.js similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/swagger-ui.js rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/swagger-ui.js diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-feeling-blue.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-feeling-blue.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-feeling-blue.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-feeling-blue.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-flattop.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-flattop.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-flattop.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-flattop.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-material.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-material.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-material.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-material.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-monokai.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-monokai.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-monokai.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-monokai.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-muted.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-muted.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-muted.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-muted.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-newspaper.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-newspaper.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-newspaper.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-newspaper.css diff --git a/src/main/resources/io/sinistral/proteus/swagger/themes/theme-outline.css b/src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-outline.css similarity index 100% rename from src/main/resources/io/sinistral/proteus/swagger/themes/theme-outline.css rename to src/main/resources/io/sinistral/proteus/server/tools/swagger/themes/theme-outline.css diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 797c70d..52aae81 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -59,10 +59,51 @@ assets { } +openapi { + + enabled=true + + resourcePrefix="io/sinistral/proteus/server/tools/oas" + + basePath= ${application.path}"/oas" + + port = ${application.ports.http} + + specFilename="openapi.yaml" + + openapi="3.0.1" + + # openapi info + info { + title = ${application.name} + version = ${application.version} + description="Proteus Server" + } + + + securitySchemes { + ApiKeyAuth = { + type="APIKEY" + in="HEADER" + name="X-API-KEY" + } + } + + servers { + url=${application.path} + description="Default Server" + } + + +} + swagger { + + enabled=true + # the path that has an index.html template and theme css files - resourcePrefix="io/sinistral/proteus/swagger" + resourcePrefix="io/sinistral/proteus/server/tools/swagger" host=${application.host} # swagger version @@ -72,8 +113,10 @@ swagger { # swagger info title title = ${application.name} # swagger info version - version = ${application.version} + version = ${application.version} } + + # swagger-ui theme from ostranme's swagger-ui-themes, the following are built-in [feeling-blue, flattop, material, monokai, muted, newspaper, outline] # specifying a different name causes the SwaggerService to search in {swagger.resourcePrefix}/themes for a file named "theme-{swagger.theme}.css" theme="default" @@ -83,8 +126,11 @@ swagger { redocPath= "redoc" #the name of the spec file specFilename="swagger.json" + consumes = ["application/json"] + produces = ["application/json"] + port = ${application.ports.http} security = @@ -109,8 +155,6 @@ swagger { # } # ] } - - } undertow diff --git a/src/test/java/io/sinistral/proteus/test/controllers/Tests.java b/src/test/java/io/sinistral/proteus/test/controllers/Tests.java index 174a44e..91959aa 100644 --- a/src/test/java/io/sinistral/proteus/test/controllers/Tests.java +++ b/src/test/java/io/sinistral/proteus/test/controllers/Tests.java @@ -169,9 +169,19 @@ public ServerResponse responsePlaintext(ServerRequest request) @GET @Path("/response/future/map") @ApiOperation(value = "Future map endpoint", httpMethod = "GET" ) - public CompletableFuture>> responseFutureMap() + public CompletableFuture>> responseFutureMap( ServerRequest request ) { - return CompletableFuture.completedFuture(response( ImmutableMap.of("message", "success") ).applicationJson()); + Map map = ImmutableMap.of("message", "success"); + return CompletableFuture.completedFuture(response( map ).applicationJson()); + } + + @GET + @Path("/response/map") + @ApiOperation(value = "Map endpoint", httpMethod = "GET" ) + public ServerResponse> futureMap( ServerRequest request ) + { + Map map = ImmutableMap.of("message", "success"); + return response( map ).applicationJson(); } @POST @@ -199,9 +209,9 @@ public ServerResponse responseEchoJson(ServerRequest request, @FormParam(" @Produces(MediaType.APPLICATION_OCTET_STREAM) @Consumes("*/*") @ApiOperation(value = "Echo json inner class endpoint", httpMethod = "POST" ) - public ServerResponse responseInnerClassTest(ServerRequest request, @BeanParam User.InnerUserModel userInnerModel ) throws Exception + public ServerResponse responseInnerClassTest(ServerRequest request, @BeanParam User user ) throws Exception { - return response(userInnerModel).applicationJson(); + return response(user).applicationJson(); } @@ -210,9 +220,7 @@ public ServerResponse responseInnerClassTest(ServerRequest @ApiOperation(value = "Generic set endpoint", httpMethod = "GET" ) public ServerResponse> genericSet( ServerRequest request, @QueryParam("ids") Set ids ) throws Exception { - - return response( ids ).applicationJson(); - + return response( ids ).applicationJson(); } @@ -221,16 +229,15 @@ public ServerResponse> genericSet( ServerRequest request, @QueryParam @ApiOperation(value = "Generic optional set endpoint", httpMethod = "GET" ) public ServerResponse> genericOptionalSet( ServerRequest request, @QueryParam("ids") Optional> ids ) throws Exception { - - return response( ids.get() ).applicationJson(); - + return response( ids.get() ).applicationJson(); } @GET @Path("/redirect/permanent") @ApiOperation(value = "Permanent redirect endpoint", httpMethod = "GET" ) - public ServerResponse testPermanentRedirect() + @Produces(MediaType.WILDCARD) + public ServerResponse testPermanentRedirect() { return response().redirectPermanently("https://google.com"); } @@ -238,7 +245,8 @@ public ServerResponse testPermanentRedirect() @GET @Path("/redirect") @ApiOperation(value = "Redirect endpoint", httpMethod = "GET" ) - public ServerResponse testRedirect() + @Produces(MediaType.WILDCARD) + public ServerResponse testRedirect() { return response().redirect("https://google.com"); } @@ -272,7 +280,6 @@ public ServerResponse responseUploadByteBuffer(ServerRequest request @GET @Path("/response/debug") - @Debug @ApiOperation(value = "Debug endpoint", httpMethod = "GET" ) public ServerResponse> debugEndpoint(ServerRequest request) { @@ -290,7 +297,6 @@ public ServerResponse> debugEndpoint(ServerRequest request) @GET @Path("/response/debug/blocking") - @Debug @Blocking @ApiOperation(value = "Debug blocking endpoint", httpMethod = "GET" ) public ServerResponse> debugBlockingEndpoint(ServerRequest request) diff --git a/src/test/java/io/sinistral/proteus/test/server/DefaultServer.java b/src/test/java/io/sinistral/proteus/test/server/DefaultServer.java index a58e992..730e4d0 100644 --- a/src/test/java/io/sinistral/proteus/test/server/DefaultServer.java +++ b/src/test/java/io/sinistral/proteus/test/server/DefaultServer.java @@ -17,6 +17,7 @@ import io.restassured.RestAssured; import io.sinistral.proteus.ProteusApplication; import io.sinistral.proteus.services.AssetsService; +import io.sinistral.proteus.services.OpenAPIService; import io.sinistral.proteus.services.SwaggerService; import io.sinistral.proteus.test.controllers.Tests; @@ -75,6 +76,8 @@ private static void runInternal(final RunNotifier notifier) app.addService(SwaggerService.class); app.addService(AssetsService.class); + app.addService(OpenAPIService.class); + app.addController(Tests.class); app.start(); @@ -85,6 +88,8 @@ private static void runInternal(final RunNotifier notifier) { Thread.sleep(2000); + System.out.println(app.getPorts()); + List ports = app.getPorts(); port = ports.get(0); diff --git a/src/test/java/io/sinistral/proteus/test/server/TestControllerEndpoints.java b/src/test/java/io/sinistral/proteus/test/server/TestControllerEndpoints.java index eeeba68..960f5d2 100644 --- a/src/test/java/io/sinistral/proteus/test/server/TestControllerEndpoints.java +++ b/src/test/java/io/sinistral/proteus/test/server/TestControllerEndpoints.java @@ -4,6 +4,8 @@ package io.sinistral.proteus.test.server; import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; + import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -77,6 +79,12 @@ public void testSwaggerDocs() given().accept(ContentType.JSON).log().uri().when().get("swagger.json").then().statusCode(200).and().body("basePath", is("/v1")); } + @Test + public void testOpenAPIDocs() + { + when().get("openapi.yaml").then().statusCode(200); + } + @Test public void testDebugEndpoint() { diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf index ecb1910..95b0cdb 100644 --- a/src/test/resources/application.conf +++ b/src/test/resources/application.conf @@ -59,10 +59,47 @@ assets { } +openapi { + + + resourcePrefix="io/sinistral/proteus/server/tools/oas" + + basePath= ${application.path}"/oas" + + port = ${application.ports.http} + + specFilename="openapi.yaml" + + openapi="3.0.1" + + # openapi info + info { + title = ${application.name} + version = ${application.version} + description="Proteus Server" + } + + + securitySchemes { + ApiKeyAuth = { + type="APIKEY" + in="HEADER" + name="X-API-KEY" + } + } + + servers { + url=${application.path} + description="Default Server" + } + + +} + swagger { # the path that has an index.html template and theme css files - resourcePrefix="io/sinistral/proteus/swagger" + resourcePrefix="io/sinistral/proteus/server/tools/swagger" # swagger version swagger="2.0" info { diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index eb27530..b1c6fff 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -10,6 +10,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +