/**
* 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;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.openmrs.Auditable;
import org.openmrs.Retireable;
import org.openmrs.Voidable;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.module.webservices.rest.SimpleObject;
import org.openmrs.module.webservices.rest.web.api.RestService;
import org.openmrs.module.webservices.rest.web.representation.DefaultRepresentation;
import org.openmrs.module.webservices.rest.web.representation.Representation;
import org.openmrs.module.webservices.rest.web.resource.api.Converter;
import org.openmrs.module.webservices.rest.web.resource.api.Resource;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceDescription.Property;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingResourceHandler;
import org.openmrs.module.webservices.rest.web.response.ConversionException;
import org.openmrs.util.HandlerUtil;
import org.openmrs.util.LocaleUtility;
public class ConversionUtil {
static final Log log = LogFactory.getLog(ConversionUtil.class);
// This would better be a Map<Pair<Class, String>, Type> but adding the dependency for
// org.apache.commons.lang3.tuple.Pair (through omrs-api) messed up other tests
private static Map<String, Type> typeVariableMap = new ConcurrentHashMap<String, Type>();
private static ConcurrentMap<Class<?>, Converter> converterCache;
private static Converter nullConverter;
static {
converterCache = new ConcurrentHashMap<Class<?>, Converter>();
nullConverter = new Converter() {
@Override
public Object newInstance(String type) {
return null;
}
@Override
public Object getByUniqueId(String string) {
return null;
}
@Override
public SimpleObject asRepresentation(Object instance, Representation rep) throws ConversionException {
return null;
}
@Override
public Object getProperty(Object instance, String propertyName) throws ConversionException {
return null;
}
@Override
public void setProperty(Object instance, String propertyName, Object value) throws ConversionException {
}
};
}
public static void clearCache() {
converterCache = new ConcurrentHashMap<Class<?>, Converter>();
}
@SuppressWarnings("unchecked")
public static <T> Converter<T> getConverter(Class<T> clazz) {
Converter<T> result = converterCache.get(clazz);
if (result != null) {
return result == nullConverter ? null : result;
}
try {
try {
Resource resource = Context.getService(RestService.class).getResourceBySupportedClass(clazz);
if (resource instanceof Converter) {
result = (Converter<T>) resource;
}
}
catch (APIException e) {}
if (result == null) {
result = HandlerUtil.getPreferredHandler(Converter.class, clazz);
}
}
catch (APIException ex) {
result = null;
}
// At this point, we don't really care if a result was found or not, we cache it regardless so that repeated
// searches are not performed.
if (result == null) {
converterCache.put(clazz, nullConverter);
} else {
converterCache.put(clazz, result);
}
return result;
}
/**
* Converts the given object to the given type
*
* @param object The value to convert
* @param toType The type to convert the value to
* @param instance The source object instance
* @return The specified object converted to the specified type
* @should resolve TypeVariables to actual type
*/
public static Object convert(Object object, Type toType, Object instance) throws ConversionException {
if (instance != null && toType instanceof TypeVariable<?>) {
TypeVariable<?> temp = ((TypeVariable<?>) toType);
toType = getTypeVariableClass(instance.getClass(), temp);
}
return convert(object, toType);
}
/**
* Converts the given object to the given type
*
* @param object
* @param toType a simple class or generic type
* @return
* @throws ConversionException
* @should convert strings to locales
* @should convert strings to enum values
* @should convert to an array
* @should convert to a class
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Object convert(Object object, Type toType) throws ConversionException {
if (object == null)
return object;
Class<?> toClass = toType instanceof Class ? ((Class<?>) toType) : (Class<?>) (((ParameterizedType) toType)
.getRawType());
// if we're trying to convert _to_ a collection, handle it as a special case
if (Collection.class.isAssignableFrom(toClass) || toClass.isArray()) {
if (!(object instanceof Collection))
throw new ConversionException("Can only convert a Collection to a Collection/Array. Not "
+ object.getClass() + " to " + toType, null);
if (toClass.isArray()) {
Class<?> targetElementType = toClass.getComponentType();
Collection input = (Collection) object;
Object ret = Array.newInstance(targetElementType, input.size());
int i = 0;
for (Object element : (Collection) object) {
Array.set(ret, i, convert(element, targetElementType));
++i;
}
return ret;
}
Collection ret = null;
if (SortedSet.class.isAssignableFrom(toClass)) {
ret = new TreeSet();
} else if (Set.class.isAssignableFrom(toClass)) {
ret = new HashSet();
} else if (List.class.isAssignableFrom(toClass)) {
ret = new ArrayList();
} else {
throw new ConversionException("Don't know how to handle collection class: " + toClass, null);
}
if (toType instanceof ParameterizedType) {
// if we have generic type information for the target collection, we can use it to do conversion
ParameterizedType toParameterizedType = (ParameterizedType) toType;
Type targetElementType = toParameterizedType.getActualTypeArguments()[0];
for (Object element : (Collection) object)
ret.add(convert(element, targetElementType));
} else {
// otherwise we must just add all items in a non-type-safe manner
ret.addAll((Collection) object);
}
return ret;
}
// otherwise we're converting _to_ a non-collection type
if (toClass.isAssignableFrom(object.getClass()))
return object;
// Numbers with a decimal are always assumed to be Double, so convert to Float, if necessary
if (toClass.isAssignableFrom(Float.class) && object instanceof Double) {
return new Float((Double) object);
}
if (object instanceof String) {
String string = (String) object;
Converter<?> converter = getConverter(toClass);
if (converter != null)
return converter.getByUniqueId(string);
if (toClass.isAssignableFrom(Date.class)) {
IllegalArgumentException pex = null;
String[] supportedFormats = { "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ssXXX", "yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd" };
for (int i = 0; i < supportedFormats.length; i++) {
try {
Date date = DateTime.parse(string, DateTimeFormat.forPattern(supportedFormats[i])).toDate();
return date;
}
catch (IllegalArgumentException ex) {
pex = ex;
}
}
throw new ConversionException(
"Error converting date - correct format (ISO8601 Long): yyyy-MM-dd'T'HH:mm:ss.SSSZ", pex);
} else if (toClass.isAssignableFrom(Locale.class)) {
return LocaleUtility.fromSpecification(object.toString());
} else if (toClass.isEnum()) {
return Enum.valueOf((Class<? extends Enum>) toClass, object.toString().toUpperCase());
} else if (toClass.isAssignableFrom(Class.class)) {
try {
return Context.loadClass((String) object);
}
catch (ClassNotFoundException e) {
throw new ConversionException("Could not convert from " + object.getClass() + " to " + toType, e);
}
}
// look for a static valueOf(String) method (e.g. Double, Integer, Boolean)
try {
Method method = toClass.getMethod("valueOf", String.class);
if (Modifier.isStatic(method.getModifiers()) && toClass.isAssignableFrom(method.getReturnType())) {
return method.invoke(null, string);
}
}
catch (Exception ex) {}
} else if (object instanceof Map) {
return convertMap((Map<String, ?>) object, toClass);
}
if (toClass.isAssignableFrom(Double.class) && object instanceof Number) {
return ((Number) object).doubleValue();
} else if (toClass.isAssignableFrom(Integer.class) && object instanceof Number) {
return ((Number) object).intValue();
}
if (toClass.isAssignableFrom(String.class) && object instanceof Boolean) {
return String.valueOf(object);
}
throw new ConversionException("Don't know how to convert from " + object.getClass() + " to " + toType, null);
}
/**
* Converts a map to the given type, using the registered converter
*
* @param map the map (typically a SimpleObject submitted as json) to convert
* @param toClass the class to convert map to
* @return the result of using a converter to instantiate a new class and set map's properties
* on it
* @throws ConversionException
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Object convertMap(Map<String, ?> map, Class<?> toClass) throws ConversionException {
// TODO handle refs by fetching the object at their URI
Converter converter = getConverter(toClass);
Object ret = null;
Object uuid = map.get(RestConstants.PROPERTY_UUID);
if (uuid instanceof String) {
ret = converter.getByUniqueId(uuid.toString());
}
if (ret == null) {
String type = (String) map.get(RestConstants.PROPERTY_FOR_TYPE);
ret = converter.newInstance(type);
}
// If the converter is a resource handler use the order of properties of its default representation
if (converter instanceof DelegatingResourceHandler) {
DelegatingResourceHandler handler = (DelegatingResourceHandler) converter;
DelegatingResourceDescription resDesc = handler.getRepresentationDescription(new DefaultRepresentation());
// Some resources do not have delegating resource description
if (resDesc != null) {
for (Map.Entry<String, Property> prop : resDesc.getProperties().entrySet()) {
if (map.containsKey(prop.getKey()) && !RestConstants.PROPERTY_FOR_TYPE.equals(prop.getKey())) {
converter.setProperty(ret, prop.getKey(), map.get(prop.getKey()));
}
}
}
}
for (Map.Entry<String, ?> prop : map.entrySet()) {
if (RestConstants.PROPERTY_FOR_TYPE.equals(prop.getKey()))
continue;
converter.setProperty(ret, prop.getKey(), prop.getValue());
}
return ret;
}
/**
* Gets a property from the delegate, with the given representation
*
* @param propertyName
* @param rep
* @return
* @throws ConversionException
*/
public static Object getPropertyWithRepresentation(Object bean, String propertyName, Representation rep)
throws ConversionException {
Object o;
try {
o = PropertyUtils.getProperty(bean, propertyName);
}
catch (Exception ex) {
throw new ConversionException(null, ex);
}
if (o instanceof Collection) {
List<Object> ret = new ArrayList<Object>();
for (Object element : (Collection<?>) o)
ret.add(convertToRepresentation(element, rep));
return ret;
} else {
o = convertToRepresentation(o, rep);
return o;
}
}
@SuppressWarnings("unchecked")
public static <S> Object convertToRepresentation(S o, Representation rep) throws ConversionException {
return convertToRepresentation(o, rep, (Converter) null);
}
@SuppressWarnings("unchecked")
public static <S> Object convertToRepresentation(S o, Representation rep, Class<?> convertAs) throws ConversionException {
Converter<?> converter = convertAs != null ? getConverter(convertAs) : null;
return convertToRepresentation(o, rep, converter);
}
public static <S> Object convertToRepresentation(S o, Representation rep, Converter specificConverter)
throws ConversionException {
if (o == null)
return null;
o = new HibernateLazyLoader().load(o);
if (o instanceof Collection) {
List ret = new ArrayList();
for (Object item : ((Collection) o)) {
ret.add(convertToRepresentation(item, rep, specificConverter));
}
return ret;
} else {
Converter<S> converter = specificConverter != null ? specificConverter : (Converter) getConverter(o.getClass());
if (converter == null) {
// try a few known datatypes
if (o instanceof Date) {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format((Date) o);
}
// otherwise we have no choice but to return the plain object
return o;
}
try {
return converter.asRepresentation(o, rep);
}
catch (Exception ex) {
throw new ConversionException("converting " + o.getClass() + " to " + rep, ex);
}
}
}
/**
* Gets the type for the specified generic type variable.
*
* @param instanceClass An instance of the class with the specified generic type variable.
* @param typeVariable The generic type variable.
* @return The actual type of the generic type variable or {@code null} if not found.
* @should return the actual type if defined on the parent class
* @should return the actual type if defined on the grand-parent class
* @should return null when actual type cannot be found
* @should return the correct actual type if there are multiple generic types
* @should throw IllegalArgumentException when instance class is null
* @should throw IllegalArgumentException when typeVariable is null
*/
public static Type getTypeVariableClass(Class<?> instanceClass, TypeVariable<?> typeVariable) {
if (instanceClass == null) {
throw new IllegalArgumentException("The instance class is required.");
}
if (typeVariable == null) {
throw new IllegalArgumentException("The type variable is required.");
}
String genericTypeName = typeVariable.getName();
Type type = instanceClass;
// Check to see if type variable has already been cached
Type result = typeVariableMap.get(instanceClass.getName().concat(genericTypeName));
// Walk the inheritance chain up and try to find the generic type with the specified name
while (result == null && type != null && !type.equals(Object.class)) {
if (type instanceof Class) {
type = ((Class) type).getGenericSuperclass();
} else {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?> rawType = (Class) parameterizedType.getRawType();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
for (int i = 0; i < actualTypeArguments.length; i++) {
String name = typeParameters[i].getName();
Type actualType = actualTypeArguments[i];
// Cache each generic type's actual type
typeVariableMap.put(instanceClass.getName().concat(name), actualType);
if (name.equals(genericTypeName)) {
// Found it
result = actualType;
break;
}
}
// Move up to the parent class
type = rawType.getGenericSuperclass();
}
}
return result;
}
/**
* Gets extra book-keeping info, for the full representation
*
* @param delegate
* @return
*/
public static SimpleObject getAuditInfo(Object delegate) {
SimpleObject ret = new SimpleObject();
if (delegate instanceof Auditable) {
Auditable auditable = (Auditable) delegate;
ret.put("creator", getPropertyWithRepresentation(auditable, "creator", Representation.REF));
ret.put("dateCreated", convertToRepresentation(auditable.getDateCreated(), Representation.DEFAULT));
ret.put("changedBy", getPropertyWithRepresentation(auditable, "changedBy", Representation.REF));
ret.put("dateChanged", convertToRepresentation(auditable.getDateChanged(), Representation.DEFAULT));
}
if (delegate instanceof Retireable) {
Retireable retireable = (Retireable) delegate;
if (retireable.isRetired()) {
ret.put("retiredBy", getPropertyWithRepresentation(retireable, "retiredBy", Representation.REF));
ret.put("dateRetired", convertToRepresentation(retireable.getDateRetired(), Representation.DEFAULT));
ret.put("retireReason", convertToRepresentation(retireable.getRetireReason(), Representation.DEFAULT));
}
}
if (delegate instanceof Voidable) {
Voidable voidable = (Voidable) delegate;
if (voidable.isVoided()) {
ret.put("voidedBy", getPropertyWithRepresentation(voidable, "voidedBy", Representation.REF));
ret.put("dateVoided", convertToRepresentation(voidable.getDateVoided(), Representation.DEFAULT));
ret.put("voidReason", convertToRepresentation(voidable.getVoidReason(), Representation.DEFAULT));
}
}
return ret;
}
}