From 6e07753e6d1b02e404e2ce10bf8fef90af75d17b Mon Sep 17 00:00:00 2001 From: mherman22 Date: Sat, 30 Nov 2024 21:32:50 +0300 Subject: [PATCH] handle openmrs objects that are in the form of enums, or parameters within Set, List, etc --- .../doc/SwaggerSpecificationCreatorTest.java | 2 +- .../docs/swagger/SwaggerGenerationUtil.java | 83 +++++++- .../swagger/SwaggerSpecificationCreator.java | 15 +- .../rest/web/SwaggerGenerationUtilTest.java | 185 +++++++++++++++++- 4 files changed, 267 insertions(+), 18 deletions(-) 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 4b72f5bbc..81f599114 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 @@ -238,7 +238,7 @@ public void addPathsWorksForCoreModels() throws NoSuchMethodException, Invocatio IllegalAccessException, NoSuchFieldException { SwaggerSpecificationCreator ssc = new SwaggerSpecificationCreator(); - // reflect the swagger propperty and initSwagger method so we can setup for the main test + // reflect the swagger property and initSwagger method so we can set up for the main test Field swagger = ssc.getClass().getDeclaredField("swagger"); swagger.setAccessible(true); swagger.set(ssc, new Swagger()); 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 index 549eb7c0d..c15ce04af 100644 --- 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 @@ -12,13 +12,20 @@ import io.swagger.models.Model; import io.swagger.models.ModelImpl; import io.swagger.models.properties.*; +import org.apache.commons.lang.StringUtils; +import org.openmrs.module.webservices.docs.swagger.core.property.EnumProperty; 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.Collection; +import java.util.List; +import java.util.Set; import java.util.UUID; import static org.openmrs.module.webservices.rest.web.representation.Representation.DEFAULT; @@ -38,6 +45,8 @@ */ public class SwaggerGenerationUtil { + private static final Logger logger = LoggerFactory.getLogger(SwaggerGenerationUtil.class); + /** * Generates the model for GET operations. * @@ -234,7 +243,8 @@ public static Property determinePropertyForField(Object resourceHandler, String * @param field the field to generate the property for * @return the Swagger property */ - private static Property createPropertyForType(Class type, String operationType, Field field) { + @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)) { @@ -248,12 +258,50 @@ private static Property createPropertyForType(Class type, String operationTyp } else if (Double.class.equals(type)) { return new DoubleProperty(); } else if (isOpenMRSResource(type)) { - return new RefProperty("#/definitions/" + field.getName() + operationType); + if (type.isEnum()) { + return new EnumProperty((Class>) type); + } else { + return new RefProperty("#/definitions/" + StringUtils.capitalize(getModelName(String.valueOf(field.getType()))) + operationType); + } + } else if (Set.class.equals(type) || List.class.equals(type)) { + Class elementType = getGenericTypeFromField(field); + if (elementType != null && isOpenMRSResource(elementType) ) { + return new ArrayProperty(new RefProperty("#/definitions/" + + StringUtils.capitalize(getModelNameFromGenericType(elementType.getName())) + operationType)); + } + return new ArrayProperty(); } else { return new ObjectProperty(); } } + + /** + * Extracts the simple name from a fully qualified class name. + * + * @param qualifiedName the fully qualified class name to extract the simple name from (e.g., "org.openmrs.Patient") + * @return the simple class name (e.g., "Patient"), or the original string if no dot is present, or null if the input is null + */ + 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).toLowerCase(); + } + + public static String getModelNameFromGenericType(String name) { + if (name == null || !name.contains(".")) { + return name; + } + + String simpleName = name.substring(name.lastIndexOf('.') + 1); + simpleName = simpleName.replace("$", ""); + return simpleName.substring(0, 1).toUpperCase() + simpleName.substring(1); + } + /** * Checks whether a class is an OpenMRS resource (e.g., references an OpenMRS data object). * @@ -261,7 +309,36 @@ private static Property createPropertyForType(Class type, String operationTyp * @return true if the class represents an OpenMRS resource, false otherwise */ private static boolean isOpenMRSResource(Class type) { - return type.getPackage().getName().startsWith("org.openmrs"); + if (type == null) { + return false; + } + + Package pkg = type.getPackage(); + return pkg != null && pkg.getName().startsWith("org.openmrs"); + } + + + /** + * 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; } /** 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 d045f54e2..264a780cc 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 @@ -834,13 +834,13 @@ private void addSubclassOperations() { Map properties = definition.getProperties(); - // 2. merge subclass properties into definition -// for (Map.Entry prop : resourceHandler.getGETModel(Representation.FULL).getProperties() -// .entrySet()) { -// if (properties.get(prop.getKey()) == null) { -// properties.put(prop.getKey(), prop.getValue()); -// } -// } +// 2. merge subclass properties into definition + for (Map.Entry prop : SwaggerGenerationUtil.generateGETModel(resourceHandler, Representation.FULL).getProperties() + .entrySet()) { + if (properties.get(prop.getKey()) == null) { + properties.put(prop.getKey(), prop.getValue()); + } + } // 3. update description post.setDescription("Certain properties may be required depending on type"); @@ -997,7 +997,6 @@ private void createDefinition(OperationEnum operationEnum, String resourceName, DelegatingResourceHandler resourceHandler) { String definitionName = getSchemaName(resourceName, resourceParentName, operationEnum); - System.out.println("definition-name:" + definitionName); Model model = null; Model modelRef = null; Model modelFull = null; 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 index 1075b3ee2..8f27ec3f4 100644 --- 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 @@ -11,26 +11,36 @@ 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.rest.SimpleObject; +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 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; @@ -44,6 +54,14 @@ class StringHandler extends BaseHandler {} 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 {} @@ -118,10 +136,12 @@ public void generateGETModel_shouldGenerateGETModelWhenGivenDefaultRepresentatio 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 @@ -136,6 +156,7 @@ public void generateGETModel_shouldGenerateGETModelWhenGivenRefRepresentation() 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); @@ -152,10 +173,12 @@ public void generateCREATEModel_shouldGenerateCREATEModelWhenGivenDefaultReprese 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 @@ -169,10 +192,12 @@ public void generateCREATEModel_shouldGenerateCREATEModelWhenGivenFullRepresenta 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 @@ -186,10 +211,77 @@ public void generateUPDATEModel_shouldGenerateUPDATEModelWhenGivenDefaultReprese 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_shouldCheckForOpenMRSResource() throws NoSuchFieldException { + Field nameField = Concept.class.getDeclaredField("retiredBy"); + Property property = SwaggerGenerationUtil.createPropertyForType( + nameField.getType(), "Get", nameField); + + assertTrue(property instanceof RefProperty); + RefProperty stringProperty = (RefProperty) property; + assertEquals("#/definitions/UserGet", stringProperty.get$ref()); + } + + @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_shouldReturnAnArrayPropertyWithRefPropertyWhenFieldIsASet() throws NoSuchFieldException { + Field attributesField = Person.class.getDeclaredField("attributes"); + Property property = SwaggerGenerationUtil.createPropertyForType( + attributesField.getType(), "GetRef", attributesField); + + assertTrue(property instanceof ArrayProperty); + ArrayProperty arrayProperty = (ArrayProperty) property; + assertTrue(arrayProperty.getItems() instanceof RefProperty); + + RefProperty refProperty = (RefProperty) arrayProperty.getItems(); + assertEquals("#/definitions/PersonAttributeGetRef", refProperty.get$ref()); + } + + @Test + public void createPropertyForType_shouldReturnAnArrayPropertyWithRefPropertyWhenFieldIsAList() 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 @@ -201,21 +293,99 @@ static class SampleResource { private String name; private int age; private boolean isActive; + private SampleResourceEnum.SampleResourceInnerEnum action; } - static class SampleResourceHandler implements DelegatingResourceHandler { + 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 String getResourceVersion() { - return ""; + public SampleResource getParent(SampleSubResource instance) { + return null; } @Override - public SampleResource newDelegate() { + public void setParent(SampleSubResource instance, SampleResource parent) { + + } + + @Override + public PageableResult doGetAll(SampleResource parent, RequestContext context) throws ResponseException { return null; } @Override - public SampleResource newDelegate(SimpleObject object) { + 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; } @@ -226,6 +396,7 @@ public SampleResource save(SampleResource delegate) { @Override public void purge(SampleResource delegate, RequestContext context) throws ResponseException { + } @Override @@ -235,6 +406,7 @@ public DelegatingResourceDescription getRepresentationDescription(Representation description.addProperty("name"); description.addProperty("age"); description.addProperty("isActive"); + description.addProperty("action"); } else if (rep instanceof RefRepresentation) { description.addProperty("name"); description.addProperty("age"); @@ -249,6 +421,7 @@ public DelegatingResourceDescription getCreatableProperties() throws ResourceDoe description.addProperty("name"); description.addProperty("age"); description.addProperty("isActive"); + description.addProperty("action"); return description; }