CustomDatatypeUtil.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
package org.openmrs.customdatatype;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.ConceptDatatype;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.attribute.Attribute;
import org.openmrs.attribute.AttributeType;
import org.openmrs.serialization.SerializationException;
import org.openmrs.util.OpenmrsConstants;

/**
 * Helper methods for dealing with custom datatypes and their handlers
 * @since 1.9
 */
public class CustomDatatypeUtil {
	
	private static Log log = LogFactory.getLog(CustomDatatypeUtil.class);
	
	/**
	 * @param descriptor
	 * @return a configured datatype appropriate for descriptor
	 */
	public static CustomDatatype<?> getDatatype(CustomValueDescriptor descriptor) {
		return getDatatype(descriptor.getDatatypeClassname(), descriptor.getDatatypeConfig());
	}
	
	/**
	 * @param datatypeClassname
	 * @param datatypeConfig
	 * @return a configured datatype with the given classname and configuration
	 */
	public static CustomDatatype<?> getDatatype(String datatypeClassname, String datatypeConfig) {
		try {
			Class dtClass = Context.loadClass(datatypeClassname);
			CustomDatatype<?> ret = (CustomDatatype<?>) Context.getDatatypeService().getDatatype(dtClass, datatypeConfig);
			if (ret == null)
				throw new CustomDatatypeException("Can't find datatype: " + datatypeClassname);
			return ret;
		}
		catch (Exception ex) {
			throw new CustomDatatypeException("Error loading " + datatypeClassname + " and configuring it with "
			        + datatypeConfig, ex);
		}
	}
	
	/**
	 * @param descriptor
	 * @return a configured datatype appropriate for descriptor
	 */
	public static CustomDatatype<?> getDatatypeOrDefault(CustomValueDescriptor descriptor) {
		try {
			return getDatatype(descriptor);
		}
		catch (CustomDatatypeException ex) {
			return getDatatype(OpenmrsConstants.DEFAULT_CUSTOM_DATATYPE, null);
		}
	}
	
	/**
	 * @param descriptor
	 * @return a configured datatype handler appropriate for descriptor
	 */
	public static CustomDatatypeHandler getHandler(CustomValueDescriptor descriptor) {
		return getHandler(getDatatypeOrDefault(descriptor), descriptor.getPreferredHandlerClassname(), descriptor
		        .getHandlerConfig());
	}
	
	/**
	 * @param dt the datatype that this handler should be for
	 * @param preferredHandlerClassname
	 * @param handlerConfig
	 * @return a configured datatype handler with the given classname and configuration
	 */
	public static CustomDatatypeHandler getHandler(CustomDatatype<?> dt, String preferredHandlerClassname,
	        String handlerConfig) {
		if (preferredHandlerClassname != null) {
			try {
				Class<? extends CustomDatatypeHandler> clazz = (Class<? extends CustomDatatypeHandler>) Context
				        .loadClass(preferredHandlerClassname);
				CustomDatatypeHandler handler = clazz.newInstance();
				if (handlerConfig != null)
					handler.setHandlerConfiguration(handlerConfig);
				return handler;
			}
			catch (Exception ex) {
				log.warn("Failed to instantiate and configure preferred handler with class " + preferredHandlerClassname
				        + " and config " + handlerConfig, ex);
			}
		}
		
		// if we couldn't get the preferred handler (or none was specified) we get the default one by datatype
		return Context.getDatatypeService().getHandler(dt, handlerConfig);
	}
	
	/**
	 * Converts a simple String-based configuration to a serialized form.
	 * Utility method for {@link AttributeHandler}s that have property-style configuration.
	 * 
	 * @param simpleConfig
	 * @return
	 */
	public static String serializeSimpleConfiguration(Map<String, String> simpleConfig) {
		if (simpleConfig == null || simpleConfig.size() == 0)
			return "";
		try {
			return Context.getSerializationService().getDefaultSerializer().serialize(simpleConfig);
		}
		catch (SerializationException ex) {
			throw new APIException(ex);
		}
	}
	
	/**
	 * Deserializes a simple String-based configuration from the serialized form used by
	 * {@link serializeSimpleConfiguration} 
	 * Utility method for {@link AttributeHandler}s that have property-style configuration.
	 * 
	 * @param serializedConfig
	 * @return
	 * @should deserialize a configuration serialized by the corresponding serialize method
	 */
	@SuppressWarnings("unchecked")
	public static Map<String, String> deserializeSimpleConfiguration(String serializedConfig) {
		if (StringUtils.isBlank(serializedConfig))
			return Collections.emptyMap();
		try {
			return Context.getSerializationService().getDefaultSerializer().deserialize(serializedConfig, Map.class);
		}
		catch (SerializationException ex) {
			throw new APIException(ex);
		}
	}
	
	/**
	 * Uses the appropriate datatypes to convert all values in the input map to their valueReference equivalents.
	 * This is a convenience method for calling XyzService.getXyz(..., attributeValues, ...).
	 * 
	 * @param datatypeValues
	 * @return a map similar to the input parameter, but with typed values converted to their reference equivalents
	 */
	public static <T extends AttributeType<?>, U> Map<T, String> getValueReferences(Map<T, U> datatypeValues) {
		Map<T, String> serializedAttributeValues = null;
		if (datatypeValues != null) {
			serializedAttributeValues = new HashMap<T, String>();
			for (Map.Entry<T, U> e : datatypeValues.entrySet()) {
				T vat = e.getKey();
				CustomDatatype<U> customDatatype = (CustomDatatype<U>) getDatatype(vat);
				String valueReference;
				try {
					valueReference = customDatatype.getReferenceStringForValue(e.getValue());
				}
				catch (UnsupportedOperationException ex) {
					throw new APIException("Cannot search for attributes with custom datatype: " + customDatatype.getClass());
				}
				serializedAttributeValues.put(vat, valueReference);
			}
		}
		return serializedAttributeValues;
	}
	
	/**
	 * @return fully-qualified classnames of all registered datatypes
	 */
	public static List<String> getDatatypeClassnames() {
		List<String> ret = new ArrayList<String>();
		for (Class<?> c : Context.getDatatypeService().getAllDatatypeClasses())
			ret.add(c.getName());
		return ret;
	}
	
	/**
	 * @return full-qualified classnames of all registered handlers
	 */
	public static List<String> getHandlerClassnames() {
		List<String> ret = new ArrayList<String>();
		for (Class<?> c : Context.getDatatypeService().getAllHandlerClasses())
			ret.add(c.getName());
		return ret;
	}
	
	/**
	 * @param handler
	 * @param datatype
	 * @return whether or not handler is compatible with datatype
	 */
	public static boolean isCompatibleHandler(CustomDatatypeHandler handler, CustomDatatype<?> datatype) {
		List<Class<? extends CustomDatatypeHandler>> handlerClasses = Context.getDatatypeService().getHandlerClasses(
		    (Class<? extends CustomDatatype<?>>) datatype.getClass());
		return handlerClasses.contains(handler.getClass());
	}
	
	/**
	 * To be called by service save methods for customizable implementations.
	 * Iterates over all attributes and calls save on the {@link ConceptDatatype} for any dirty ones.
	 * 
	 * @param customizable
	 */
	public static void saveAttributesIfNecessary(Customizable<?> customizable) {
		// TODO decide whether we can move this into a SingleCustomValueSaveHandler instead of leaving it here to be called by each Customizable service's save method
		for (Attribute attr : customizable.getAttributes()) {
			saveIfDirty(attr);
		}
	}
	
	/**
	 * Calls the save method on value's {@link ConceptDatatype} if necessary
	 * 
	 * @param value
	 */
	public static void saveIfDirty(SingleCustomValue<?> value) {
		if (value.isDirty()) {
			CustomDatatype datatype = CustomDatatypeUtil.getDatatype(value.getDescriptor());
			if (value.getValue() == null)
				throw new InvalidCustomValueException(value.getClass() + " with type=" + value.getDescriptor()
				        + " cannot be null");
			String existingValueReference = null;
			try {
				existingValueReference = value.getValueReference();
			}
			catch (NotYetPersistedException ex) {
				// this is expected
			}
			String newValueReference = datatype.save(value.getValue(), existingValueReference);
			value.setValueReferenceInternal(newValueReference);
		}
		
	}
	
	/**
	 * Validates a {@link SingleCustomValue}
	 * 
	 * @param value
	 * @return true is value is valid, according to its configured datatype
	 */
	@SuppressWarnings("unchecked")
	public static <T, D extends CustomValueDescriptor> boolean validate(SingleCustomValue<D> value) {
		try {
			CustomDatatype<T> datatype = (CustomDatatype<T>) getDatatype(value.getDescriptor());
			datatype.validate((T) value.getValue());
			return true;
		}
		catch (Exception ex) {
			return false;
		}
	}
}