diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/model/util/ModelUtils.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/model/util/ModelUtils.java index 995c1ec8be2..5e54903853b 100644 --- a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/model/util/ModelUtils.java +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/model/util/ModelUtils.java @@ -50,7 +50,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.function.BiFunction; import static java.util.logging.Level.WARNING; import java.util.logging.Logger; @@ -67,7 +66,6 @@ import javax.ws.rs.PATCH; import javax.ws.rs.POST; import javax.ws.rs.PUT; -import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; @@ -79,11 +77,9 @@ import org.eclipse.microprofile.openapi.models.PathItem.HttpMethod; import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType; import org.eclipse.microprofile.openapi.models.parameters.Parameter.In; -import org.glassfish.hk2.classmodel.reflect.AnnotatedElement; import org.glassfish.hk2.classmodel.reflect.AnnotationModel; -import org.glassfish.hk2.classmodel.reflect.ClassModel; import org.glassfish.hk2.classmodel.reflect.MethodModel; -import org.glassfish.hk2.classmodel.reflect.Type; +import org.glassfish.hk2.classmodel.reflect.ParameterizedType; public final class ModelUtils { @@ -271,7 +267,7 @@ public static void removeOperation(PathItem pathItem, Operation operation) { } } - public static SchemaType getSchemaType(org.glassfish.hk2.classmodel.reflect.ParameterizedType type, ApiContext context) { + public static SchemaType getSchemaType(ParameterizedType type, ApiContext context) { if(type.isArray()) { return SchemaType.ARRAY; } else { @@ -320,6 +316,19 @@ public static SchemaType getSchemaType(String typeName, ApiContext context) { return SchemaType.OBJECT; } + public static boolean isMap(String typeName, ApiContext context) { + Class clazz = null; + try { + clazz = context.getApplicationClassLoader().loadClass(typeName); + } catch (Throwable app) { + try { + clazz = Class.forName(typeName); + } catch (Throwable t) { + } + } + return Map.class.isAssignableFrom(clazz); + } + /** * Finds a {@link SchemaType} that can represent both of the given types. If one * of the input values are null, this function returns the other. If both are diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java index 1435cd011a9..3c74355580a 100644 --- a/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/main/java/fish/payara/microprofile/openapi/impl/processor/ApplicationProcessor.java @@ -64,8 +64,9 @@ import fish.payara.microprofile.openapi.impl.model.util.ModelUtils; import fish.payara.microprofile.openapi.impl.visitor.OpenApiWalker; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -76,17 +77,25 @@ import java.util.stream.Collectors; import javax.ws.rs.DefaultValue; import javax.ws.rs.FormParam; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.media.Schema; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.models.ExternalDocumentation; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.PathItem; import org.eclipse.microprofile.openapi.models.Reference; +import org.eclipse.microprofile.openapi.models.callbacks.Callback; import org.eclipse.microprofile.openapi.models.media.MediaType; -import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType; +import org.eclipse.microprofile.openapi.models.media.Schema; +import org.eclipse.microprofile.openapi.models.parameters.Parameter; import org.eclipse.microprofile.openapi.models.parameters.Parameter.In; import org.eclipse.microprofile.openapi.models.parameters.Parameter.Style; +import org.eclipse.microprofile.openapi.models.parameters.RequestBody; +import org.eclipse.microprofile.openapi.models.Operation; +import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType; +import org.eclipse.microprofile.openapi.models.responses.APIResponse; +import org.eclipse.microprofile.openapi.models.responses.APIResponses; +import org.eclipse.microprofile.openapi.models.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; +import org.eclipse.microprofile.openapi.models.servers.Server; +import org.eclipse.microprofile.openapi.models.tags.Tag; import org.glassfish.hk2.classmodel.reflect.AnnotatedElement; import org.glassfish.hk2.classmodel.reflect.AnnotationModel; import org.glassfish.hk2.classmodel.reflect.ClassModel; @@ -94,8 +103,10 @@ import org.glassfish.hk2.classmodel.reflect.ExtensibleType; import org.glassfish.hk2.classmodel.reflect.FieldModel; import org.glassfish.hk2.classmodel.reflect.MethodModel; +import org.glassfish.hk2.classmodel.reflect.ParameterizedInterfaceModel; import org.glassfish.hk2.classmodel.reflect.Type; import org.glassfish.hk2.classmodel.reflect.Types; +import org.glassfish.hk2.classmodel.reflect.ParameterizedType; /** * A processor to parse the application for annotations, to add to the OpenAPI @@ -121,7 +132,8 @@ public class ApplicationProcessor implements OASProcessor, ApiVisitor { /** * @param types parsed application classes - * @param allowedTypes filtered application classes for OpenAPI metadata processing + * @param allowedTypes filtered application classes for OpenAPI metadata + * processing * @param appClassLoader the class loader for the application. */ public ApplicationProcessor(Types allTypes, Set allowedTypes, ClassLoader appClassLoader) { @@ -155,7 +167,7 @@ public void visitGET(AnnotationModel get, MethodModel element, ApiContext contex PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setGET(operation); operation.setOperationId(element.getName()); @@ -176,7 +188,7 @@ public void visitPOST(AnnotationModel post, MethodModel element, ApiContext cont PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setPOST(operation); operation.setOperationId(element.getName()); @@ -197,7 +209,7 @@ public void visitPUT(AnnotationModel put, MethodModel element, ApiContext contex PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setPUT(operation); operation.setOperationId(element.getName()); @@ -218,7 +230,7 @@ public void visitDELETE(AnnotationModel delete, MethodModel element, ApiContext PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setDELETE(operation); operation.setOperationId(element.getName()); @@ -239,7 +251,7 @@ public void visitHEAD(AnnotationModel head, MethodModel element, ApiContext cont PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setHEAD(operation); operation.setOperationId(element.getName()); @@ -260,7 +272,7 @@ public void visitOPTIONS(AnnotationModel options, MethodModel element, ApiContex PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setOPTIONS(operation); operation.setOperationId(element.getName()); @@ -281,7 +293,7 @@ public void visitPATCH(AnnotationModel patch, MethodModel element, ApiContext co PathItem pathItem = context.getApi().getPaths().getOrDefault(context.getPath(), new PathItemImpl()); context.getApi().getPaths().addPathItem(context.getPath(), pathItem); - org.eclipse.microprofile.openapi.models.Operation operation = new OperationImpl(); + Operation operation = new OperationImpl(); pathItem.setPATCH(operation); operation.setOperationId(element.getName()); @@ -295,7 +307,7 @@ public void visitPATCH(AnnotationModel patch, MethodModel element, ApiContext co @Override public void visitProduces(AnnotationModel produces, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel && context.getWorkingOperation() != null) { - for (org.eclipse.microprofile.openapi.models.responses.APIResponse response : context.getWorkingOperation() + for (APIResponse response : context.getWorkingOperation() .getResponses().values()) { if (response != null) { @@ -320,7 +332,7 @@ public void visitProduces(AnnotationModel produces, AnnotatedElement element, Ap @Override public void visitConsumes(AnnotationModel consumes, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel && context.getWorkingOperation() != null) { - org.eclipse.microprofile.openapi.models.parameters.RequestBody requestBody = context.getWorkingOperation() + RequestBody requestBody = context.getWorkingOperation() .getRequestBody(); if (requestBody != null) { @@ -395,7 +407,7 @@ public void visitCookieParam(AnnotationModel param, AnnotatedElement element, Ap } private static void addParameter(AnnotatedElement element, ApiContext context, String name, In in, Boolean required) { - org.eclipse.microprofile.openapi.models.parameters.Parameter newParameter = new ParameterImpl(); + Parameter newParameter = new ParameterImpl(); newParameter.setName(name); newParameter.setIn(in); newParameter.setStyle(Style.SIMPLE); @@ -435,14 +447,14 @@ private static void addParameter(AnnotatedElement element, ApiContext context, S private static SchemaImpl getArraySchema(AnnotatedElement element, ApiContext context) { SchemaImpl arraySchema = new SchemaImpl(); - List parameterizedType; + List parameterizedType; if (element instanceof org.glassfish.hk2.classmodel.reflect.Parameter) { org.glassfish.hk2.classmodel.reflect.Parameter parameter = (org.glassfish.hk2.classmodel.reflect.Parameter) element; - parameterizedType = parameter.getGenericTypes(); + parameterizedType = parameter.getParameterizedTypes(); } else { FieldModel field = (FieldModel) element; - parameterizedType = field.getGenericTypes(); + parameterizedType = field.getParameterizedTypes(); } arraySchema.setType(ModelUtils.getSchemaType(parameterizedType.get(0).getTypeName(), context)); @@ -471,7 +483,7 @@ public void visitOpenAPI(AnnotationModel definition, AnnotatedElement element, A @Override public void visitSchema(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { if (element instanceof ClassModel) { - visitSchemaClass(annotation, (ClassModel) element, context); + visitSchemaClass(null, annotation, (ClassModel) element, Collections.emptyList(), context); } else if (element instanceof EnumType) { vistEnumClass(annotation, (EnumType) element, context); } else if (element instanceof FieldModel) { @@ -487,9 +499,9 @@ private void vistEnumClass(AnnotationModel schemaAnnotation, EnumType enumType, if (schemaName == null || schemaName.isEmpty()) { schemaName = enumType.getSimpleName(); } - org.eclipse.microprofile.openapi.models.media.Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); + Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); - org.eclipse.microprofile.openapi.models.media.Schema newSchema = new SchemaImpl(); + Schema newSchema = new SchemaImpl(); context.getApi().getComponents().addSchema(schemaName, newSchema); if (schema != null) { SchemaImpl.merge(schema, newSchema, true, context); @@ -503,30 +515,36 @@ private void vistEnumClass(AnnotationModel schemaAnnotation, EnumType enumType, } - private void visitSchemaClass(AnnotationModel schemaAnnotation, ClassModel clazz, ApiContext context) { + private Schema visitSchemaClass( + Schema schema, + AnnotationModel schemaAnnotation, ClassModel clazz, + Collection parameterizedInterfaces, + ApiContext context) { + // Get the schema object name String schemaName = (schemaAnnotation == null) ? null : schemaAnnotation.getValue("name", String.class); if (schemaName == null || schemaName.isEmpty()) { schemaName = clazz.getSimpleName(); } - org.eclipse.microprofile.openapi.models.media.Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); // Add a new schema - org.eclipse.microprofile.openapi.models.media.Schema newSchema = new SchemaImpl(); - context.getApi().getComponents().addSchema(schemaName, newSchema); + if (schema == null) { + schema = new SchemaImpl(); + context.getApi().getComponents().addSchema(schemaName, schema); + } // If there is an annotation - if (schema != null) { - SchemaImpl.merge(schema, newSchema, true, context); - } else { - newSchema.setType(SchemaType.OBJECT); - Map fields = new LinkedHashMap<>(); - for (FieldModel field : clazz.getFields()) { - if (!field.isTransient()) { - fields.put(field.getName(), createSchema(context, clazz, field)); - } + if (schemaAnnotation != null) { + SchemaImpl.merge(SchemaImpl.createInstance(schemaAnnotation, context), schema, true, context); + } + for (FieldModel field : clazz.getFields()) { + if (!field.isTransient() && !field.getName().startsWith("this$")) { + schema.addProperty(field.getName(), createSchema(null, context, field, clazz, parameterizedInterfaces)); } - newSchema.setProperties(fields); + } + + if (schema.getType() == null) { + schema.setType(ModelUtils.getSchemaType(clazz.getName(), context)); } // If there is an extending class, add the data @@ -537,36 +555,42 @@ private void visitSchemaClass(AnnotationModel schemaAnnotation, ClassModel clazz if (superClass != null) { // Get the parent schema annotation - AnnotationModel parentSchemAnnotation = superClass.getAnnotation(Schema.class.getName()); + AnnotationModel parentSchemAnnotation = AnnotationInfo.valueOf(superClass) + .getAnnotation(org.eclipse.microprofile.openapi.annotations.media.Schema.class); - if (parentSchemAnnotation != null) { + ParameterizedInterfaceModel parameterizedInterface = clazz.getParameterizedInterface(superClass); + if (parameterizedInterface == null) { // Create a schema for the parent - visitSchema(parentSchemAnnotation, superClass, context); + visitSchemaClass(null, parentSchemAnnotation, superClass, Collections.emptyList(), context); // Get the superclass schema name - String parentSchemaName = parentSchemAnnotation.getValue("name", String.class); + String parentSchemaName = parentSchemAnnotation == null ? null : parentSchemAnnotation.getValue("name", String.class); if (parentSchemaName == null || parentSchemaName.isEmpty()) { parentSchemaName = superClass.getSimpleName(); } // Link the schemas - newSchema.addAllOf(new SchemaImpl().ref(parentSchemaName)); + schema.addAllOf(new SchemaImpl().ref(parentSchemaName)); + } else { + visitSchemaClass(schema, parentSchemAnnotation, superClass, parameterizedInterface.getParametizedTypes(), context); } } } + return schema; } - private static void visitSchemaField(AnnotationModel schemaAnnotation, FieldModel field, ApiContext context) { + public void visitSchemaField(AnnotationModel schemaAnnotation, FieldModel field, ApiContext context) { // Get the schema object name String schemaName = (schemaAnnotation == null) ? null : schemaAnnotation.getValue("name", String.class); if (schemaName == null || schemaName.isEmpty()) { schemaName = field.getName(); } - org.eclipse.microprofile.openapi.models.media.Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); + Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); // Get the parent schema object name String parentName = null; - AnnotationModel classSchemaAnnotation = field.getDeclaringType().getAnnotation(Schema.class.getName()); + AnnotationModel classSchemaAnnotation = AnnotationInfo.valueOf(field.getDeclaringType()) + .getAnnotation(org.eclipse.microprofile.openapi.annotations.media.Schema.class); if (classSchemaAnnotation != null) { parentName = classSchemaAnnotation.getValue("name", String.class); } @@ -575,12 +599,14 @@ private static void visitSchemaField(AnnotationModel schemaAnnotation, FieldMode } // Get or create the parent schema object - org.eclipse.microprofile.openapi.models.media.Schema parent = context.getApi().getComponents().getSchemas() - .getOrDefault(parentName, new SchemaImpl()); - context.getApi().getComponents().getSchemas().put(parentName, parent); - - org.eclipse.microprofile.openapi.models.media.Schema property = new SchemaImpl(); - parent.addProperty(schemaName, property); + Map schemas + = context.getApi().getComponents().getSchemas(); + Schema parentSchema + = schemas.getOrDefault(parentName, new SchemaImpl()); + schemas.put(parentName, parentSchema); + + Schema property = new SchemaImpl(); + parentSchema.addProperty(schemaName, property); property.setType(ModelUtils.getSchemaType(field.getTypeName(), context)); SchemaImpl.merge(schema, property, true, context); } @@ -598,16 +624,16 @@ private static void visitSchemaParameter(AnnotationModel schemaAnnotation, org.g // Insert the schema to the request body media type MediaType mediaType = context.getWorkingOperation().getRequestBody().getContent() .getMediaType(javax.ws.rs.core.MediaType.WILDCARD); - org.eclipse.microprofile.openapi.models.media.Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); + Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); SchemaImpl.merge(schema, mediaType.getSchema(), true, context); if (schema.getRef() != null && !schema.getRef().isEmpty()) { mediaType.setSchema(new SchemaImpl().ref(schema.getRef())); } } else if (ModelUtils.getParameterType(parameter) != null) { - for (org.eclipse.microprofile.openapi.models.parameters.Parameter param : context.getWorkingOperation() + for (Parameter param : context.getWorkingOperation() .getParameters()) { if (param.getName().equals(ModelUtils.getParameterName(parameter))) { - org.eclipse.microprofile.openapi.models.media.Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); + Schema schema = SchemaImpl.createInstance(schemaAnnotation, context); SchemaImpl.merge(schema, param.getSchema(), true, context); if (schema.getRef() != null && !schema.getRef().isEmpty()) { param.setSchema(new SchemaImpl().ref(schema.getRef())); @@ -655,7 +681,7 @@ public void visitOperation(AnnotationModel annotation, AnnotatedElement element, public void visitCallback(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel) { String name = annotation.getValue("name", String.class); - org.eclipse.microprofile.openapi.models.callbacks.Callback callbackModel = context.getWorkingOperation() + Callback callbackModel = context.getWorkingOperation() .getCallbacks().getOrDefault(name, new CallbackImpl()); context.getWorkingOperation().getCallbacks().put(name, callbackModel); CallbackImpl.merge(CallbackImpl.createInstance(annotation, context), callbackModel, true, context); @@ -673,7 +699,7 @@ public void visitCallbacks(AnnotationModel annotation, AnnotatedElement element, @Override public void visitRequestBody(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel || element instanceof org.glassfish.hk2.classmodel.reflect.Parameter) { - org.eclipse.microprofile.openapi.models.parameters.RequestBody currentRequestBody = context + RequestBody currentRequestBody = context .getWorkingOperation().getRequestBody(); if (currentRequestBody != null || element instanceof org.glassfish.hk2.classmodel.reflect.Parameter) { RequestBodyImpl.merge(RequestBodyImpl.createInstance(annotation, context), currentRequestBody, true, context); @@ -689,21 +715,22 @@ public void visitAPIResponse(AnnotationModel annotation, AnnotatedElement elemen // If an APIResponse has been processed that isn't the default String responseCode = apiResponse.getResponseCode(); if (responseCode != null && !responseCode.isEmpty() && !responseCode - .equals(org.eclipse.microprofile.openapi.models.responses.APIResponses.DEFAULT)) { + .equals(APIResponses.DEFAULT)) { // If the element doesn't also contain a response mapping to the default - AnnotationModel apiResponsesParent = element.getAnnotation(APIResponses.class.getName()); + AnnotationModel apiResponsesParent = element + .getAnnotation(org.eclipse.microprofile.openapi.annotations.responses.APIResponses.class.getName()); if (apiResponsesParent != null) { List apiResponses = apiResponsesParent.getValue("value", List.class); if (apiResponses.stream() .map(a -> a.getValue("responseCode", String.class)) - .noneMatch(code -> code == null || code.isEmpty() || code.equals(org.eclipse.microprofile.openapi.models.responses.APIResponses.DEFAULT))) { + .noneMatch(code -> code == null || code.isEmpty() || code.equals(APIResponses.DEFAULT))) { // Then remove the default response context.getWorkingOperation().getResponses() - .removeAPIResponse(org.eclipse.microprofile.openapi.models.responses.APIResponses.DEFAULT); + .removeAPIResponse(APIResponses.DEFAULT); } } else { context.getWorkingOperation().getResponses() - .removeAPIResponse(org.eclipse.microprofile.openapi.models.responses.APIResponses.DEFAULT); + .removeAPIResponse(APIResponses.DEFAULT); } } } @@ -726,8 +753,8 @@ public void visitParameters(AnnotationModel annotation, AnnotatedElement element @Override public void visitParameter(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { - org.eclipse.microprofile.openapi.models.parameters.Parameter matchedParam = null; - org.eclipse.microprofile.openapi.models.parameters.Parameter parameter = ParameterImpl.createInstance(annotation, context); + Parameter matchedParam = null; + Parameter parameter = ParameterImpl.createInstance(annotation, context); if (element instanceof org.glassfish.hk2.classmodel.reflect.Parameter) { matchedParam = findOperationParameterFor((org.glassfish.hk2.classmodel.reflect.Parameter) element, context); @@ -757,8 +784,8 @@ public void visitParameter(AnnotationModel annotation, AnnotatedElement element, } } - private static org.eclipse.microprofile.openapi.models.parameters.Parameter findOperationParameterFor( - org.eclipse.microprofile.openapi.models.parameters.Parameter parameter, + private static Parameter findOperationParameterFor( + Parameter parameter, MethodModel annotated, ApiContext context) { String name = parameter.getName(); @@ -782,7 +809,7 @@ private static org.eclipse.microprofile.openapi.models.parameters.Parameter find // If there's only one matching parameter, handle it immediately String matchingMethodParamName = ModelUtils.getParameterName(matchingMethodParameters.get(0)); // Find the matching operation parameter - for (org.eclipse.microprofile.openapi.models.parameters.Parameter operationParam : context + for (Parameter operationParam : context .getWorkingOperation().getParameters()) { if (operationParam.getName().equals(matchingMethodParamName)) { return operationParam; @@ -795,13 +822,13 @@ private static org.eclipse.microprofile.openapi.models.parameters.Parameter find /** * Find the matching parameter, and match it */ - private static org.eclipse.microprofile.openapi.models.parameters.Parameter findOperationParameterFor( + private static Parameter findOperationParameterFor( org.glassfish.hk2.classmodel.reflect.Parameter annotated, ApiContext context) { String actualName = ModelUtils.getParameterName(annotated); if (actualName == null) { return null; } - for (org.eclipse.microprofile.openapi.models.parameters.Parameter param : context.getWorkingOperation() + for (Parameter param : context.getWorkingOperation() .getParameters()) { if (actualName.equals(param.getName())) { return param; @@ -814,7 +841,7 @@ private static org.eclipse.microprofile.openapi.models.parameters.Parameter find public void visitExternalDocumentation(AnnotationModel externalDocs, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel) { - org.eclipse.microprofile.openapi.models.ExternalDocumentation newExternalDocs = new ExternalDocumentationImpl(); + ExternalDocumentation newExternalDocs = new ExternalDocumentationImpl(); ExternalDocumentationImpl.merge(ExternalDocumentationImpl.createInstance(externalDocs), newExternalDocs, true); if (newExternalDocs.getUrl() != null && !newExternalDocs.getUrl().isEmpty()) { context.getWorkingOperation().setExternalDocs(newExternalDocs); @@ -825,7 +852,7 @@ public void visitExternalDocumentation(AnnotationModel externalDocs, AnnotatedEl @Override public void visitServer(AnnotationModel server, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel) { - org.eclipse.microprofile.openapi.models.servers.Server newServer = new ServerImpl(); + Server newServer = new ServerImpl(); context.getWorkingOperation().addServer(newServer); ServerImpl.merge(ServerImpl.createInstance(server, context), newServer, true); } @@ -841,11 +868,11 @@ public void visitServers(AnnotationModel annotation, AnnotatedElement element, A @Override public void visitTag(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { - org.eclipse.microprofile.openapi.models.tags.Tag from = TagImpl.createInstance(annotation, context); + Tag from = TagImpl.createInstance(annotation, context); if (element instanceof MethodModel) { TagImpl.merge(from, context.getWorkingOperation(), true, context.getApi().getTags()); } else { - org.eclipse.microprofile.openapi.models.tags.Tag newTag = new TagImpl(); + Tag newTag = new TagImpl(); TagImpl.merge(from, newTag, true); if (newTag.getName() != null && !newTag.getName().isEmpty()) { context.getApi().getTags().add(newTag); @@ -876,9 +903,9 @@ public void visitTags(AnnotationModel annotation, AnnotatedElement element, ApiC @Override public void visitSecurityScheme(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { String securitySchemeName = annotation.getValue("securitySchemeName", String.class); - org.eclipse.microprofile.openapi.models.security.SecurityScheme securityScheme = SecuritySchemeImpl.createInstance(annotation, context); + SecurityScheme securityScheme = SecuritySchemeImpl.createInstance(annotation, context); if (securitySchemeName != null && !securitySchemeName.isEmpty()) { - org.eclipse.microprofile.openapi.models.security.SecurityScheme newScheme = context.getApi().getComponents() + SecurityScheme newScheme = context.getApi().getComponents() .getSecuritySchemes().getOrDefault(securitySchemeName, new SecuritySchemeImpl()); context.getApi().getComponents().addSecurityScheme(securitySchemeName, newScheme); SecuritySchemeImpl.merge(securityScheme, newScheme, true); @@ -897,9 +924,9 @@ public void visitSecuritySchemes(AnnotationModel annotation, AnnotatedElement el public void visitSecurityRequirement(AnnotationModel annotation, AnnotatedElement element, ApiContext context) { if (element instanceof MethodModel) { String securityRequirementName = annotation.getValue("name", String.class); - org.eclipse.microprofile.openapi.models.security.SecurityRequirement securityRequirement = SecurityRequirementImpl.createInstance(annotation, context); + SecurityRequirement securityRequirement = SecurityRequirementImpl.createInstance(annotation, context); if (securityRequirementName != null && !securityRequirementName.isEmpty()) { - org.eclipse.microprofile.openapi.models.security.SecurityRequirement model = new SecurityRequirementImpl(); + SecurityRequirement model = new SecurityRequirementImpl(); SecurityRequirementImpl.merge(securityRequirement, model); context.getWorkingOperation().addSecurityRequirement(model); } @@ -915,9 +942,9 @@ public void visitSecurityRequirements(AnnotationModel annotation, AnnotatedEleme } // PRIVATE METHODS - private org.eclipse.microprofile.openapi.models.parameters.RequestBody insertDefaultRequestBody(ApiContext context, - org.eclipse.microprofile.openapi.models.Operation operation, MethodModel method) { - org.eclipse.microprofile.openapi.models.parameters.RequestBody requestBody = new RequestBodyImpl(); + private RequestBody insertDefaultRequestBody(ApiContext context, + Operation operation, MethodModel method) { + RequestBody requestBody = new RequestBodyImpl(); // Get the request body type of the method org.glassfish.hk2.classmodel.reflect.ParameterizedType bodyType = null; @@ -948,9 +975,9 @@ private org.eclipse.microprofile.openapi.models.parameters.RequestBody insertDef * @param method the {@link Method} to model the default response on. * @return the newly created {@link APIResponse}. */ - private org.eclipse.microprofile.openapi.models.responses.APIResponse insertDefaultResponse(ApiContext context, - org.eclipse.microprofile.openapi.models.Operation operation, MethodModel method) { - org.eclipse.microprofile.openapi.models.responses.APIResponse defaultResponse = new APIResponseImpl(); + private APIResponse insertDefaultResponse(ApiContext context, + Operation operation, MethodModel method) { + APIResponse defaultResponse = new APIResponseImpl(); defaultResponse.setDescription("Default Response."); // Create the default response with a wildcard mediatype @@ -961,7 +988,7 @@ private org.eclipse.microprofile.openapi.models.responses.APIResponse insertDefa // Add the default response operation.setResponses(new APIResponsesImpl().addAPIResponse( - org.eclipse.microprofile.openapi.models.responses.APIResponses.DEFAULT, defaultResponse)); + APIResponses.DEFAULT, defaultResponse)); return defaultResponse; } @@ -982,24 +1009,33 @@ private static String getContentType(String name) { return contentType; } - private org.eclipse.microprofile.openapi.models.media.Schema createSchema( + private Schema createSchema( ApiContext context, - org.glassfish.hk2.classmodel.reflect.ParameterizedType type) { + ParameterizedType type) { + return createSchema(null, context, type); + } - String typeName = type.getTypeName(); - List genericType = type.getGenericTypes(); + private Schema createSchema( + Schema schema, + ApiContext context, + ParameterizedType type) { - org.eclipse.microprofile.openapi.models.media.Schema schema = new SchemaImpl(); + String typeName = type.getTypeName(); + List genericTypes = type.getParameterizedTypes(); SchemaType schemaType = ModelUtils.getSchemaType(type, context); - schema.setType(schemaType); + + if (schema == null) { + schema = new SchemaImpl(); + schema.setType(schemaType); + } // Set the subtype if it's an array (for example an array of ints) if (schemaType == SchemaType.ARRAY) { if (type.isArray()) { schemaType = ModelUtils.getSchemaType(type.getTypeName(), context); schema.setType(schemaType); - } else if (!genericType.isEmpty()) { // should be something Iterable - schema.setItems(createSchema(context, genericType.get(0))); + } else if (!genericTypes.isEmpty()) { // should be something Iterable + schema.setItems(createSchema(context, genericTypes.get(0))); } } @@ -1014,21 +1050,99 @@ private org.eclipse.microprofile.openapi.models.media.Schema createSchema( return schema; } - private org.eclipse.microprofile.openapi.models.media.Schema createSchema( + private Schema createSchema( + Schema schema, ApiContext context, - AnnotatedElement annotatedElement, - org.glassfish.hk2.classmodel.reflect.ParameterizedType type) { + ParameterizedType type, + ExtensibleType clazz, + Collection classParameterizedTypes) { + if (schema == null) { + schema = new SchemaImpl(); + } SchemaType schemaType = ModelUtils.getSchemaType(type, context); // If the annotated element is the same type as the reference class, return a null schema - if (schemaType == SchemaType.OBJECT && type.getType().equals(annotatedElement)) { - org.eclipse.microprofile.openapi.models.media.Schema schema = new SchemaImpl(); + if (schemaType == SchemaType.OBJECT && type.getType() != null && type.getType().equals(clazz)) { schema.setType(null); schema.setItems(null); return schema; } - return createSchema(context, type); + + if (type.getType() == null) { + ParameterizedInterfaceModel classParameterizedType = findParameterizedModelFromGenerics( + clazz, + classParameterizedTypes, + type + ); + String typeName = null; + if (type.getTypeName() != null) { + typeName = type.getTypeName(); + } + if ((typeName == null || Object.class.getName().equals(typeName)) && classParameterizedType != null) { + typeName = classParameterizedType.getRawInterfaceName(); + } + + schemaType = ModelUtils.getSchemaType(typeName, context); + if (schema.getType() == null) { + schema.setType(schemaType); + } + + Schema containerSchema = schema; + if (schemaType == SchemaType.ARRAY) { + containerSchema = new SchemaImpl(); + schema.setItems(containerSchema); + } + if (classParameterizedType != null) { + Collection genericTypes = classParameterizedType.getParametizedTypes(); + if (genericTypes.isEmpty()) { + if (insertObjectReference(context, containerSchema, classParameterizedType.getRawInterface(), classParameterizedType.getRawInterfaceName())) { + containerSchema.setType(null); + containerSchema.setItems(null); + } + } else if (classParameterizedType.getRawInterface() instanceof ClassModel) { + visitSchemaClass(containerSchema, null, (ClassModel) classParameterizedType.getRawInterface(), genericTypes, context); + } else { + LOGGER.log(FINE, "Unrecognised schema {0} class found.", new Object[]{classParameterizedType.getRawInterface()}); + } + } else if (!type.getParameterizedTypes().isEmpty()) { + List genericTypes = type.getParameterizedTypes(); + if (ModelUtils.isMap(typeName, context) && genericTypes.size() == 2) { + createSchema(containerSchema, context, genericTypes.get(0), clazz, classParameterizedTypes); + + containerSchema = new SchemaImpl(); + schema.setAdditionalPropertiesSchema(containerSchema); + createSchema(containerSchema, context, genericTypes.get(1), clazz, classParameterizedTypes); + } else { + createSchema(containerSchema, context, genericTypes.get(0), clazz, classParameterizedTypes); + } + } else { + return createSchema(containerSchema, context, type); + } + return schema; + } + + return createSchema(schema, context, type); + } + + private ParameterizedInterfaceModel findParameterizedModelFromGenerics( + ExtensibleType annotatedElement, + Collection parameterizedModels, + ParameterizedType genericType) { + if (parameterizedModels == null + || parameterizedModels.isEmpty()) { + return null; + } + + List formalParamKeys = new ArrayList<>(annotatedElement.getFormalTypeParameters().keySet()); + int i = 0; + for (ParameterizedInterfaceModel parameterizedModel : parameterizedModels) { + if (formalParamKeys.get(i).equals(genericType.getFormalType())) { + return parameterizedModel; + } + i++; + } + return null; } /** @@ -1060,30 +1174,30 @@ private boolean insertObjectReference(ApiContext context, Reference referee, if (referenceClass != null && referenceClass instanceof ExtensibleType) { ExtensibleType referenceClassType = (ExtensibleType) referenceClass; final AnnotationModel schemaAnnotation = AnnotationInfo.valueOf(referenceClassType) - .getAnnotation(Schema.class); + .getAnnotation(org.eclipse.microprofile.openapi.annotations.media.Schema.class); + String schemaName = null; if (schemaAnnotation != null) { - String schemaName = schemaAnnotation.getValue("name", String.class); - - if (schemaName == null || schemaName.isEmpty()) { - schemaName = ModelUtils.getSimpleName(referenceClassName); - } - // Set the reference name - referee.setRef(schemaName); - - org.eclipse.microprofile.openapi.models.media.Schema schema = context.getApi().getComponents().getSchemas().get(schemaName); - if (schema == null) { - // Create the schema - if (context.isAllowedType(referenceClassType)) { - visitSchema(schemaAnnotation, referenceClass, context); - } else if(referenceClassType instanceof ClassModel) { - apiWalker.processAnnotation((ClassModel)referenceClassType, this); - } else { - LOGGER.log(FINE, "Unrecognised schema {0} class found.", new Object[]{referenceClassName}); - } + schemaName = schemaAnnotation.getValue("name", String.class); + } + if (schemaName == null || schemaName.isEmpty()) { + schemaName = ModelUtils.getSimpleName(referenceClassName); + } + // Set the reference name + referee.setRef(schemaName); + + Schema schema = context.getApi().getComponents().getSchemas().get(schemaName); + if (schema == null) { + // Create the schema + if (context.isAllowedType(referenceClassType)) { + visitSchema(schemaAnnotation, referenceClassType, context); + } else if (referenceClassType instanceof ClassModel) { + apiWalker.processAnnotation((ClassModel) referenceClassType, this); + } else { + LOGGER.log(FINE, "Unrecognised schema {0} class found.", new Object[]{referenceClassName}); } - - return true; } + + return true; } return false; diff --git a/appserver/payara-appserver-modules/microprofile/openapi/src/test/java/fish/payara/microprofile/openapi/test/app/application/GenericSchemaMappingTest.java b/appserver/payara-appserver-modules/microprofile/openapi/src/test/java/fish/payara/microprofile/openapi/test/app/application/GenericSchemaMappingTest.java new file mode 100644 index 00000000000..d9d67a6fd77 --- /dev/null +++ b/appserver/payara-appserver-modules/microprofile/openapi/src/test/java/fish/payara/microprofile/openapi/test/app/application/GenericSchemaMappingTest.java @@ -0,0 +1,188 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) [2020] Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.microprofile.openapi.test.app.application; + +import fish.payara.microprofile.openapi.test.app.OpenApiApplicationTest; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import static javax.ws.rs.core.MediaType.WILDCARD; +import org.eclipse.microprofile.openapi.models.media.Schema; +import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType; +import org.eclipse.microprofile.openapi.models.responses.APIResponses; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; + +/** + * A resource to test that schema classes with generic signature mapped + * correctly. + */ +@Path("/response") +public class GenericSchemaMappingTest extends OpenApiApplicationTest { + + public class JsonData { + + T data; + + public JsonData() { + } + + public JsonData(T data) { + this.data = data; + } + } + + public static class JsonItems { + + List items; + Map itemMap; + Map textMap; + Map> itemsMap; + int totalItems; + + public JsonItems(List items, int totalItems) { + this.items = items; + this.totalItems = totalItems; + } + } + + public class Animal { + + String name; + int age; + + public Animal() { + } + + public Animal(String name, int age) { + this.name = name; + this.age = age; + } + + } + + @org.eclipse.microprofile.openapi.annotations.media.Schema(description = "JSON wrapper for a list of animals") + public class JsonAnimalList extends JsonData> { + + public JsonAnimalList(List items, int totalItems) { + super(new JsonItems(items, totalItems)); + } + } + + @GET + public JsonAnimalList loadAnimals() { + List animals = new ArrayList<>(); + animals.add(new Animal("Leo the tiger", 7)); + animals.add(new Animal("Joe the wolf", 11)); + + return new JsonAnimalList(animals, animals.size()); + } + + @Test + public void genericSchemaTest() { + APIResponses responses = getDocument().getPaths().getPathItem("/test/response").getGET().getResponses(); + assertNotNull("The default response should have been created.", + responses.getDefaultValue()); + assertNotNull("The default response should return */*.", + responses.getDefaultValue().getContent().getMediaType(WILDCARD)); + assertEquals("The default response */* should match the specified schema.", + "#/components/schemas/JsonAnimalList", + responses.getDefaultValue().getContent().getMediaType(WILDCARD).getSchema().getRef()); + + Map schemas = getDocument().getComponents().getSchemas(); + assertEquals(2, schemas.size()); + + Schema jsonAnimalList = schemas.get("JsonAnimalList"); + assertNotNull(jsonAnimalList); + assertEquals(1, jsonAnimalList.getProperties().size()); + assertEquals("JSON wrapper for a list of animals", jsonAnimalList.getDescription()); + assertEquals(SchemaType.OBJECT, jsonAnimalList.getType()); + + Schema data = jsonAnimalList.getProperties().get("data"); + assertNotNull(data); + assertEquals(5, data.getProperties().size()); + assertEquals(SchemaType.OBJECT, data.getType()); + + Schema totalItems = data.getProperties().get("totalItems"); + assertNotNull(totalItems); + assertEquals(SchemaType.INTEGER, totalItems.getType()); + + Schema items = data.getProperties().get("items"); + assertNotNull(items); + assertEquals(SchemaType.ARRAY, items.getType()); + assertEquals("#/components/schemas/Animal", items.getItems().getRef()); + + Schema itemMap = data.getProperties().get("itemMap"); + assertNotNull(itemMap); + assertEquals(SchemaType.OBJECT, itemMap.getType()); + assertNotNull(itemMap.getAdditionalPropertiesSchema()); + assertEquals("#/components/schemas/Animal", itemMap.getAdditionalPropertiesSchema().getRef()); + + Schema textMap = data.getProperties().get("textMap"); + assertNotNull(textMap); + assertEquals(SchemaType.OBJECT, textMap.getType()); + assertNotNull(textMap.getAdditionalPropertiesSchema()); + assertEquals(SchemaType.STRING, textMap.getAdditionalPropertiesSchema().getType()); + + Schema itemsMap = data.getProperties().get("itemsMap"); + assertNotNull(itemsMap); + assertEquals(SchemaType.OBJECT, itemsMap.getType()); + assertNotNull(itemsMap.getAdditionalPropertiesSchema()); + assertEquals(SchemaType.ARRAY, itemsMap.getAdditionalPropertiesSchema().getType()); + assertEquals("#/components/schemas/Animal", itemsMap.getAdditionalPropertiesSchema().getItems().getRef()); + + Schema animal = schemas.get("Animal"); + assertNotNull(animal); + assertEquals(2, animal.getProperties().size()); + assertEquals(SchemaType.OBJECT, animal.getType()); + + Schema name = animal.getProperties().get("name"); + assertNotNull(name); + assertEquals(SchemaType.STRING, name.getType()); + + Schema age = animal.getProperties().get("age"); + assertNotNull(age); + assertEquals(SchemaType.INTEGER, age.getType()); + } + +} diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/server/ApplicationLifecycle.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/server/ApplicationLifecycle.java index 5086e54e23c..a3f030ca180 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/server/ApplicationLifecycle.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/server/ApplicationLifecycle.java @@ -148,6 +148,7 @@ import static java.util.stream.Collectors.toMap; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.glassfish.hk2.classmodel.reflect.util.ParsingConfig; /** * Application Loader is providing useful methods to load applications @@ -671,8 +672,25 @@ public Types getDeployableTypes(DeploymentContext context) throws IOException { try { ResourceLocator locator = determineLocator(); // scan the jar and store the result in the deployment context. - ParsingContext.Builder parsingContextBuilder = new ParsingContext.Builder().logger(context.getLogger()) - .executorService(executorService.getUnderlyingExecutorService()); + ParsingContext.Builder parsingContextBuilder = new ParsingContext.Builder() + .logger(context.getLogger()) + .executorService(executorService.getUnderlyingExecutorService()) + .config(new ParsingConfig() { + @Override + public Set getAnnotationsOfInterest() { + return Collections.emptySet(); + } + + @Override + public Set getTypesOfInterest() { + return Collections.emptySet(); + } + + @Override + public boolean modelUnAnnotatedMembers() { + return true; + } + }); // workaround bug in Builder parsingContextBuilder.locator(locator); ParsingContext parsingContext = parsingContextBuilder.build(); diff --git a/pom.xml b/pom.xml index 800794d83b4..5e745844ec1 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,7 @@ 1.1.1 1.6.4.payara-p1 1.3.5 - 2.6.1.payara-p2 + 2.6.1.payara-p3 1.0.3 1.0-2 2.10.2