Skip to content

Commit f87f12f

Browse files
committed
Implemented a method to find all properties of a given resource
1 parent e17c11e commit f87f12f

File tree

3 files changed

+341
-0
lines changed

3 files changed

+341
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.webservices.rest.web.api;
11+
12+
import java.util.Map;
13+
14+
import org.openmrs.module.webservices.rest.web.resource.api.Resource;
15+
16+
/**
17+
* Service for introspecting resource properties via reflection.
18+
* This enables clients to discover all properties that may be available in custom representations.
19+
*/
20+
public interface SchemaIntrospectionService {
21+
22+
/**
23+
* Gets the delegate type (T) for a resource that extends DelegatingCrudResource<T>.
24+
*
25+
* @param resource The resource whose delegate class we want to determine
26+
* @return The class type of the delegate, or null if not determinable
27+
*/
28+
Class<?> getDelegateType(Resource resource);
29+
30+
/**
31+
* Discovers all available properties on a delegate type using reflection.
32+
* This includes public instance fields and JavaBean-style getter methods.
33+
*
34+
* @param delegateType The class to introspect for properties
35+
* @return A map of property names to their Java type names
36+
*/
37+
Map<String, String> discoverAvailableProperties(Class<?> delegateType);
38+
39+
/**
40+
* Combines getDelegateType and discoverAvailableProperties for convenience.
41+
*
42+
* @param resource The resource to introspect
43+
* @return A map of property names to their Java type names
44+
*/
45+
Map<String, String> discoverResourceProperties(Resource resource);
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.webservices.rest.web.api.impl;
11+
12+
import java.beans.PropertyDescriptor;
13+
import java.lang.reflect.Field;
14+
import java.lang.reflect.Method;
15+
import java.lang.reflect.Modifier;
16+
import java.lang.reflect.ParameterizedType;
17+
import java.lang.reflect.Type;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
import org.openmrs.api.impl.BaseOpenmrsService;
24+
import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService;
25+
import org.openmrs.module.webservices.rest.web.resource.api.Resource;
26+
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler;
27+
import org.springframework.beans.BeanUtils;
28+
import org.springframework.stereotype.Component;
29+
30+
/**
31+
* Default implementation of {@link SchemaIntrospectionService}
32+
*/
33+
@Component
34+
public class SchemaIntrospectionServiceImpl extends BaseOpenmrsService implements SchemaIntrospectionService {
35+
36+
protected final Log log = LogFactory.getLog(getClass());
37+
38+
/**
39+
* @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#getDelegateType(Resource)
40+
*/
41+
@Override
42+
public Class<?> getDelegateType(Resource resource) {
43+
if (resource == null) {
44+
return null;
45+
}
46+
47+
// Handle non-delegating resources
48+
if (!(resource instanceof DelegatingResourceHandler)) {
49+
log.warn("Resource " + resource.getClass().getName() + " is not a DelegatingResourceHandler");
50+
return null;
51+
}
52+
53+
// Attempt to determine the delegate type from the generic parameter T in DelegatingCrudResource<T>
54+
// or DelegatingSubResource<T, P, PR>
55+
Class<?> resourceClass = resource.getClass();
56+
57+
// Search through the class hierarchy to find the class that implements/extends with the generic type
58+
while (resourceClass != null) {
59+
Type[] genericInterfaces = resourceClass.getGenericInterfaces();
60+
for (Type genericInterface : genericInterfaces) {
61+
if (genericInterface instanceof ParameterizedType) {
62+
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
63+
Type rawType = parameterizedType.getRawType();
64+
65+
// Check if this interface is DelegatingResourceHandler or a subinterface of it
66+
if (rawType instanceof Class
67+
&& DelegatingResourceHandler.class.isAssignableFrom((Class<?>) rawType)) {
68+
// First type parameter should be the delegate type
69+
Type[] typeArgs = parameterizedType.getActualTypeArguments();
70+
if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
71+
return (Class<?>) typeArgs[0];
72+
}
73+
}
74+
}
75+
}
76+
77+
// Check the superclass's generic type
78+
Type genericSuperclass = resourceClass.getGenericSuperclass();
79+
if (genericSuperclass instanceof ParameterizedType) {
80+
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
81+
Type[] typeArgs = parameterizedType.getActualTypeArguments();
82+
83+
// First type argument should be the delegate type
84+
if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
85+
return (Class<?>) typeArgs[0];
86+
}
87+
}
88+
89+
// Move up the hierarchy
90+
resourceClass = resourceClass.getSuperclass();
91+
}
92+
93+
log.warn("Could not determine delegate type for " + resource.getClass().getName());
94+
return null;
95+
}
96+
97+
/**
98+
* @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#discoverAvailableProperties(Class)
99+
*/
100+
@Override
101+
public Map<String, String> discoverAvailableProperties(Class<?> delegateType) {
102+
if (delegateType == null) {
103+
return new HashMap<String, String>();
104+
}
105+
106+
Map<String, String> properties = new HashMap<String, String>();
107+
108+
// Process fields up through the class hierarchy
109+
Class<?> currentClass = delegateType;
110+
while (currentClass != null && !currentClass.equals(Object.class)) {
111+
processFields(currentClass, properties);
112+
currentClass = currentClass.getSuperclass();
113+
}
114+
115+
// Process getters - use Spring's BeanUtils to get all property descriptors
116+
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(delegateType);
117+
for (PropertyDescriptor descriptor : propertyDescriptors) {
118+
// Skip "class" and properties without a readable method (getter)
119+
if ("class".equals(descriptor.getName()) || descriptor.getReadMethod() == null) {
120+
continue;
121+
}
122+
123+
Method readMethod = descriptor.getReadMethod();
124+
125+
// Only include public methods
126+
if (Modifier.isPublic(readMethod.getModifiers()) && !Modifier.isStatic(readMethod.getModifiers())) {
127+
// Get return type, including generic type if available
128+
String typeName = getTypeName(readMethod.getGenericReturnType());
129+
properties.put(descriptor.getName(), typeName);
130+
}
131+
}
132+
133+
return properties;
134+
}
135+
136+
/**
137+
* @see org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService#discoverResourceProperties(Resource)
138+
*/
139+
@Override
140+
public Map<String, String> discoverResourceProperties(Resource resource) {
141+
Class<?> delegateType = getDelegateType(resource);
142+
return discoverAvailableProperties(delegateType);
143+
}
144+
145+
/**
146+
* Helper method to process fields from a class and add them to the properties map
147+
*
148+
* @param clazz The class to process fields from
149+
* @param properties The map to add properties to
150+
*/
151+
private void processFields(Class<?> clazz, Map<String, String> properties) {
152+
Field[] fields = clazz.getDeclaredFields();
153+
for (Field field : fields) {
154+
// Only include public instance fields
155+
if (Modifier.isPublic(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
156+
String typeName = getTypeName(field.getGenericType());
157+
properties.put(field.getName(), typeName);
158+
}
159+
}
160+
}
161+
162+
/**
163+
* Helper method to get a user-friendly type name from a Type object
164+
*
165+
* @param type The type to get a name for
166+
* @return A user-friendly type name string
167+
*/
168+
private String getTypeName(Type type) {
169+
if (type instanceof Class) {
170+
return ((Class<?>) type).getSimpleName();
171+
} else if (type instanceof ParameterizedType) {
172+
ParameterizedType paramType = (ParameterizedType) type;
173+
Type rawType = paramType.getRawType();
174+
Type[] typeArgs = paramType.getActualTypeArguments();
175+
176+
StringBuilder sb = new StringBuilder();
177+
if (rawType instanceof Class) {
178+
sb.append(((Class<?>) rawType).getSimpleName());
179+
} else {
180+
sb.append(rawType.toString());
181+
}
182+
183+
if (typeArgs.length > 0) {
184+
sb.append("<");
185+
for (int i = 0; i < typeArgs.length; i++) {
186+
if (i > 0) {
187+
sb.append(", ");
188+
}
189+
if (typeArgs[i] instanceof Class) {
190+
sb.append(((Class<?>) typeArgs[i]).getSimpleName());
191+
} else {
192+
sb.append(typeArgs[i].toString());
193+
}
194+
}
195+
sb.append(">");
196+
}
197+
198+
return sb.toString();
199+
} else {
200+
return type.toString();
201+
}
202+
}
203+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.webservices.rest.web.v1_0.controller;
11+
12+
import java.util.Map;
13+
14+
import org.apache.commons.logging.Log;
15+
import org.apache.commons.logging.LogFactory;
16+
import org.openmrs.module.webservices.rest.SimpleObject;
17+
import org.openmrs.module.webservices.rest.web.RestConstants;
18+
import org.openmrs.module.webservices.rest.web.api.RestService;
19+
import org.openmrs.module.webservices.rest.web.api.SchemaIntrospectionService;
20+
import org.openmrs.module.webservices.rest.web.resource.api.Resource;
21+
import org.openmrs.module.webservices.rest.web.response.ObjectNotFoundException;
22+
import org.openmrs.module.webservices.rest.web.response.ResponseException;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.stereotype.Controller;
25+
import org.springframework.web.bind.annotation.PathVariable;
26+
import org.springframework.web.bind.annotation.RequestMapping;
27+
import org.springframework.web.bind.annotation.RequestMethod;
28+
import org.springframework.web.bind.annotation.ResponseBody;
29+
30+
/**
31+
* Controller that provides schema introspection for REST resources.
32+
* This allows clients to discover what properties are available for a given resource.
33+
*/
34+
@Controller
35+
@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/schemaintrospector")
36+
public class SchemaIntrospectionController extends BaseRestController {
37+
38+
protected final Log log = LogFactory.getLog(getClass());
39+
40+
@Autowired
41+
private RestService restService;
42+
43+
@Autowired
44+
private SchemaIntrospectionService schemaIntrospectionService;
45+
46+
/**
47+
* Gets all properties available on a resource's delegate domain object
48+
*
49+
* @param resourceName The name of the resource to introspect
50+
* @return A JSON object containing the resource properties
51+
* @throws ResponseException
52+
*/
53+
@RequestMapping(value = "/{resourceName}", method = RequestMethod.GET)
54+
@ResponseBody
55+
public SimpleObject getResourceProperties(@PathVariable("resourceName") String resourceName) throws ResponseException {
56+
// Get the resource by name with proper prefix
57+
Resource resource;
58+
try {
59+
// Try with the full path (which is how resources are registered)
60+
String fullResourceName = RestConstants.VERSION_1 + "/" + resourceName;
61+
log.debug("Looking for resource with name: " + fullResourceName);
62+
resource = restService.getResourceByName(fullResourceName);
63+
}
64+
catch (Exception e) {
65+
log.error("Failed to find resource with name: " + resourceName, e);
66+
throw new ObjectNotFoundException("Resource '" + resourceName + "' was not found");
67+
}
68+
69+
if (resource == null) {
70+
log.error("Resource is null for name: " + resourceName);
71+
throw new ObjectNotFoundException("Resource '" + resourceName + "' was not found");
72+
}
73+
74+
// Get the delegate type for the resource
75+
Class<?> delegateType = schemaIntrospectionService.getDelegateType(resource);
76+
if (delegateType == null) {
77+
log.error("Could not determine delegate type for resource: " + resourceName);
78+
throw new ObjectNotFoundException("Could not determine delegate type for resource '" + resourceName + "'");
79+
}
80+
81+
// Get properties for the delegate type
82+
Map<String, String> properties = schemaIntrospectionService.discoverAvailableProperties(delegateType);
83+
84+
// Build the response
85+
SimpleObject result = new SimpleObject();
86+
result.add("resourceName", resourceName);
87+
result.add("delegateType", delegateType.getName());
88+
result.add("discoverableProperties", properties);
89+
90+
return result;
91+
}
92+
}

0 commit comments

Comments
 (0)