Skip to content

RESTWS-980: Initial implementation of OpenAPI Generator Maven Plugin with JavaParser integration #657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<T>.
*
* @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<String, String> 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<String, String> discoverResourceProperties(Resource resource);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* 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.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;
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<T>
// or DelegatingSubResource<T, P, PR>
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<String, String> discoverAvailableProperties(Class<?> delegateType) {
if (delegateType == null) {
return new HashMap<String, String>();
}

Map<String, String> properties = new HashMap<String, String>();

// 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<String, String> discoverResourceProperties(Resource resource) {
Class<?> delegateType = getDelegateType(resource);
Map<String, String> 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<String, String> 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();
}
}

/**
* 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<String, String> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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;
}
}
Loading
Loading