diff --git a/omod-2.4/pom.xml b/omod-2.4/pom.xml index 236bc8b6e..07f178603 100644 --- a/omod-2.4/pom.xml +++ b/omod-2.4/pom.xml @@ -97,6 +97,24 @@
${project.parent.basedir}/license-header.txt
+ + org.openmrs.plugin + openapi-generator-maven-plugin + 1.0.0-SNAPSHOT + + + org.openmrs.module.webservices.rest.web.v1_0.resource + + + + + process-classes + + openapi + + + + diff --git a/omod-2.4/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/resource/openmrs2_2/ConditionResource2_2.java b/omod-2.4/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/resource/openmrs2_2/ConditionResource2_2.java index 9bba1b5a2..e8b88b103 100644 --- a/omod-2.4/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/resource/openmrs2_2/ConditionResource2_2.java +++ b/omod-2.4/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/resource/openmrs2_2/ConditionResource2_2.java @@ -42,7 +42,14 @@ "2.2.* - 9.*" }) public class ConditionResource2_2 extends DataDelegatingCrudResource { - private ConditionService conditionService = Context.getConditionService(); + private ConditionService conditionService; + + private ConditionService getConditionService() { + if (conditionService == null) { + conditionService = Context.getConditionService(); + } + return conditionService; + } /** * @see org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource#getRepresentationDescription(Representation) @@ -180,7 +187,7 @@ public DelegatingResourceDescription getUpdatableProperties() { */ @Override public Condition getByUniqueId(String uuid) { - return conditionService.getConditionByUuid(uuid); + return getConditionService().getConditionByUuid(uuid); } /** @@ -189,7 +196,7 @@ public Condition getByUniqueId(String uuid) { */ @Override protected void delete(Condition condition, String reason, RequestContext requestContext) throws ResponseException { - conditionService.voidCondition(condition, reason); + getConditionService().voidCondition(condition, reason); } /** @@ -205,7 +212,7 @@ public Condition newDelegate() { */ @Override public Condition save(Condition condition) { - return conditionService.saveCondition(condition); + return getConditionService().saveCondition(condition); } /** @@ -214,7 +221,7 @@ public Condition save(Condition condition) { */ @Override public void purge(Condition condition, RequestContext requestContext) throws ResponseException { - conditionService.purgeCondition(condition); + getConditionService().purgeCondition(condition); } /** @@ -237,7 +244,6 @@ public String getDisplayString(Condition condition) { protected PageableResult doSearch(RequestContext context) { String patientUuid = context.getRequest().getParameter("patientUuid"); String includeInactive = context.getRequest().getParameter("includeInactive"); - ConditionService conditionService = Context.getConditionService(); if (StringUtils.isBlank(patientUuid)) { return new EmptySearchResult(); } @@ -249,13 +255,13 @@ protected PageableResult doSearch(RequestContext context) { if (StringUtils.isNotBlank(includeInactive)) { boolean isIncludeInactive = BooleanUtils.toBoolean(includeInactive); if (isIncludeInactive) { - return new NeedsPaging(conditionService.getAllConditions(patient), context); + return new NeedsPaging(getConditionService().getAllConditions(patient), context); } else { - return new NeedsPaging(conditionService.getActiveConditions(patient), context); + return new NeedsPaging(getConditionService().getActiveConditions(patient), context); } } else { - return new NeedsPaging(conditionService.getActiveConditions(patient), context); + return new NeedsPaging(getConditionService().getActiveConditions(patient), context); } } } diff --git a/omod/pom.xml b/omod/pom.xml index aac118259..098c35fb6 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -82,13 +82,13 @@ org.apache.maven.plugins maven-dependency-plugin - + Expand resources unpack-dependencies - generate-resources + prepare-package ${project.parent.groupId} ${project.parent.artifactId}-omod-common @@ -105,7 +105,7 @@
${project.parent.basedir}/license-header.txt
-
+ org.jacoco jacoco-maven-plugin diff --git a/openapi-generator-maven-plugin/pom.xml b/openapi-generator-maven-plugin/pom.xml new file mode 100644 index 000000000..b5ee2cb4b --- /dev/null +++ b/openapi-generator-maven-plugin/pom.xml @@ -0,0 +1,156 @@ + + 4.0.0 + + org.openmrs.plugin + openapi-generator-maven-plugin + 1.0.0-SNAPSHOT + maven-plugin + + Rest Web Services OpenAPI Plugin + A Maven plugin for generating OpenAPI specifications for OpenMRS modules. + + + 1.8 + 1.8 + UTF-8 + + + + + openmrs-repo + OpenMRS Nexus Repository + https://mavenrepo.openmrs.org/public + + + + + + org.apache.maven + maven-plugin-api + 3.8.6 + provided + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.4 + provided + + + + org.apache.maven + maven-core + 3.8.6 + provided + + + + com.github.javaparser + javaparser-core + 3.25.5 + + + + io.swagger + swagger-core + 1.6.2 + + + + io.swagger + swagger-models + 1.6.2 + + + + io.swagger + swagger-annotations + 1.6.2 + + + + io.swagger.core.v3 + swagger-core + 2.2.8 + + + + io.swagger.core.v3 + swagger-models + 2.2.8 + + + + io.swagger.core.v3 + swagger-annotations + 2.2.8 + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.15.2 + + + + org.openmrs.api + openmrs-api + 2.7.0 + + + + org.openmrs.module + webservices.rest-omod-common + 2.50.0-SNAPSHOT + + + + org.springframework + spring-web + 5.3.30 + + + + javax.servlet + servlet-api + 2.5 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.4 + + + default-descriptor + process-classes + + descriptor + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 1.8 + 1.8 + + + + + \ No newline at end of file diff --git a/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/OpenmrsOpenApiSpecMojo.java b/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/OpenmrsOpenApiSpecMojo.java new file mode 100644 index 000000000..2e9233afb --- /dev/null +++ b/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/OpenmrsOpenApiSpecMojo.java @@ -0,0 +1,411 @@ +/** + * 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.plugin; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.apache.maven.artifact.Artifact; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.Map; + +@Mojo(name = "openapi", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.RUNTIME) +public class OpenmrsOpenApiSpecMojo extends AbstractMojo { + + private static final Logger log = LoggerFactory.getLogger(OpenmrsOpenApiSpecMojo.class); + + private URLClassLoader classLoader; + + private Class representationClass; + private Class defaultRepresentationClass; + private Class fullRepresentationClass; + private Class requestMappingClass; + private Class restUtilClass; + private Class restConstantsClass; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + + @Parameter + private List scanPackages; + + @Parameter(defaultValue = "false") + private boolean skipEmptyModules; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + log.info("=== OPENAPI GENERATION STARTED ==="); + log.info("Processing module: {}", project.getArtifactId()); + + this.classLoader = setupProjectClassLoader(); + loadCommonClasses(); + disableOpenMRSContext(); + + List> resourceClasses = discoverResourceClasses(); + + if (resourceClasses.isEmpty()) { + log.info("No resources found in this module - skipping OpenAPI generation"); + return; + } + + log.info("Found {} resource classes to process", resourceClasses.size()); + + for (Class resourceClass : resourceClasses) { + processResourceClass(resourceClass); + } + + log.info("=== OPENAPI GENERATION COMPLETED ==="); + } + + private URLClassLoader setupProjectClassLoader() { + try { + List urls = new ArrayList<>(); + + String outputDirectory = project.getBuild().getOutputDirectory(); + File outputDir = new File(outputDirectory); + if (outputDir.exists()) { + urls.add(outputDir.toURI().toURL()); + } else { + log.warn("Project output directory does not exist: " + outputDirectory); + } + + Set allArtifacts = project.getArtifacts(); + if (allArtifacts != null) { + log.debug("Found " + allArtifacts.size() + " project artifacts"); + for (Artifact artifact : allArtifacts) { + File file = artifact.getFile(); + if (file != null && file.exists()) { + urls.add(file.toURI().toURL()); + log.debug("Added artifact: " + artifact.getGroupId() + ":" + artifact.getArtifactId() + " -> " + file.getPath()); + } else { + log.warn("Artifact file not found: " + artifact.getGroupId() + ":" + artifact.getArtifactId()); + } + } } + + return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + + } catch (java.net.MalformedURLException e) { + throw new RuntimeException("Invalid URL in classpath elements: " + e.getMessage(), e); + } + } + + private void loadCommonClasses() { + log.debug("=== Loading Common Classes (One-time Cache) ==="); + + try { + representationClass = classLoader.loadClass("org.openmrs.module.webservices.rest.web.representation.Representation"); + defaultRepresentationClass = classLoader.loadClass("org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation"); + fullRepresentationClass = classLoader.loadClass("org.openmrs.module.webservices.rest.web.representation.FullRepresentation"); + log.debug("Cached representation classes"); + + restUtilClass = classLoader.loadClass("org.openmrs.module.webservices.rest.web.RestUtil"); + restConstantsClass = classLoader.loadClass("org.openmrs.module.webservices.rest.web.RestConstants"); + log.debug("Cached REST utility classes"); + + try { + requestMappingClass = classLoader.loadClass("org.springframework.web.bind.annotation.RequestMapping"); + log.debug("Cached RequestMapping annotation class"); + } catch (ClassNotFoundException e) { + log.debug("RequestMapping annotation not available - will skip annotation checks"); + requestMappingClass = null; + } + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to load required OpenMRS classes. Ensure webservices.rest module is in dependencies.", e); + } + } + + private void disableOpenMRSContext() { + try { + Method disableContextMethod = restUtilClass.getMethod("disableContext"); + disableContextMethod.invoke(null); + log.info("SUCCESS: OpenMRS Context disabled successfully"); + log.debug(" RestUtil.contextEnabled is now false"); + log.debug(" Static initializers will not attempt Context access"); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + log.error("FAILED: Cannot disable OpenMRS context", e); + throw new RuntimeException("Cannot disable OpenMRS context", e); + } + } + + private Class loadClass(String className) { + try { + Class clazz = classLoader.loadClass(className); + log.info("Loaded class: " + clazz.getName()); + return clazz; + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to load class: " + className, e); + } + } + + private List> discoverResourceClasses() throws MojoExecutionException { + List> discoveredClasses = new ArrayList<>(); + + if (scanPackages != null && !scanPackages.isEmpty()) { + log.info("Scanning packages: {}", scanPackages); + discoveredClasses = scanPackagesForResources(scanPackages); + } + + else { + throw new MojoExecutionException( + "No resource discovery configuration provided. " + + "Please specify either 'targetClasses' or 'scanPackages' in plugin configuration." + ); + } + + if (discoveredClasses.isEmpty() && !skipEmptyModules) { + throw new MojoExecutionException("No REST resource classes found in this module"); + } + + log.info("Discovered {} resource classes", discoveredClasses.size()); + return discoveredClasses; + } + + private List> scanPackagesForResources(List packages) { + List> resourceClasses = new ArrayList<>(); + + for (String packageName : packages) { + log.debug("Scanning package: {}", packageName); + + String packagePath = packageName.replace('.', '/'); + + String outputDirectory = project.getBuild().getOutputDirectory(); + File packageDir = new File(outputDirectory, packagePath); + + if (!packageDir.exists()) { + log.warn("Package directory does not exist: {}", packageDir.getAbsolutePath()); + continue; + } + + List classFiles = findClassFiles(packageDir); + + for (File classFile : classFiles) { + String className = getClassNameFromFile(classFile, outputDirectory); + + try { + Class clazz = classLoader.loadClass(className); + + if (isRestResourceClass(clazz)) { + resourceClasses.add(clazz); + log.info("Found REST resource: {}", className); + } else { + log.debug("Skipped non-resource class: {}", className); + } + + } catch (ClassNotFoundException | NoClassDefFoundError e) { + log.debug("Could not load class {}: {}", className, e.getMessage()); + } + } + } + + return resourceClasses; + } + + private boolean isRestResourceClass(Class clazz) { + if (clazz.isInterface() || + java.lang.reflect.Modifier.isAbstract(clazz.getModifiers()) || + clazz.getName().contains("Test")) { + return false; + } + + if (!clazz.getName().contains(".resource.")) { + return false; + } + + try { + Method getRepDescMethod = clazz.getMethod("getRepresentationDescription", representationClass); + + if (getRepDescMethod != null) { + log.debug("Class {} has getRepresentationDescription method", clazz.getSimpleName()); + return true; + } + + } catch (NoSuchMethodException e) { + Class current = clazz; + while (current != null) { + String baseClassName = current.getName(); + if (baseClassName.contains("DelegatingResourceHandler") || + baseClassName.contains("DelegatingCrudResource") || + baseClassName.contains("BaseRestController")) { + log.debug("Class {} extends REST resource base class", clazz.getSimpleName()); + return true; + } + current = current.getSuperclass(); + } + } + + if (requestMappingClass != null) { + try { + if (clazz.isAnnotationPresent(requestMappingClass.asSubclass(java.lang.annotation.Annotation.class))) { + log.debug("Class {} has REST annotations", clazz.getSimpleName()); + return true; + } + } catch (ClassCastException e) { + log.debug("RequestMapping class is not an annotation, skipping annotation check"); + } + } + + return false; + } + + private List findClassFiles(File directory) { + List classFiles = new ArrayList<>(); + + if (!directory.exists() || !directory.isDirectory()) { + return classFiles; + } + + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + classFiles.addAll(findClassFiles(file)); + } else if (file.getName().endsWith(".class")) { + classFiles.add(file); + } + } + } + + return classFiles; + } + + private String getClassNameFromFile(File classFile, String outputDirectory) { + String filePath = classFile.getAbsolutePath(); + String outputPath = new File(outputDirectory).getAbsolutePath(); + + String relativePath = filePath.substring(outputPath.length() + 1); + String className = relativePath.substring(0, relativePath.length() - 6); // Remove ".class" + + return className.replace(File.separatorChar, '.'); + } + + private void processResourceClass(Class resourceClass) { + log.info("=== Resource: {} ===", resourceClass.getSimpleName()); + + try { + Object instance = resourceClass.getDeclaredConstructor().newInstance(); + + processGetRepresentationDescription(instance); + + } catch (Exception e) { + log.warn("Failed to process resource {}: {}", resourceClass.getSimpleName(), e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("Resource processing error details", e); + } + } + } + + private void processGetRepresentationDescription(Object instance) throws MojoExecutionException { + + try { + Object defaultRep = createRequiredInstance(defaultRepresentationClass); + Object fullRep = createRequiredInstance(fullRepresentationClass); + + Method method = instance.getClass().getMethod("getRepresentationDescription", representationClass); + + Object defaultResult = invokeRepresentationMethod(method, instance, defaultRep, "DEFAULT"); + Object fullResult = invokeRepresentationMethod(method, instance, fullRep, "FULL"); + + tryExtractSchema(defaultResult, "DEFAULT"); + tryExtractSchema(fullResult, "FULL"); + + } catch (NoSuchMethodException e) { + throw new MojoExecutionException("getRepresentationDescription method not found on resource class. " + + "This OpenMRS version may not be supported.", e); + } catch (InstantiationException | IllegalAccessException e) { + throw new MojoExecutionException("Cannot create or access OpenMRS representation instances. " + + "Check OpenMRS version compatibility.", e); + } + } + + private Object createRequiredInstance(Class clazz) throws InstantiationException, IllegalAccessException { + try { + return clazz.getDeclaredConstructor().newInstance(); + } catch (NoSuchMethodException | InvocationTargetException e) { + String message = e instanceof NoSuchMethodException ? + "No default constructor found for " + clazz.getSimpleName() : + "Constructor failed: " + ((InvocationTargetException)e).getCause().getMessage(); + throw new InstantiationException(message); + } + } + + private Object invokeRepresentationMethod(Method method, Object instance, Object representation, String type) { + try { + Object result = method.invoke(instance, representation); + return result; + } catch (java.lang.reflect.InvocationTargetException e) { + log.warn("Failed to invoke getRepresentationDescription for {}: {}", type, e.getCause().getMessage()); + if (log.isDebugEnabled()) { + log.debug("Method invocation error details", e.getCause()); + } + return null; + } catch (IllegalAccessException e) { + log.warn("Cannot access getRepresentationDescription method for {}: {}", type, e.getMessage()); + return null; + } + } + + private boolean tryExtractSchema(Object description, String representationType) { + if (description == null) { + log.info("No {} representation to extract schema from", representationType); + return false; + } + + boolean propertiesSuccess = tryExtractProperties(description, representationType); + if (!propertiesSuccess) { + log.warn("Could not extract properties for {}", representationType); + } + + return propertiesSuccess; + + } + + private boolean tryExtractProperties(Object description, String type) { + try { + Method getPropertiesMethod = description.getClass().getMethod("getProperties"); + Object properties = getPropertiesMethod.invoke(description); + + if (properties instanceof Map) { + Map propertyMap = (Map) properties; + log.info("Found {} properties in {}", propertyMap.size(), type); + return true; + } else { + log.warn("getProperties() returned non-Map type for {}: {}", type, + properties != null ? properties.getClass().getSimpleName() : "null"); + return false; + } + + } catch (NoSuchMethodException | IllegalAccessException | ClassCastException e) { + log.warn("Cannot extract properties for {}: {}", type, e.getMessage()); + return false; + } catch (InvocationTargetException e) { + log.warn("getProperties method failed for {}: {}", type, e.getCause().getMessage()); + if (log.isDebugEnabled()) { + log.debug("getProperties error details", e.getCause()); + } + return false; + } + } +} diff --git a/pom.xml b/pom.xml index cad09349a..cf5cbc106 100644 --- a/pom.xml +++ b/pom.xml @@ -466,6 +466,7 @@ + openapi-generator-maven-plugin omod-common omod-2.4 omod-2.5 @@ -484,6 +485,7 @@ + openapi-generator-maven-plugin omod-common omod-2.4 omod @@ -528,6 +530,7 @@ release + openapi-generator-maven-plugin omod-common omod-2.4 omod-2.5