From 5df64ab5e73112045a6ea5f188c3ced3b471ee75 Mon Sep 17 00:00:00 2001 From: capernix Date: Thu, 17 Apr 2025 09:39:15 +0530 Subject: [PATCH 1/5] Implemented a method to find all properties of a given resource --- .../web/api/SchemaIntrospectionService.java | 46 ++++ .../impl/SchemaIntrospectionServiceImpl.java | 203 ++++++++++++++++++ .../SchemaIntrospectionController.java | 92 ++++++++ 3 files changed, 341 insertions(+) create mode 100644 omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/SchemaIntrospectionService.java create mode 100644 omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java create mode 100644 omod-common/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionController.java diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/SchemaIntrospectionService.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/SchemaIntrospectionService.java new file mode 100644 index 000000000..f88731320 --- /dev/null +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/SchemaIntrospectionService.java @@ -0,0 +1,46 @@ +/** + * 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.api; + +import java.util.Map; + +import org.openmrs.module.webservices.rest.web.resource.api.Resource; + +/** + * Service for introspecting resource properties via reflection. + * This enables clients to discover all properties that may be available in custom representations. + */ +public interface SchemaIntrospectionService { + + /** + * Gets the delegate type (T) for a resource that extends DelegatingCrudResource. + * + * @param resource The resource whose delegate class we want to determine + * @return The class type of the delegate, or null if not determinable + */ + Class getDelegateType(Resource resource); + + /** + * Discovers all available properties on a delegate type using reflection. + * This includes public instance fields and JavaBean-style getter methods. + * + * @param delegateType The class to introspect for properties + * @return A map of property names to their Java type names + */ + Map discoverAvailableProperties(Class delegateType); + + /** + * Combines getDelegateType and discoverAvailableProperties for convenience. + * + * @param resource The resource to introspect + * @return A map of property names to their Java type names + */ + Map discoverResourceProperties(Resource resource); +} \ No newline at end of file diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java new file mode 100644 index 000000000..312f36cf2 --- /dev/null +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java @@ -0,0 +1,203 @@ +/** + * 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.api.impl; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.api.impl.BaseOpenmrsService; +import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService; +import org.openmrs.module.webservices.rest.web.resource.api.Resource; +import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; + +/** + * Default implementation of {@link SchemaIntrospectionService} + */ +@Component +public class SchemaIntrospectionServiceImpl extends BaseOpenmrsService implements SchemaIntrospectionService { + + protected final Log log = LogFactory.getLog(getClass()); + + /** + * @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#getDelegateType(Resource) + */ + @Override + public Class getDelegateType(Resource resource) { + if (resource == null) { + return null; + } + + // Handle non-delegating resources + if (!(resource instanceof DelegatingResourceHandler)) { + log.warn("Resource " + resource.getClass().getName() + " is not a DelegatingResourceHandler"); + return null; + } + + // Attempt to determine the delegate type from the generic parameter T in DelegatingCrudResource + // or DelegatingSubResource + Class resourceClass = resource.getClass(); + + // Search through the class hierarchy to find the class that implements/extends with the generic type + while (resourceClass != null) { + Type[] genericInterfaces = resourceClass.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericInterface; + Type rawType = parameterizedType.getRawType(); + + // Check if this interface is DelegatingResourceHandler or a subinterface of it + if (rawType instanceof Class + && DelegatingResourceHandler.class.isAssignableFrom((Class) rawType)) { + // First type parameter should be the delegate type + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + return (Class) typeArgs[0]; + } + } + } + } + + // Check the superclass's generic type + Type genericSuperclass = resourceClass.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + + // First type argument should be the delegate type + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + return (Class) typeArgs[0]; + } + } + + // Move up the hierarchy + resourceClass = resourceClass.getSuperclass(); + } + + log.warn("Could not determine delegate type for " + resource.getClass().getName()); + return null; + } + + /** + * @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#discoverAvailableProperties(Class) + */ + @Override + public Map discoverAvailableProperties(Class delegateType) { + if (delegateType == null) { + return new HashMap(); + } + + Map properties = new HashMap(); + + // Process fields up through the class hierarchy + Class currentClass = delegateType; + while (currentClass != null && !currentClass.equals(Object.class)) { + processFields(currentClass, properties); + currentClass = currentClass.getSuperclass(); + } + + // Process getters - use Spring's BeanUtils to get all property descriptors + PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(delegateType); + for (PropertyDescriptor descriptor : propertyDescriptors) { + // Skip "class" and properties without a readable method (getter) + if ("class".equals(descriptor.getName()) || descriptor.getReadMethod() == null) { + continue; + } + + Method readMethod = descriptor.getReadMethod(); + + // Only include public methods + if (Modifier.isPublic(readMethod.getModifiers()) && !Modifier.isStatic(readMethod.getModifiers())) { + // Get return type, including generic type if available + String typeName = getTypeName(readMethod.getGenericReturnType()); + properties.put(descriptor.getName(), typeName); + } + } + + return properties; + } + + /** + * @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#discoverResourceProperties(Resource) + */ + @Override + public Map discoverResourceProperties(Resource resource) { + Class delegateType = getDelegateType(resource); + return discoverAvailableProperties(delegateType); + } + + /** + * Helper method to process fields from a class and add them to the properties map + * + * @param clazz The class to process fields from + * @param properties The map to add properties to + */ + private void processFields(Class clazz, Map properties) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + // Only include public instance fields + if (Modifier.isPublic(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) { + String typeName = getTypeName(field.getGenericType()); + properties.put(field.getName(), typeName); + } + } + } + + /** + * Helper method to get a user-friendly type name from a Type object + * + * @param type The type to get a name for + * @return A user-friendly type name string + */ + private String getTypeName(Type type) { + if (type instanceof Class) { + return ((Class) type).getSimpleName(); + } else if (type instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) type; + Type rawType = paramType.getRawType(); + Type[] typeArgs = paramType.getActualTypeArguments(); + + StringBuilder sb = new StringBuilder(); + if (rawType instanceof Class) { + sb.append(((Class) rawType).getSimpleName()); + } else { + sb.append(rawType.toString()); + } + + if (typeArgs.length > 0) { + sb.append("<"); + for (int i = 0; i < typeArgs.length; i++) { + if (i > 0) { + sb.append(", "); + } + if (typeArgs[i] instanceof Class) { + sb.append(((Class) typeArgs[i]).getSimpleName()); + } else { + sb.append(typeArgs[i].toString()); + } + } + sb.append(">"); + } + + return sb.toString(); + } else { + return type.toString(); + } + } +} \ No newline at end of file diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionController.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionController.java new file mode 100644 index 000000000..6f29139f3 --- /dev/null +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionController.java @@ -0,0 +1,92 @@ +/** + * 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.v1_0.controller; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.api.RestService; +import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService; +import org.openmrs.module.webservices.rest.web.resource.api.Resource; +import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; +import org.openmrs.module.webservices.rest.web.response.ResponseException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Controller that provides schema introspection for REST resources. + * This allows clients to discover what properties are available for a given resource. + */ +@Controller +@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/schemaintrospector") +public class SchemaIntrospectionController extends BaseRestController { + + protected final Log log = LogFactory.getLog(getClass()); + + @Autowired + private RestService restService; + + @Autowired + private SchemaIntrospectionService schemaIntrospectionService; + + /** + * Gets all properties available on a resource's delegate domain object + * + * @param resourceName The name of the resource to introspect + * @return A JSON object containing the resource properties + * @throws ResponseException + */ + @RequestMapping(value = "/{resourceName}", method = RequestMethod.GET) + @ResponseBody + public SimpleObject getResourceProperties(@PathVariable("resourceName") String resourceName) throws ResponseException { + // Get the resource by name with proper prefix + Resource resource; + try { + // Try with the full path (which is how resources are registered) + String fullResourceName = RestConstants.VERSION_1 + "/" + resourceName; + log.debug("Looking for resource with name: " + fullResourceName); + resource = restService.getResourceByName(fullResourceName); + } + catch (Exception e) { + log.error("Failed to find resource with name: " + resourceName, e); + throw new ObjectNotFoundException("Resource '" + resourceName + "' was not found"); + } + + if (resource == null) { + log.error("Resource is null for name: " + resourceName); + throw new ObjectNotFoundException("Resource '" + resourceName + "' was not found"); + } + + // Get the delegate type for the resource + Class delegateType = schemaIntrospectionService.getDelegateType(resource); + if (delegateType == null) { + log.error("Could not determine delegate type for resource: " + resourceName); + throw new ObjectNotFoundException("Could not determine delegate type for resource '" + resourceName + "'"); + } + + // Get properties for the delegate type + Map properties = schemaIntrospectionService.discoverAvailableProperties(delegateType); + + // Build the response + SimpleObject result = new SimpleObject(); + result.add("resourceName", resourceName); + result.add("delegateType", delegateType.getName()); + result.add("discoverableProperties", properties); + + return result; + } +} \ No newline at end of file From e788b51693539f12fccfd11f8d3be63a661f5cda Mon Sep 17 00:00:00 2001 From: capernix Date: Thu, 17 Apr 2025 20:48:11 +0530 Subject: [PATCH 2/5] Added in tests to validate Schema Introspection --- .../SchemaIntrospectionServiceImplTest.java | 277 ++++++++++++++++++ .../SchemaIntrospectionControllerTest.java | 131 +++++++++ .../controller/SchemaIntrospectionTest.java | 82 ++++++ 3 files changed, 490 insertions(+) create mode 100644 omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java create mode 100644 omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionControllerTest.java create mode 100644 omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionTest.java diff --git a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java new file mode 100644 index 000000000..958dd2e4e --- /dev/null +++ b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java @@ -0,0 +1,277 @@ +/** + * 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.api.impl; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.openmrs.Patient; +import org.openmrs.Person; +import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.resource.api.Resource; +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.DelegatingSubResource; +import org.openmrs.module.webservices.rest.web.representation.Representation; +import org.openmrs.module.webservices.rest.web.response.ResourceDoesNotSupportOperationException; + +/** + * Tests for {@link SchemaIntrospectionServiceImpl}. + */ +public class SchemaIntrospectionServiceImplTest { + + private SchemaIntrospectionServiceImpl service; + + @Before + public void setup() { + service = new SchemaIntrospectionServiceImpl(); + } + + /** + * @see SchemaIntrospectionServiceImpl#getDelegateType(Resource) + */ + @Test + public void getDelegateType_shouldReturnNullForNullResource() { + // Test with null resource + assertThat(service.getDelegateType(null), is(nullValue())); + } + + /** + * @see SchemaIntrospectionServiceImpl#getDelegateType(Resource) + */ + @Test + public void getDelegateType_shouldReturnCorrectTypeForDelegatingCrudResource() { + // Test with a mock DelegatingCrudResource + TestPatientResource resource = new TestPatientResource(); + Class delegateType = service.getDelegateType(resource); + + assertThat(delegateType, is(notNullValue())); + assertThat(delegateType.getName(), is(Patient.class.getName())); + } + + /** + * @see SchemaIntrospectionServiceImpl#getDelegateType(Resource) + */ + @Test + public void getDelegateType_shouldReturnCorrectTypeForDelegatingSubResource() { + // Test with a mock DelegatingSubResource + TestPatientSubResource resource = new TestPatientSubResource(); + Class delegateType = service.getDelegateType(resource); + + assertThat(delegateType, is(notNullValue())); + assertThat(delegateType.getName(), is(Patient.class.getName())); + } + + /** + * @see SchemaIntrospectionServiceImpl#discoverAvailableProperties(Class) + */ + @Test + public void discoverAvailableProperties_shouldReturnEmptyMapForNullType() { + Map properties = service.discoverAvailableProperties(null); + + assertThat(properties, is(notNullValue())); + assertThat(properties.size(), is(0)); + } + + /** + * @see SchemaIntrospectionServiceImpl#discoverAvailableProperties(Class) + */ + @Test + public void discoverAvailableProperties_shouldIncludePropertiesFromGetters() { + Map properties = service.discoverAvailableProperties(Patient.class); + + // Test a few essential properties that should be discovered via getters + assertThat(properties, hasKey("uuid")); + assertThat(properties, hasKey("patientId")); + assertThat(properties, hasKey("identifiers")); + assertThat(properties, hasKey("person")); + assertThat(properties, hasKey("voided")); + } + + /** + * @see SchemaIntrospectionServiceImpl#discoverAvailableProperties(Class) + */ + @Test + public void discoverAvailableProperties_shouldIncludePropertiesFromSuperclasses() { + Map properties = service.discoverAvailableProperties(Patient.class); + + // Patient extends Person, so should have Person properties + assertThat(properties, hasKey("gender")); + assertThat(properties, hasKey("age")); + assertThat(properties, hasKey("birthdate")); + assertThat(properties, hasKey("names")); + assertThat(properties, hasKey("addresses")); + } + + /** + * @see SchemaIntrospectionServiceImpl#discoverAvailableProperties(Class) + */ + @Test + public void discoverAvailableProperties_shouldIncludePropertiesWithCorrectTypes() { + Map properties = service.discoverAvailableProperties(Patient.class); + + // Verify some property types + assertThat(properties, hasEntry("uuid", "String")); + assertThat(properties, hasEntry("voided", "Boolean")); + // Set of PatientIdentifier + assertThat(properties.get("identifiers").contains("Set"), is(true)); + } + + /** + * @see SchemaIntrospectionServiceImpl#discoverResourceProperties(Resource) + */ + @Test + public void discoverResourceProperties_shouldCombineGetDelegateTypeAndDiscoverAvailableProperties() { + TestPatientResource resource = new TestPatientResource(); + Map properties = service.discoverResourceProperties(resource); + + // Verify it discovered Patient properties + assertThat(properties, hasKey("uuid")); + assertThat(properties, hasKey("patientId")); + assertThat(properties, hasKey("identifiers")); + + // And Person properties (from superclass) + assertThat(properties, hasKey("gender")); + assertThat(properties, hasKey("birthdate")); + } + + /** + * Mock DelegatingCrudResource for testing + */ + private class TestPatientResource extends DelegatingCrudResource { + + @Override + public Patient newDelegate() { + return new Patient(); + } + + @Override + public Patient save(Patient delegate) { + return delegate; + } + + @Override + public Patient getByUniqueId(String uniqueId) { + return new Patient(); + } + + @Override + public void purge(Patient delegate, RequestContext context) { + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + return null; + } + + @Override + public void delete(Patient delegate, String reason, RequestContext context) throws ResourceDoesNotSupportOperationException { + throw new ResourceDoesNotSupportOperationException(); + } + } + + /** + * Mock DelegatingSubResource for testing + */ + private class TestPatientSubResource extends DelegatingSubResource { + + @Override + public Patient getByUniqueId(String uniqueId) { + return new Patient(); + } + + @Override + public Patient newDelegate() { + return new Patient(); + } + + @Override + public Patient save(Patient delegate) { + return delegate; + } + + @Override + public void purge(Patient delegate, RequestContext context) { + } + + @Override + public Person getParent(Patient instance) { + return instance.getPerson(); + } + + @Override + public void setParent(Patient instance, Person parent) { + // In actual Patient class, the setPerson method is generated by Hibernate + // For our test, we'll just ignore the call + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + return null; + } + + @Override + public org.openmrs.module.webservices.rest.web.resource.api.PageableResult doGetAll( + Person parent, + RequestContext context) { + return null; + } + + @Override + public void delete(Patient delegate, String reason, RequestContext context) throws ResourceDoesNotSupportOperationException { + throw new ResourceDoesNotSupportOperationException(); + } + } + + /** + * Mock TestPersonResource for testing sub-resources + */ + private class TestPersonResource extends DelegatingCrudResource { + + @Override + public Person newDelegate() { + return new Person(); + } + + @Override + public Person save(Person delegate) { + return delegate; + } + + @Override + public Person getByUniqueId(String uniqueId) { + return new Person(); + } + + @Override + public void purge(Person delegate, RequestContext context) { + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + return null; + } + + @Override + public void delete(Person delegate, String reason, RequestContext context) throws ResourceDoesNotSupportOperationException { + throw new ResourceDoesNotSupportOperationException(); + } + } +} \ No newline at end of file diff --git a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionControllerTest.java b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionControllerTest.java new file mode 100644 index 000000000..4fd48308e --- /dev/null +++ b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionControllerTest.java @@ -0,0 +1,131 @@ +/** + * 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.v1_0.controller; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.openmrs.Patient; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.api.RestService; +import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService; +import org.openmrs.module.webservices.rest.web.resource.api.Resource; +import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException; + +/** + * Tests for {@link SchemaIntrospectionController}. + */ +public class SchemaIntrospectionControllerTest { + + @Mock + private RestService restService; + + @Mock + private SchemaIntrospectionService schemaIntrospectionService; + + @InjectMocks + private SchemaIntrospectionController controller; + + private Resource mockResource; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mockResource = mock(Resource.class); + } + + /** + * @see SchemaIntrospectionController#getResourceProperties(String) + */ + @Test + public void getResourceProperties_shouldReturnPropertiesForValidResource() throws Exception { + // Setup mock behaviors + when(restService.getResourceByName(RestConstants.VERSION_1 + "/patient")).thenReturn(mockResource); + when(schemaIntrospectionService.getDelegateType(mockResource)).thenReturn((Class) Patient.class); + + // Setup mock properties + Map mockProperties = new HashMap(); + mockProperties.put("uuid", "String"); + mockProperties.put("patientId", "Integer"); + mockProperties.put("gender", "String"); + mockProperties.put("voided", "Boolean"); + + when(schemaIntrospectionService.discoverAvailableProperties(Patient.class)).thenReturn(mockProperties); + + // Call the controller method + SimpleObject result = controller.getResourceProperties("patient"); + + // Verify the result + assertThat(result, is(notNullValue())); + assertThat((String) result.get("resourceName"), is(equalTo("patient"))); + assertThat((String) result.get("delegateType"), is(equalTo(Patient.class.getName()))); + + // Verify the properties were included + @SuppressWarnings("unchecked") + Map resultProperties = (Map) result.get("discoverableProperties"); + assertThat(resultProperties, is(notNullValue())); + assertThat(resultProperties.size(), is(equalTo(4))); + assertThat(resultProperties.get("uuid"), is(equalTo("String"))); + assertThat(resultProperties.get("patientId"), is(equalTo("Integer"))); + assertThat(resultProperties.get("gender"), is(equalTo("String"))); + assertThat(resultProperties.get("voided"), is(equalTo("Boolean"))); + } + + /** + * @see SchemaIntrospectionController#getResourceProperties(String) + */ + @Test(expected = ObjectNotFoundException.class) + public void getResourceProperties_shouldThrowExceptionForNonExistentResource() throws Exception { + // Setup mock behavior for non-existent resource + when(restService.getResourceByName(anyString())).thenReturn(null); + + // This should throw ObjectNotFoundException + controller.getResourceProperties("nonexistent"); + } + + /** + * @see SchemaIntrospectionController#getResourceProperties(String) + */ + @Test(expected = ObjectNotFoundException.class) + public void getResourceProperties_shouldThrowExceptionWhenDelegateTypeCannotBeDetermined() throws Exception { + // Setup mock behaviors where resource is found but delegate type isn't + when(restService.getResourceByName(RestConstants.VERSION_1 + "/patient")).thenReturn(mockResource); + when(schemaIntrospectionService.getDelegateType(mockResource)).thenReturn(null); + + // This should throw ObjectNotFoundException + controller.getResourceProperties("patient"); + } + + /** + * @see SchemaIntrospectionController#getResourceProperties(String) + */ + @Test(expected = ObjectNotFoundException.class) + public void getResourceProperties_shouldThrowExceptionWhenResourceLookupFails() throws Exception { + // Setup mock behavior to throw an exception during resource lookup + when(restService.getResourceByName(anyString())).thenThrow(new RuntimeException("Resource lookup failed")); + + // This should throw ObjectNotFoundException + controller.getResourceProperties("patient"); + } +} \ No newline at end of file diff --git a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionTest.java b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionTest.java new file mode 100644 index 000000000..69f5e9384 --- /dev/null +++ b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/v1_0/controller/SchemaIntrospectionTest.java @@ -0,0 +1,82 @@ +/** + * 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.v1_0.controller; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasKey; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.openmrs.Patient; +import org.openmrs.module.webservices.rest.web.api.impl.SchemaIntrospectionServiceImpl; + +/** + * Tests for schema introspection functionality. + * This is a standard unit test rather than an integration test to avoid Spring context issues. + */ +public class SchemaIntrospectionTest { + + private SchemaIntrospectionServiceImpl service; + + @Before + public void setup() { + service = new SchemaIntrospectionServiceImpl(); + } + + /** + * Tests that reflection discovers expected properties on Patient + */ + @Test + public void shouldDiscoverPatientProperties() { + Map properties = service.discoverAvailableProperties(Patient.class); + + // Verify discovered properties + assertThat(properties, is(notNullValue())); + assertThat(properties.size(), is(greaterThan(10))); // Should have many properties + + // Check some essential Patient properties + assertThat(properties, hasKey("uuid")); + assertThat(properties, hasKey("patientId")); + assertThat(properties, hasKey("identifiers")); + assertThat(properties, hasKey("person")); + + // Check some properties inherited from Person + assertThat(properties, hasKey("gender")); + assertThat(properties, hasKey("birthdate")); + assertThat(properties, hasKey("names")); + assertThat(properties, hasKey("addresses")); + } + + /** + * Tests property discovery for another common class + */ + @Test + public void shouldDiscoverPropertiesFromGetters() { + // Use a simple class from the OpenMRS API + Map properties = service.discoverAvailableProperties(org.openmrs.Location.class); + + // Verify some expected properties + assertThat(properties, is(notNullValue())); + assertThat(properties.size(), is(greaterThan(5))); + assertThat(properties, hasKey("uuid")); + assertThat(properties, hasKey("name")); + assertThat(properties, hasKey("description")); + assertThat(properties, hasKey("address1")); // Fixed: Location uses address1, address2, etc. + assertThat(properties, hasKey("address2")); // Added another address field to verify + assertThat(properties, hasKey("parentLocation")); + assertThat(properties, hasKey("childLocations")); + assertThat(properties, hasKey("retired")); + } +} \ No newline at end of file From 42e48f2485f49020188ca54cd6c2716df1a11f25 Mon Sep 17 00:00:00 2001 From: capernix Date: Wed, 30 Apr 2025 20:05:50 +0530 Subject: [PATCH 3/5] Added the feature of including properties from @PropertyGetter as well --- .../impl/SchemaIntrospectionServiceImpl.java | 49 +++++++++++- .../SchemaIntrospectionServiceImplTest.java | 80 +++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java index 312f36cf2..034549b88 100644 --- a/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java +++ b/omod-common/src/main/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImpl.java @@ -21,6 +21,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.api.impl.BaseOpenmrsService; +import org.openmrs.module.webservices.rest.web.annotation.PropertyGetter; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService; import org.openmrs.module.webservices.rest.web.resource.api.Resource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler; @@ -139,7 +141,52 @@ public Map discoverAvailableProperties(Class delegateType) { @Override public Map discoverResourceProperties(Resource resource) { Class delegateType = getDelegateType(resource); - return discoverAvailableProperties(delegateType); + Map properties = discoverAvailableProperties(delegateType); + + // Also discover properties defined by PropertyGetter and PropertySetter annotations + if (resource != null) { + discoverAnnotatedProperties(resource.getClass(), properties); + } + + return properties; + } + + /** + * Discovers properties defined by PropertyGetter and PropertySetter annotations in a resource class + * and its superclasses + * + * @param resourceClass The resource class to scan for annotations + * @param properties The map to add discovered properties to + */ + private void discoverAnnotatedProperties(Class resourceClass, Map properties) { + // Process the current class and all its superclasses + Class currentClass = resourceClass; + while (currentClass != null && !currentClass.equals(Object.class)) { + // Process all methods in the class + for (Method method : currentClass.getDeclaredMethods()) { + // Check for PropertyGetter annotation + PropertyGetter getter = method.getAnnotation(PropertyGetter.class); + if (getter != null) { + String propertyName = getter.value(); + Type returnType = method.getGenericReturnType(); + properties.put(propertyName, getTypeName(returnType)); + } + + // Check for PropertySetter annotation + PropertySetter setter = method.getAnnotation(PropertySetter.class); + if (setter != null && method.getParameterTypes().length > 1) { + String propertyName = setter.value(); + // For setters, use the type of the second parameter (after the delegate) + Type paramType = method.getGenericParameterTypes()[1]; + // Only add if not already discovered through a getter + if (!properties.containsKey(propertyName)) { + properties.put(propertyName, getTypeName(paramType)); + } + } + } + // Move up to the superclass + currentClass = currentClass.getSuperclass(); + } } /** diff --git a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java index 958dd2e4e..56ead9837 100644 --- a/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java +++ b/omod-common/src/test/java/org/openmrs/module/webservices/rest/web/api/impl/SchemaIntrospectionServiceImplTest.java @@ -18,13 +18,18 @@ import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.openmrs.Patient; +import org.openmrs.PatientIdentifier; import org.openmrs.Person; import org.openmrs.module.webservices.rest.web.RequestContext; +import org.openmrs.module.webservices.rest.web.annotation.PropertyGetter; +import org.openmrs.module.webservices.rest.web.annotation.PropertySetter; import org.openmrs.module.webservices.rest.web.resource.api.Resource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource; import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription; @@ -152,6 +157,29 @@ public void discoverResourceProperties_shouldCombineGetDelegateTypeAndDiscoverAv assertThat(properties, hasKey("birthdate")); } + /** + * @see SchemaIntrospectionServiceImpl#discoverResourceProperties(Resource) + */ + @Test + public void discoverResourceProperties_shouldIncludePropertiesFromAnnotations() { + TestPatientResourceWithAnnotations resource = new TestPatientResourceWithAnnotations(); + Map properties = service.discoverResourceProperties(resource); + + // Verify standard properties from the delegate type + assertThat(properties, hasKey("uuid")); + assertThat(properties, hasKey("patientId")); + + // Verify properties defined by @PropertyGetter annotations + assertThat(properties, hasKey("activeIdentifiers")); + assertThat(properties, hasKey("displayName")); + assertThat(properties, hasEntry("activeIdentifiers", "List")); + assertThat(properties, hasEntry("displayName", "String")); + + // Verify properties defined by @PropertySetter annotations + assertThat(properties, hasKey("preferredName")); + assertThat(properties, hasEntry("preferredName", "String")); + } + /** * Mock DelegatingCrudResource for testing */ @@ -187,6 +215,58 @@ public void delete(Patient delegate, String reason, RequestContext context) thro } } + /** + * Mock DelegatingCrudResource for testing PropertyGetter and PropertySetter annotations + */ + private class TestPatientResourceWithAnnotations extends DelegatingCrudResource { + + @PropertyGetter("activeIdentifiers") + public List getActiveIdentifiers(Patient patient) { + return new ArrayList(); + } + + @PropertyGetter("displayName") + public String getDisplayName(Patient patient) { + return patient.getPersonName().getFullName(); + } + + @PropertySetter("preferredName") + public void setPreferredName(Patient patient, String name) { + // Implementation not needed for test + } + + // Standard resource methods + + @Override + public Patient newDelegate() { + return new Patient(); + } + + @Override + public Patient save(Patient delegate) { + return delegate; + } + + @Override + public Patient getByUniqueId(String uniqueId) { + return new Patient(); + } + + @Override + public void purge(Patient delegate, RequestContext context) { + } + + @Override + public DelegatingResourceDescription getRepresentationDescription(Representation rep) { + return null; + } + + @Override + public void delete(Patient delegate, String reason, RequestContext context) throws ResourceDoesNotSupportOperationException { + throw new ResourceDoesNotSupportOperationException(); + } + } + /** * Mock DelegatingSubResource for testing */ From c9ffe2cdccaf24b75a95b0add00bb68dbfd62bbe Mon Sep 17 00:00:00 2001 From: capernix Date: Mon, 19 May 2025 21:39:34 +0530 Subject: [PATCH 4/5] Added a working Mojo Plugin --- omod/pom.xml | 17 ++++- .../openapi-generator-maven-plugin/README.md | 68 +++++++++++++++++ tools/openapi-generator-maven-plugin/pom.xml | 74 +++++++++++++++++++ .../java/org/openmrs/plugin/HelloMojo.java | 25 +++++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 tools/openapi-generator-maven-plugin/README.md create mode 100644 tools/openapi-generator-maven-plugin/pom.xml create mode 100644 tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java diff --git a/omod/pom.xml b/omod/pom.xml index d297407fc..b9b6b79d9 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -207,11 +207,26 @@
${project.parent.basedir}/license-header.txt
- + org.jacoco jacoco-maven-plugin + + + org.openmrs.plugin + openapi-generator-maven-plugin + 1.0-SNAPSHOT + + + say-hello + process-classes + + hello + + + + diff --git a/tools/openapi-generator-maven-plugin/README.md b/tools/openapi-generator-maven-plugin/README.md new file mode 100644 index 000000000..a20d0f55c --- /dev/null +++ b/tools/openapi-generator-maven-plugin/README.md @@ -0,0 +1,68 @@ +# OpenAPI Generator Maven Plugin + +## Overview + +The `openapi-generator-maven-plugin` is a custom Maven plugin designed for the OpenMRS ecosystem. Its primary function is to print a friendly message during the Maven build lifecycle, serving as a foundational tool for future enhancements related to OpenAPI specification generation. + +## Purpose + +This plugin aims to facilitate integration with OpenMRS modules, particularly the `webservices.rest` module, by providing a simple yet effective way to execute custom build actions. + +## Features + +- **Maven Goal**: `hello` +- **Output**: Logs "Hello from OpenMRS Maven Plugin! 🎉" during the build process. +- **Lifecycle Phase**: Can be executed during the `process-classes` or `compile` phase. +- **Integration**: Designed to be easily integrated into OpenMRS modules. + +## Installation + +To install the plugin locally, navigate to the project directory and run: + +```bash +mvn clean install +``` + +This command will compile the plugin and install it into your local Maven repository. + +## Usage + +To use the plugin in your OpenMRS module, add the following configuration to your `pom.xml`: + +```xml + + org.openmrs.plugin + openapi-generator-maven-plugin + 1.0-SNAPSHOT + + + say-hello + process-classes + + hello + + + + +``` + +After adding the plugin configuration, run the build: + +```bash +mvn clean install +``` + +You should see the output: `Hello from OpenMRS Maven Plugin! 🎉` + +## Future Enhancements + +The plugin is intended to evolve with additional features, including: + +- Support for loading resource classes. +- Scanning for annotations like `@Resource` and `@DocumentedResource`. +- Generating OpenAPI JSON specifications. +- Archiving documentation into module artifacts. + +## Contribution + +Contributions to enhance the functionality of this plugin are welcome. Please ensure to follow the project's coding standards and guidelines when submitting changes. \ No newline at end of file diff --git a/tools/openapi-generator-maven-plugin/pom.xml b/tools/openapi-generator-maven-plugin/pom.xml new file mode 100644 index 000000000..4db375907 --- /dev/null +++ b/tools/openapi-generator-maven-plugin/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + + org.openmrs.plugin + openapi-generator-maven-plugin + 1.0-SNAPSHOT + maven-plugin + + OpenAPI Generator Maven Plugin + A Maven plugin for generating OpenAPI specifications for OpenMRS modules. + https://example.com/openapi-generator-maven-plugin + + + 1.8 + 1.8 + UTF-8 + + + + + + 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 + + + + + + + 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/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java b/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java new file mode 100644 index 000000000..616bad563 --- /dev/null +++ b/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java @@ -0,0 +1,25 @@ +/** + * 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; + +@Mojo(name = "hello", defaultPhase = LifecyclePhase.PROCESS_CLASSES) +public class HelloMojo extends AbstractMojo { + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + getLog().info("Hello from OpenMRS Maven Plugin!"); + } +} \ No newline at end of file From 066d0df0cb1a44a2ca637106e811057e715fd48c Mon Sep 17 00:00:00 2001 From: capernix Date: Tue, 20 May 2025 22:28:30 +0530 Subject: [PATCH 5/5] Added javaparser PoC successfully --- omod/pom.xml | 5 +- tools/openapi-generator-maven-plugin/pom.xml | 7 ++ .../java/org/openmrs/plugin/HelloMojo.java | 67 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/omod/pom.xml b/omod/pom.xml index b9b6b79d9..857af8d70 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -184,13 +184,12 @@ org.apache.maven.plugins maven-dependency-plugin - - + Expand resources unpack-dependencies - generate-resources + prepare-package ${project.parent.groupId} ${project.parent.artifactId}-omod-common diff --git a/tools/openapi-generator-maven-plugin/pom.xml b/tools/openapi-generator-maven-plugin/pom.xml index 4db375907..c0ce9d3d9 100644 --- a/tools/openapi-generator-maven-plugin/pom.xml +++ b/tools/openapi-generator-maven-plugin/pom.xml @@ -42,6 +42,13 @@ 3.8.6 provided + + + + com.github.javaparser + javaparser-core + 3.25.5 + diff --git a/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java b/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java index 616bad563..4ae82a3eb 100644 --- a/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java +++ b/tools/openapi-generator-maven-plugin/src/main/java/org/openmrs/plugin/HelloMojo.java @@ -15,11 +15,78 @@ 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.project.MavenProject; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Optional; @Mojo(name = "hello", defaultPhase = LifecyclePhase.PROCESS_CLASSES) public class HelloMojo extends AbstractMojo { + + private static final String TARGET_CLASS_NAME = "org.openmrs.module.webservices.rest.web.v1_0.resource.openmrs1_8.PatientResource1_8"; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info("Hello from OpenMRS Maven Plugin!"); + getLog().info("Starting Javadoc parsing for target class: " + TARGET_CLASS_NAME); + + // Convert fully qualified class name to relative path + String classRelativePath = TARGET_CLASS_NAME.replace('.', File.separatorChar) + ".java"; + + // Extract simple class name for later use + String targetClassSimpleName = TARGET_CLASS_NAME.substring(TARGET_CLASS_NAME.lastIndexOf('.') + 1); + + // Try to find the source file in compile source roots + File targetSourceFile = null; + List sourceRoots = project.getCompileSourceRoots(); + + for (String sourceRoot : sourceRoots) { + File possibleFile = new File(sourceRoot, classRelativePath); + if (possibleFile.exists()) { + targetSourceFile = possibleFile; + getLog().info("Found source file: " + possibleFile.getAbsolutePath()); + break; + } + } + + if (targetSourceFile == null) { + getLog().warn("Could not find source file for class: " + TARGET_CLASS_NAME); + return; + } + + // Parse with JavaParser + try { + CompilationUnit cu = StaticJavaParser.parse(targetSourceFile); + Optional classOptional = cu.getClassByName(targetClassSimpleName); + + if (classOptional.isPresent()) { + ClassOrInterfaceDeclaration classDecl = classOptional.get(); + Optional javadocOptional = classDecl.getJavadocComment(); + + if (javadocOptional.isPresent()) { + String javadocText = javadocOptional.get().getContent(); + getLog().info("Class Javadoc for " + TARGET_CLASS_NAME + ":\n" + javadocText); + } else { + getLog().info("No class-level Javadoc found for " + TARGET_CLASS_NAME); + } + } else { + getLog().warn("Could not find class " + targetClassSimpleName + " in parsed file " + targetSourceFile.getName()); + } + } catch (FileNotFoundException e) { + getLog().error("Could not find source file for Javadoc parsing: " + targetSourceFile.getAbsolutePath(), e); + } catch (Exception e) { + getLog().error("Error parsing Javadoc for " + TARGET_CLASS_NAME, e); + } } } \ No newline at end of file