From 9a6ee8e27fbd1341216ad5d25c4927a5bccf8acd Mon Sep 17 00:00:00 2001 From: mherman22 Date: Tue, 3 Dec 2024 14:04:38 +0300 Subject: [PATCH] RESTWS-963: Add a swagger autogeneration util to scan resource handlers and generate the swagger spec --- .../UnrelatedGenericChildResource.java | 31 +- .../doc/SwaggerSpecificationCreatorTest.java | 80 ++- .../rest/resource/SubDetailsResource.java | 7 +- .../docs/swagger/SwaggerGenerationUtil.java | 469 ++++++++++++++++++ .../swagger/SwaggerSpecificationCreator.java | 13 +- .../webservices/rest/web/api/RestService.java | 9 +- .../rest/web/api/impl/RestServiceImpl.java | 15 +- .../rest/web/SwaggerGenerationUtilTest.java | 401 +++++++++++++++ 8 files changed, 995 insertions(+), 30 deletions(-) create mode 100644 omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerGenerationUtil.java create mode 100644 omod-common/src/test/java/org/openmrs/module/webservices/rest/web/SwaggerGenerationUtilTest.java diff --git a/omod-2.0/src/test/java/org/openmrs/module/unrelatedtest/rest/resource/UnrelatedGenericChildResource.java b/omod-2.0/src/test/java/org/openmrs/module/unrelatedtest/rest/resource/UnrelatedGenericChildResource.java index 29377bcc9..d6f1fecd5 100644 --- a/omod-2.0/src/test/java/org/openmrs/module/unrelatedtest/rest/resource/UnrelatedGenericChildResource.java +++ b/omod-2.0/src/test/java/org/openmrs/module/unrelatedtest/rest/resource/UnrelatedGenericChildResource.java @@ -9,49 +9,50 @@ */ package org.openmrs.module.unrelatedtest.rest.resource; -import io.swagger.models.Model; import org.openmrs.module.unrelatedtest.UnrelatedGenericChild; import org.openmrs.module.webservices.rest.doc.SwaggerSpecificationCreatorTest; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.annotation.Resource; import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; import org.openmrs.module.webservices.rest.web.v1_0.test.GenericChildResource; /** * A test resource that is unrelated to the main webservices package. - * + * * @see SwaggerSpecificationCreatorTest#testUnrelatedResourceDefinitions() */ @Resource(name = RestConstants.VERSION_1 + "/unrelated", supportedClass = UnrelatedGenericChild.class, supportedOpenmrsVersions = { "1.9.* - 9.*" }) public class UnrelatedGenericChildResource extends GenericChildResource { - + public static boolean getGETCalled = false; - + public static boolean getCREATECalled = false; - + public static boolean getUPDATECalled = false; - + /******************************* * TEST METHOD IMPLEMENTATIONS * These methods are the ones we want to test against. There * implementaion is unimportant, they just set flags so we can assert the methods were called * correctly by the reflector. */ - + @Override - public Model getGETModel(Representation rep) { + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { getGETCalled = true; - return super.getGETModel(rep); + return super.getRepresentationDescription(rep); } - + @Override - public Model getCREATEModel(Representation rep) { + public DelegatingResourceDescription getCreatableProperties() { getCREATECalled = true; - return super.getCREATEModel(rep); + return super.getCreatableProperties(); } - + @Override - public Model getUPDATEModel(Representation rep) { + public DelegatingResourceDescription getUpdatableProperties() throws ResourceDoesNotSupportOperationException { getUPDATECalled = true; - return super.getUPDATEModel(rep); + return super.getUpdatableProperties(); } } diff --git a/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/doc/SwaggerSpecificationCreatorTest.java b/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/doc/SwaggerSpecificationCreatorTest.java index 8d95d79d8..fc95c8bec 100644 --- a/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/doc/SwaggerSpecificationCreatorTest.java +++ b/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/doc/SwaggerSpecificationCreatorTest.java @@ -15,12 +15,18 @@ import io.swagger.jackson.ModelResolver; import io.swagger.models.Info; import io.swagger.models.Model; +import io.swagger.models.ModelImpl; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.Scheme; import io.swagger.models.Swagger; import io.swagger.models.auth.BasicAuthDefinition; import io.swagger.models.parameters.Parameter; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.DateProperty; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import io.swagger.models.properties.StringProperty; import io.swagger.util.Json; import org.dbunit.database.DatabaseConnection; import org.junit.Assert; @@ -30,9 +36,14 @@ import org.openmrs.Patient; import org.openmrs.api.context.Context; import org.openmrs.module.unrelatedtest.rest.resource.UnrelatedGenericChildResource; +import org.openmrs.module.webservices.docs.swagger.SwaggerGenerationUtil; import org.openmrs.module.webservices.docs.swagger.SwaggerSpecificationCreator; import org.openmrs.module.webservices.rest.web.RestConstants; import org.openmrs.module.webservices.rest.web.api.RestService; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.v1_0.resource.openmrs1_8.ObsResource1_8; +import org.openmrs.module.webservices.rest.web.v1_0.resource.openmrs1_8.PatientResource1_8; +import org.openmrs.module.webservices.rest.web.v1_0.resource.openmrs1_8.PersonResource1_8; import org.openmrs.web.test.BaseModuleWebContextSensitiveTest; import java.lang.reflect.Field; @@ -49,6 +60,7 @@ import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; public class SwaggerSpecificationCreatorTest extends BaseModuleWebContextSensitiveTest { @@ -270,7 +282,7 @@ public void createOnlySubresourceDefinitions() { assertFalse(json.contains("SystemsettingSubdetailsUpdate")); assertTrue(json.contains("SystemsettingSubdetailsCreate")); } - + /** * Ensure that resources not directly related to the webservices.rest package are successfully * defined in the swagger documentation. @@ -281,24 +293,82 @@ public void testUnrelatedResourceDefinitions() { UnrelatedGenericChildResource.getGETCalled = false; UnrelatedGenericChildResource.getCREATECalled = false; UnrelatedGenericChildResource.getUPDATECalled = false; - + // make sure to reset the cache for multiple tests in the same run if (SwaggerSpecificationCreator.isCached()) { SwaggerSpecificationCreator.clearCache(); } - + SwaggerSpecificationCreator ssc = new SwaggerSpecificationCreator(); ssc.getJSON(); - + // check our custom methods were called assertTrue(UnrelatedGenericChildResource.getGETCalled); assertTrue(UnrelatedGenericChildResource.getCREATECalled); assertTrue(UnrelatedGenericChildResource.getUPDATECalled); - + // assert the definition is now in the swagger object Swagger swagger = ssc.getSwagger(); assertTrue(swagger.getDefinitions().containsKey("UnrelatedGet")); assertTrue(swagger.getDefinitions().containsKey("UnrelatedUpdate")); assertTrue(swagger.getDefinitions().containsKey("UnrelatedCreate")); } + + @Test + public void generateGETModel_shouldCheckForOpenMRSResource() { + Model model = SwaggerGenerationUtil.generateGETModel(new ObsResource1_8(), Representation.DEFAULT); + Assert.assertTrue(model instanceof ModelImpl); + + Map propertyMap = model.getProperties(); + Assert.assertTrue(propertyMap.containsKey("location")); + Assert.assertTrue(propertyMap.containsKey("person")); + Assert.assertTrue(propertyMap.containsKey("obsDatetime")); + Assert.assertTrue(propertyMap.containsKey("accessionNumber")); + + Assert.assertTrue(propertyMap.get("location") instanceof RefProperty); + Assert.assertTrue(propertyMap.get("person") instanceof RefProperty); + Assert.assertTrue(propertyMap.get("obsDatetime") instanceof DateProperty); + Assert.assertTrue(propertyMap.get("accessionNumber") instanceof StringProperty); + + Property property = propertyMap.get("encounter"); + Assert.assertTrue(property instanceof RefProperty); + RefProperty stringProperty = (RefProperty) property; + assertEquals("#/definitions/EncounterGet", stringProperty.get$ref()); + } + + @Test + public void generateGETModel_shouldReturnAnArrayPropertyWithRefPropertyWhenFieldIsASet() { + Model model = SwaggerGenerationUtil.generateGETModel(new PersonResource1_8(), Representation.DEFAULT); + Assert.assertTrue(model instanceof ModelImpl); + + Map propertyMap = model.getProperties(); + System.out.println(propertyMap); + Assert.assertTrue(propertyMap.containsKey("attributes")); + + Property property = propertyMap.get("attributes"); + Assert.assertTrue(property instanceof ArrayProperty); + ArrayProperty arrayProperty = (ArrayProperty) property; + Assert.assertTrue(arrayProperty.getItems() instanceof RefProperty); + + RefProperty refProperty = (RefProperty) arrayProperty.getItems(); + assertEquals("#/definitions/PersonAttributeGet", refProperty.get$ref()); + } + + @Test + public void generateGETModelPatient_shouldReturnAnArrayPropertyWithRefPropertyWhenFieldIsASet() { + Model model = SwaggerGenerationUtil.generateGETModel(new PatientResource1_8(), Representation.DEFAULT); + Assert.assertTrue(model instanceof ModelImpl); + + Map propertyMap = model.getProperties(); + System.out.println(propertyMap); + Assert.assertTrue(propertyMap.containsKey("identifiers")); + + Property property = propertyMap.get("identifiers"); + Assert.assertTrue(property instanceof ArrayProperty); + ArrayProperty arrayProperty = (ArrayProperty) property; + Assert.assertTrue(arrayProperty.getItems() instanceof RefProperty); + + RefProperty refProperty = (RefProperty) arrayProperty.getItems(); + assertEquals("#/definitions/PatientIdentifierGet", refProperty.get$ref()); + } } diff --git a/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/resource/SubDetailsResource.java b/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/resource/SubDetailsResource.java index 26f76a9df..62671ebe7 100644 --- a/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/resource/SubDetailsResource.java +++ b/omod-2.0/src/test/java/org/openmrs/module/webservices/rest/resource/SubDetailsResource.java @@ -63,7 +63,12 @@ public void purge(SubDetails delegate, RequestContext context) throws ResponseEx public DelegatingResourceDescription getRepresentationDescription(Representation rep) { return new DelegatingResourceDescription(); } - + + @Override + public DelegatingResourceDescription getCreatableProperties() throws ResourceDoesNotSupportOperationException { + return new DelegatingResourceDescription(); + } + @Override public SubDetails newDelegate() { return new SubDetails(); diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerGenerationUtil.java b/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerGenerationUtil.java new file mode 100644 index 000000000..f3ab6e6ec --- /dev/null +++ b/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerGenerationUtil.java @@ -0,0 +1,469 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.webservices.docs.swagger; + +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BooleanProperty; +import io.swagger.models.properties.DateProperty; +import io.swagger.models.properties.DoubleProperty; +import io.swagger.models.properties.IntegerProperty; +import io.swagger.models.properties.ObjectProperty; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import io.swagger.models.properties.StringProperty; +import org.apache.commons.lang.StringUtils; +import org.openmrs.api.context.Context; +import org.openmrs.module.webservices.docs.swagger.core.property.EnumProperty; +import org.openmrs.module.webservices.rest.web.annotation.Resource; +import org.openmrs.module.webservices.rest.web.annotation.SubResource; +import org.openmrs.module.webservices.rest.web.api.RestService; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.openmrs.module.webservices.rest.web.representation.Representation.DEFAULT; +import static org.openmrs.module.webservices.rest.web.representation.Representation.FULL; +import static org.openmrs.module.webservices.rest.web.representation.Representation.REF; + +/** + *

+ * This class provides methods to dynamically generate schemas for GET, CREATE, and UPDATE operations + * based on the resource's representations and properties. It maps Java types and OpenMRS-specific + * resource definitions to Swagger-compatible schema definitions. + *

+ *

This class is designed to work with {@link DelegatingResourceHandler} implementations, ensuring that + * models dynamically adapt to the properties and representations defined for each resource. It keeps track of + * {@link DelegatingResourceHandler#getRepresentationDescription(Representation)}, {@link DelegatingResourceHandler#getCreatableProperties()} + * {@link DelegatingResourceHandler#getUpdatableProperties()} methods that are used to create the rest api

+ */ +public class SwaggerGenerationUtil { + + private static final Logger logger = LoggerFactory.getLogger(SwaggerGenerationUtil.class); + + private static final Map, DelegatingResourceHandler> resourceHandlers = new HashMap, DelegatingResourceHandler>(); + + public static void addResourceHandler(DelegatingResourceHandler resourceHandler) { + resourceHandlers.put(resourceHandler.getClass(), resourceHandler); + } + + /** + * Generates the model for GET operations. + * + * @param resourceHandler the resource handler for the resource + * @param representation the representation type (DEFAULT, REF, FULL) + * @return the generated schema + * @throws IllegalArgumentException if the representation is unsupported + */ + public static Model generateGETModel(DelegatingResourceHandler resourceHandler, Representation representation) { + ModelImpl model = new ModelImpl(); + + if (representation.equals(DEFAULT)) { + model = addDefaultProperties(resourceHandler); + } else if (representation.equals(REF)) { + model = addRefProperties(resourceHandler); + } else if (representation.equals(FULL)) { + model = addFullProperties(resourceHandler); + } else { + throw new IllegalArgumentException("Unsupported representation: " + representation); + } + + return model; + } + + /** + * Generates the model for CREATE operations. + * + * @param resourceHandler the resource handler for the resource + * @param representation the representation type (DEFAULT, FULL) + * @return the generated schema + * @throws IllegalArgumentException if the representation is unsupported + */ + public static Model generateCREATEModel(DelegatingResourceHandler resourceHandler, Representation representation) { + ModelImpl model = new ModelImpl(); + + if (representation.equals(DEFAULT)) { + model = addCreatableProperties(resourceHandler, "Create"); + } else if (representation.equals(FULL)) { + model = addCreatableProperties(resourceHandler, "CreateFull"); + } else { + throw new IllegalArgumentException("Unsupported representation: " + representation); + } + + return model; + } + + /** + * Generates the model for UPDATE operations. + * + * @param resourceHandler the resource handler for the resource + * @param representation the representation type (DEFAULT) + * @return the generated schema + * @throws IllegalArgumentException if the representation is unsupported + */ + public static Model generateUPDATEModel(DelegatingResourceHandler resourceHandler, Representation representation) { + ModelImpl model = new ModelImpl(); + + if (representation.equals(DEFAULT)) { + model = addUpdatableProperties(resourceHandler); + } else { + throw new IllegalArgumentException("Unsupported representation: " + representation); + } + + return model; + } + + /** + * Adds creatable properties to the schema based on the resource handler's creatable properties. + * + * @param resourceHandler the resource handler for the resource + * @return the updated schema with creatable properties + */ + private static ModelImpl addCreatableProperties(DelegatingResourceHandler resourceHandler, String operationType) { + ModelImpl model = new ModelImpl(); + addResourceHandler(resourceHandler); + DelegatingResourceDescription description = resourceHandler.getCreatableProperties(); + if (description != null) { + for (String property : description.getProperties().keySet()) { + model.property(property, determinePropertyForField(resourceHandler, property, operationType)); + } + } + + return model; + } + + /** + * Adds updatable properties to the schema based on the resource handler's updatable properties. + * + * @param resourceHandler the resource handler for the resource + * @return the updated schema with updatable properties + */ + private static ModelImpl addUpdatableProperties(DelegatingResourceHandler resourceHandler) { + ModelImpl model = new ModelImpl(); + addResourceHandler(resourceHandler); + DelegatingResourceDescription description = resourceHandler.getUpdatableProperties(); + if (description != null) { + for (String property : description.getProperties().keySet()) { + model.property(property, determinePropertyForField(resourceHandler, property, "Update")); + } + } + + return model; + } + + /** + * Adds default properties to the schema based on the resource handler's DEFAULT representation. + * + * @param resourceHandler the resource handler for the resource + * @return the updated schema with default properties + */ + private static ModelImpl addDefaultProperties(DelegatingResourceHandler resourceHandler) { + ModelImpl model = new ModelImpl(); + addResourceHandler(resourceHandler); + model.property("uuid", new StringProperty().description("Unique identifier of the resource")); + model.property("display", new StringProperty().description("Display name of the resource")); + + DelegatingResourceDescription description = resourceHandler.getRepresentationDescription(DEFAULT); + if (description != null) { + for (String property : description.getProperties().keySet()) { + model.property(property, determinePropertyForField(resourceHandler, property, "Get")); + } + } + + return model; + } + + /** + * Adds reference properties to the schema based on the resource handler's REF representation. + * + * @param resourceHandler the resource handler for the resource + * @return the updated schema with reference properties + */ + private static ModelImpl addRefProperties(DelegatingResourceHandler resourceHandler) { + ModelImpl model = new ModelImpl(); + addResourceHandler(resourceHandler); + model.property("uuid", new StringProperty().description("Unique identifier of the resource")); + model.property("display", new StringProperty().description("Display name of the resource")); + + DelegatingResourceDescription description = resourceHandler.getRepresentationDescription(REF); + if (description != null) { + for (String property : description.getProperties().keySet()) { + model.property(property, determinePropertyForField(resourceHandler, property, "GetRef")); + } + } + + return model; + } + + /** + * Adds full properties to the schema based on the resource handler's FULL representation. + * + * @param resourceHandler the resource handler for the resource + * @return the updated schema with full properties + */ + private static ModelImpl addFullProperties(DelegatingResourceHandler resourceHandler) { + ModelImpl model = new ModelImpl(); + addResourceHandler(resourceHandler); + + DelegatingResourceDescription description = resourceHandler.getRepresentationDescription(FULL); + if (description != null) { + for (String property : description.getProperties().keySet()) { + model.property(property, determinePropertyForField(resourceHandler, property, "GetFull")); + } + } + + return model; + } + + /** + * Determines the property for a field based on the resource handler and property name. + * + * @param resourceHandler the resource handler for the resource + * @param propertyName the name of the property + * @param operationType the type of operation + * @return the property for the field + */ + public static Property determinePropertyForField(DelegatingResourceHandler resourceHandler, String propertyName, String operationType) { + Class genericType = getGenericType(resourceHandler.getClass()); + if (genericType == null) { + // Worst case scenario, no parameterized superclass / interface found in the class hierarchy + throw new IllegalArgumentException("No generic type for resource handler"); + } + + try { + Field field = genericType.getDeclaredField(propertyName); + return createPropertyForType(field.getType(), operationType, field); + } catch (NoSuchFieldException e) { + logger.warn("Field {} not found in class {}", propertyName, genericType.getName()); + return new StringProperty(); + } + } + + /** + * Maps Java types to their corresponding Swagger properties. + * + * @param type the Java class type + * @param operationType the type of operation (e.g., "Create", "Get") + * @param field the field to generate the property for + * @return the Swagger property + */ + @SuppressWarnings("unchecked") + public static Property createPropertyForType(Class type, String operationType, Field field) { + if (String.class.equals(type)) { + return new StringProperty(); + } else if (Integer.class.equals(type) || int.class.equals(type)) { + return new IntegerProperty(); + } else if (Boolean.class.equals(type) || boolean.class.equals(type)) { + return new BooleanProperty(); + } else if (UUID.class.equals(type)) { + return new StringProperty().description("uuid"); + } else if (java.util.Date.class.equals(type)) { + return new DateProperty(); + } else if (Double.class.equals(type)) { + return new DoubleProperty(); + } else if (isOpenMRSResource(type)) { + if (type.isEnum()) { + return new EnumProperty((Class>) type); + } else { + String resourceName = getResourceNameBySupportedClass(type); + if (resourceName == null) { + return new StringProperty(); + } + return new RefProperty("#/definitions/" + StringUtils.capitalize(resourceName) + operationType); + } + } else if (Set.class.equals(type) || List.class.equals(type)) { + Class elementType = getGenericTypeFromField(field); + if (isOpenMRSResource(elementType)) { + String resourceName = getSubResourceNameBySupportedClass(elementType); + if (resourceName == null) { + return new StringProperty(); + } + return new ArrayProperty(new RefProperty("#/definitions/" + StringUtils.capitalize(resourceName) + operationType)); + } + return new ArrayProperty(); + } else { + return new ObjectProperty(); + } + } + + /** + * Retrieves the name of a resource or sub-resource associated with a given class. + * + * @param supportedClass the class to find the resource name for + * @return the name of the {@link Resource} or {@link SubResource} associated with the given class, + * or "null" if no match is found + */ + public static String getResourceNameBySupportedClass(Class supportedClass) { + for (DelegatingResourceHandler resourceHandler : resourceHandlers.values()) { + Resource annotation = resourceHandler.getClass().getAnnotation(Resource.class); + SubResource subResourceAnnotation = resourceHandler.getClass().getAnnotation(SubResource.class); + + if (annotation != null && annotation.supportedClass().equals(supportedClass)) { + return annotation.name().substring(annotation.name().indexOf('/') + 1); + } else if (subResourceAnnotation != null && subResourceAnnotation.supportedClass().equals(supportedClass)) { + Resource parentResourceAnnotation = subResourceAnnotation.parent().getAnnotation(Resource.class); + + String resourceName = subResourceAnnotation.path(); + String resourceParentName = parentResourceAnnotation.name().substring( + parentResourceAnnotation.name().indexOf('/') + 1); + + String combinedName = capitalize(resourceParentName) + capitalize(resourceName); + return combinedName.replace("/", ""); + } + } + return null; + } + + public static String getSubResourceNameBySupportedClass(Class supportedClass) { + org.openmrs.module.webservices.rest.web.resource.api.Resource resource = Context.getService(RestService.class).getResourceHandlerForSupportedClass(supportedClass); + + if (resource == null) { + return null; + } + + Resource annotation = resource.getClass().getAnnotation(Resource.class); + SubResource subResourceAnnotation = resource.getClass().getAnnotation(SubResource.class); + + if (annotation != null && annotation.supportedClass().equals(supportedClass)) { + return annotation.name().substring(annotation.name().indexOf('/') + 1); + } else if (subResourceAnnotation != null && subResourceAnnotation.supportedClass().equals(supportedClass)) { + Resource parentResourceAnnotation = subResourceAnnotation.parent().getAnnotation(Resource.class); + + String resourceName = subResourceAnnotation.path(); + String resourceParentName = parentResourceAnnotation.name().substring( + parentResourceAnnotation.name().indexOf('/') + 1); + + String combinedName = capitalize(resourceParentName) + capitalize(resourceName); + return combinedName.replace("/", ""); + } + return null; + } + + public static String capitalize(String name) { + if (name == null || name.isEmpty()) return name; + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + + /** + * Checks whether a class is an OpenMRS resource (e.g., references an OpenMRS data object). + * + * @param type the class to check + * @return true if the class represents an OpenMRS resource, false otherwise + */ + private static boolean isOpenMRSResource(Class type) { + if (type == null) { + return false; + } + + Package pkg = type.getPackage(); + return pkg != null && pkg.getName().startsWith("org.openmrs"); + } + + public static String getModelName(String qualifiedName) { + if (qualifiedName == null || !qualifiedName.contains(".")) { + return qualifiedName; + } + + String simpleName = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1); + simpleName = simpleName.replace("$", ""); + return simpleName.substring(0, 1).toUpperCase() + simpleName.substring(1); + } + + /** + * Extracts the generic type argument of a field that represents a parameterized collection (e.g., List, Set). + * If the field is not parameterized or the generic type cannot be determined, it returns {@code null}. + * + * @param field the field whose generic type is to be determined + * @return the {@link Class} object representing the generic type parameter, + * or {@code null} if the field is not parameterized or the type cannot be resolved + */ + private static Class getGenericTypeFromField(Field field) { + try { + if (field.getGenericType() instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length > 0 && typeArguments[0] instanceof Class) { + return (Class) typeArguments[0]; + } + } + } catch (Exception e) { + logger.warn("Could not determine the generic type for field: {}. This may not affect functionality.", field.getName(), e); + } + return null; + } + + /** + * Extracts the generic type parameter from a specified class or its superclasses + * that implement a parameterized interface or extend a parameterized class. + *

+ * This method traverses the inheritance hierarchy of the provided class to locate + * a generic type declaration. It examines both parameterized interfaces and + * parameterized superclasses, returning the first generic type parameter found. + * If no generic type can be determined, the method returns {@code null}. + *

+ *

Example usage:

+ *
{@code
+     * public class PatientResource1_8 extends DelegatingResourceHandler {
+     *     // ...snip
+     * }
+     *
+     * Class genericType = GenericTypeUtils.getGenericType(PatientResource1_8.class);
+     * System.out.println(genericType); // Output: class Patient
+     * }
+ * + * @param resourceHandlerClass the class implementing or extending a parameterized type + * @return the {@link Class} representing the generic type parameter, or {@code null} + * if the generic type cannot be determined + * @throws NullPointerException if {@code resourceHandlerClass} is {@code null} + */ + public static Class getGenericType(Class resourceHandlerClass) { + Class currentClass = resourceHandlerClass; + + while (currentClass != null) { + Type[] genericInterfaces = currentClass.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericInterface; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + + if (typeArguments.length > 0 && typeArguments[0] instanceof Class) { + return (Class) typeArguments[0]; + } + } + } + + Type genericSuperclass = currentClass.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + + if (typeArguments.length > 0 && typeArguments[0] instanceof Class) { + return (Class) typeArguments[0]; + } + } + + currentClass = currentClass.getSuperclass(); + } + + return null; + } +} diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerSpecificationCreator.java b/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerSpecificationCreator.java index d0232bf2f..26559f78a 100644 --- a/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerSpecificationCreator.java +++ b/omod-common/src/main/java/org/openmrs/module/webservices/docs/swagger/SwaggerSpecificationCreator.java @@ -999,14 +999,14 @@ private void createDefinition(OperationEnum operationEnum, String resourceName, Model modelFull = null; if (definitionName.endsWith("Get")) { - model = resourceHandler.getGETModel(Representation.DEFAULT); - modelRef = resourceHandler.getGETModel(Representation.REF); - modelFull = resourceHandler.getGETModel(Representation.FULL); + model = SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.DEFAULT); + modelRef = SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.REF); + modelFull = SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.FULL); } else if (definitionName.endsWith("Create")) { - model = resourceHandler.getCREATEModel(Representation.DEFAULT); - modelFull = resourceHandler.getCREATEModel(Representation.FULL); + model = SwaggerGenerationUtil.generateCREATEModel(resourceHandler, Representation.DEFAULT); + modelFull = SwaggerGenerationUtil.generateCREATEModel(resourceHandler, Representation.FULL); } else if (definitionName.endsWith("Update")) { - model = resourceHandler.getUPDATEModel(Representation.DEFAULT); + model = SwaggerGenerationUtil.generateUPDATEModel(resourceHandler, Representation.DEFAULT); } if (model != null) { @@ -1025,7 +1025,6 @@ private void createDefinition(OperationEnum operationEnum, String resourceName, * @param operationName get, post, delete * @param resourceName * @param resourceParentName - * @param representation * @param operationEnum * @return */ diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/RestService.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/RestService.java index 4922db767..d1a1f95b4 100644 --- a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/RestService.java +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/RestService.java @@ -73,7 +73,14 @@ public interface RestService { * @throws APIException */ public List> getResourceHandlers() throws APIException; - + + + /** + * @param resourceClass the resource class e.g PatientIdentifier.class + * @return the resource handler for the provided class e.g PatientIdentifierResource1_8 + */ + Resource getResourceHandlerForSupportedClass(Class resourceClass); + /** * Initializes all Resources and Search handlers for use; called after module startup */ diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/RestServiceImpl.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/RestServiceImpl.java index 9ffc4c06e..02ab424ea 100644 --- a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/RestServiceImpl.java +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/RestServiceImpl.java @@ -442,7 +442,7 @@ public Resource getResourceBySupportedClass(Class resourceClass) throws APIEx if (HibernateProxy.class.isAssignableFrom(resourceClass)) { resourceClass = resourceClass.getSuperclass(); } - + Resource resource = resourcesBySupportedClasses.get(resourceClass); if (resource == null) { @@ -689,6 +689,19 @@ public Set getSearchHandlers(String resourceName) { } return searchHandlersByResource.get(resourceName); } + + /** + * @see RestService#getResourceHandlerForSupportedClass(Class) + * Should return search resources for given resource class + * @param resourceClass the resource class e.g. PatientIdentifier + */ + @Override + public Resource getResourceHandlerForSupportedClass(Class resourceClass) { + if (resourcesBySupportedClasses == null) { + initializeResources(); + } + return resourcesBySupportedClasses.get(resourceClass); + } /** * @see RestService#initialize() diff --git a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/SwaggerGenerationUtilTest.java b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/SwaggerGenerationUtilTest.java new file mode 100644 index 000000000..44d7bc42c --- /dev/null +++ b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/SwaggerGenerationUtilTest.java @@ -0,0 +1,401 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.webservices.rest.web; + +import io.swagger.models.Model; +import io.swagger.models.ModelImpl; +import io.swagger.models.properties.ArrayProperty; +import io.swagger.models.properties.BooleanProperty; +import io.swagger.models.properties.IntegerProperty; +import io.swagger.models.properties.Property; +import io.swagger.models.properties.RefProperty; +import io.swagger.models.properties.StringProperty; +import org.junit.Test; +import org.openmrs.Concept; +import org.openmrs.Person; +import org.openmrs.User; +import org.openmrs.module.webservices.docs.swagger.SwaggerGenerationUtil; +import org.openmrs.module.webservices.docs.swagger.core.property.EnumProperty; +import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation; +import org.openmrs.module.webservices.rest.web.representation.FullRepresentation; +import org.openmrs.module.webservices.rest.web.representation.RefRepresentation; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.resource.api.PageableResult; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingSubResource; +import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; +import org.openmrs.module.webservices.rest.web.response.ResponseException; +import org.openmrs.web.test.BaseModuleWebContextSensitiveTest; + +import java.lang.reflect.Field; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class SwaggerGenerationUtilTest extends BaseModuleWebContextSensitiveTest { + + @Test + public void genericType_shouldReturnClassFromParameterizedSuperclass() { + class StringHandler extends BaseHandler {} + Class result = SwaggerGenerationUtil.getGenericType(StringHandler.class); + + assertEquals(String.class, result); + } + + @Test + public void genericType_shouldReturnTheFirstParameterOfDelegatingSubResourceForSubResource() { + Class genericType = SwaggerGenerationUtil.getGenericType(SampleSubResourceHandler.class); + + assertNotNull(genericType); + assertEquals(SampleSubResource.class, genericType); + } + + @Test + public void genericType_shouldReturnClassFromParameterizedInterface() { + class InterfaceHandler implements ParameterizedInterface {} + Class result = SwaggerGenerationUtil.getGenericType(InterfaceHandler.class); + + assertEquals(Integer.class, result); + } + + @Test + public void genericType_shouldReturnNullWhenGivenNonGenericClass() { + class NonGenericHandler {} + Class result = SwaggerGenerationUtil.getGenericType(NonGenericHandler.class); + assertNull(result); + } + + @Test + public void genericType_shouldReturnNullWhenGivenNullClass() { + Class result = SwaggerGenerationUtil.getGenericType(null); + assertNull(result); + } + + @Test + public void genericType_shouldReturnClassFromDeepInheritance() { + class IntermediateHandler extends BaseHandler {} + class ConcreteHandler extends IntermediateHandler {} + + Class result = SwaggerGenerationUtil.getGenericType(ConcreteHandler.class); + assertEquals(Double.class, result); + } + + @Test + public void determinePropertyForField_shouldReturnStringPropertyWhenGivenValidStringField() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Property property = SwaggerGenerationUtil.determinePropertyForField(resourceHandler, "name", "GET"); + + assertTrue(property instanceof StringProperty); + } + + @Test + public void determinePropertyForField_shouldReturnIntegerPropertyWhenGivenValidIntegerField() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Property property = SwaggerGenerationUtil.determinePropertyForField(resourceHandler, "age", "GET"); + + assertTrue(property instanceof IntegerProperty); + } + + @Test + public void determinePropertyForField_shouldReturnUnknownWhenGivenNonExistentFieldUnknownField() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Property property = SwaggerGenerationUtil.determinePropertyForField(resourceHandler, "nonExistentField", "GET"); + + assertTrue(property instanceof StringProperty); + } + + @Test + public void generateGETModel_shouldGenerateGETModelWhenGivenDefaultRepresentation() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Model model = SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.DEFAULT); + + assertTrue(model instanceof ModelImpl); + + Map propertyMap = model.getProperties(); + + assertTrue(propertyMap.containsKey("name")); + assertTrue(propertyMap.containsKey("age")); + assertTrue(propertyMap.containsKey("isActive")); + assertTrue(propertyMap.containsKey("action")); + + assertTrue(propertyMap.get("name") instanceof StringProperty); + assertTrue(propertyMap.get("age") instanceof IntegerProperty); + assertTrue(propertyMap.get("isActive") instanceof BooleanProperty); + assertTrue(propertyMap.get("action") instanceof EnumProperty); + } + + @Test + public void generateGETModel_shouldGenerateGETModelWhenGivenRefRepresentation() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Model model = SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.REF); + + assertTrue(model instanceof ModelImpl); + + Map propertyMap = model.getProperties(); + + assertTrue(propertyMap.containsKey("name")); + assertTrue(propertyMap.containsKey("age")); + assertNotEquals(propertyMap.containsKey("isActive"), true); + assertNotEquals(propertyMap.containsKey("action"), true); + + assertTrue(propertyMap.get("name") instanceof StringProperty); + assertTrue(propertyMap.get("age") instanceof IntegerProperty); + } + + @Test + public void generateCREATEModel_shouldGenerateCREATEModelWhenGivenDefaultRepresentation() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Model model = SwaggerGenerationUtil.generateCREATEModel(resourceHandler, Representation.DEFAULT); + + assertTrue(model instanceof ModelImpl); + Map propertyMap = model.getProperties(); + + assertTrue(propertyMap.containsKey("name")); + assertTrue(propertyMap.containsKey("age")); + assertTrue(propertyMap.containsKey("isActive")); + assertTrue(propertyMap.containsKey("action")); + + assertTrue(propertyMap.get("name") instanceof StringProperty); + assertTrue(propertyMap.get("age") instanceof IntegerProperty); + assertTrue(propertyMap.get("isActive") instanceof BooleanProperty); + assertTrue(propertyMap.get("action") instanceof EnumProperty); + } + + @Test + public void generateCREATEModel_shouldGenerateCREATEModelWhenGivenFullRepresentation() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Model model = SwaggerGenerationUtil.generateCREATEModel(resourceHandler, Representation.FULL); + + assertTrue(model instanceof ModelImpl); + Map propertyMap = model.getProperties(); + + assertTrue(propertyMap.containsKey("name")); + assertTrue(propertyMap.containsKey("age")); + assertTrue(propertyMap.containsKey("isActive")); + assertTrue(propertyMap.containsKey("action")); + + assertTrue(propertyMap.get("name") instanceof StringProperty); + assertTrue(propertyMap.get("age") instanceof IntegerProperty); + assertTrue(propertyMap.get("isActive") instanceof BooleanProperty); + assertTrue(propertyMap.get("action") instanceof EnumProperty); + } + + @Test + public void generateUPDATEModel_shouldGenerateUPDATEModelWhenGivenDefaultRepresentation() { + DelegatingResourceHandler resourceHandler = new SampleResourceHandler(); + Model model = SwaggerGenerationUtil.generateUPDATEModel(resourceHandler, Representation.DEFAULT); + + assertTrue(model instanceof ModelImpl); + Map propertyMap = model.getProperties(); + + assertTrue(propertyMap.containsKey("name")); + assertTrue(propertyMap.containsKey("age")); + assertTrue(propertyMap.containsKey("isActive")); + assertTrue(propertyMap.containsKey("action")); + + assertTrue(propertyMap.get("name") instanceof StringProperty); + assertTrue(propertyMap.get("age") instanceof IntegerProperty); + assertTrue(propertyMap.get("isActive") instanceof BooleanProperty); + assertTrue(propertyMap.get("action") instanceof EnumProperty); + } + + @Test + public void createPropertyForType_shouldReturnListOfEnumsWhenGivenAnOutterNestedEnum() throws NoSuchFieldException { + Field actionField = SampleResourceEnum.class.getDeclaredField("sampleResourceOutterEnum"); + Property property = SwaggerGenerationUtil.createPropertyForType(actionField.getType(), "Get", actionField); + + assertTrue(property instanceof StringProperty); + StringProperty stringProperty = (StringProperty) property; + assertNotNull(stringProperty.getEnum()); + + assertTrue(stringProperty.getEnum().contains("CREATE")); + assertTrue(stringProperty.getEnum().contains("PATCH")); + assertTrue(stringProperty.getEnum().contains("UPDATE")); + } + + @Test + public void createPropertyForType_shouldReturnListOfEnumsWhenGivenAnInnerNestedEnum() throws NoSuchFieldException { + Field actionField = SampleResourceEnum.class.getDeclaredField("sampleResourceInnerEnum"); + Property property = SwaggerGenerationUtil.createPropertyForType( + actionField.getType(), "Create", actionField); + + assertTrue(property instanceof StringProperty); + StringProperty stringProperty = (StringProperty) property; + assertNotNull(stringProperty.getEnum()); + + assertTrue(stringProperty.getEnum().contains("SCHEDULETASK")); + assertTrue(stringProperty.getEnum().contains("SHUTDOWNTASK")); + } + + @Test + public void createPropertyForType_shouldReturnAnArrayPropertyWithNoRefPropertyWhenFieldIsAList() throws NoSuchFieldException { + Field attributesField = User.class.getDeclaredField("proficientLocales"); + Property property = SwaggerGenerationUtil.createPropertyForType( + attributesField.getType(), "GetRef", attributesField); + System.out.println("prop" + property); + + assertTrue(property instanceof ArrayProperty); + ArrayProperty arrayProperty = (ArrayProperty) property; + assertNotEquals(arrayProperty.getItems() instanceof RefProperty, true); + } + + //classes to be used in this test class + public static class BaseHandler {} + + public interface ParameterizedInterface {} + + static class SampleResource { + private String name; + private int age; + private boolean isActive; + private SampleResourceEnum.SampleResourceInnerEnum action; + } + + static class SampleSubResource { + private String subName; + private String subAge; + } + + public enum SampleResourceOutterEnum { + CREATE, PATCH, UPDATE; + } + + public static class SampleResourceEnum { + + public enum SampleResourceInnerEnum { + SCHEDULETASK, SHUTDOWNTASK, RESCHEDULETASK, RESCHEDULEALLTASKS, DELETE, RUNTASK; + } + + private SampleResourceInnerEnum sampleResourceInnerEnum; + + private SampleResourceOutterEnum sampleResourceOutterEnum; + + public SampleResourceInnerEnum getAction() { + return sampleResourceInnerEnum; + } + + public void setAction(SampleResourceInnerEnum sampleResourceInnerEnum) { + this.sampleResourceInnerEnum = sampleResourceInnerEnum; + } + } + + // Resource Handler for Sub Resource + static class SampleSubResourceHandler extends DelegatingSubResource { + @Override + public SampleResource getParent(SampleSubResource instance) { + return null; + } + + @Override + public void setParent(SampleSubResource instance, SampleResource parent) { + + } + + @Override + public PageableResult doGetAll(SampleResource parent, RequestContext context) throws ResponseException { + return null; + } + + @Override + public SampleSubResource getByUniqueId(String uniqueId) { + return null; + } + + @Override + protected void delete(SampleSubResource delegate, String reason, RequestContext context) throws ResponseException { + + } + + @Override + public SampleSubResource newDelegate() { + return null; + } + + @Override + public SampleSubResource save(SampleSubResource delegate) { + return null; + } + + @Override + public void purge(SampleSubResource delegate, RequestContext context) throws ResponseException { + + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + return null; + } + } + + // Resource Handler for Parent Class + static class SampleResourceHandler extends DelegatingCrudResource { + @Override + public SampleResource getByUniqueId(String uniqueId) { + return null; + } + + @Override + protected void delete(SampleResource delegate, String reason, RequestContext context) throws ResponseException { + + } + + @Override + public SampleResource newDelegate() { + return null; + } + + @Override + public SampleResource save(SampleResource delegate) { + return null; + } + + @Override + public void purge(SampleResource delegate, RequestContext context) throws ResponseException { + + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + if (rep instanceof DefaultRepresentation || rep instanceof FullRepresentation) { + description.addProperty("name"); + description.addProperty("age"); + description.addProperty("isActive"); + description.addProperty("action"); + } else if (rep instanceof RefRepresentation) { + description.addProperty("name"); + description.addProperty("age"); + description.addSelfLink(); + } + return description; + } + + @Override + public DelegatingResourceDescription getCreatableProperties() throws ResourceDoesNotSupportOperationException { + DelegatingResourceDescription description = new DelegatingResourceDescription(); + description.addProperty("name"); + description.addProperty("age"); + description.addProperty("isActive"); + description.addProperty("action"); + return description; + } + + @Override + public DelegatingResourceDescription getUpdatableProperties() throws ResourceDoesNotSupportOperationException { + return getCreatableProperties(); + } + } +}