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