dpc-common/src/main/java/gov/cms/dpc/fhir/converters/FHIREntityConverter.java
package gov.cms.dpc.fhir.converters;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import gov.cms.dpc.fhir.converters.exceptions.DataTranslationException;
import gov.cms.dpc.fhir.converters.exceptions.FHIRConverterException;
import gov.cms.dpc.fhir.converters.exceptions.MissingConverterException;
import gov.cms.dpc.fhir.helpers.ServiceLoaderHelpers;
import org.hl7.fhir.dstu3.model.Base;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Conversion engine which handles converting between Java {@link Object} and their corresponding FHIR {@link org.hl7.fhir.dstu3.model.Resource} types.
* Converters, which implement the {@link FHIRConverter} interface are loaded via the corresponding {@link ServiceLoader}.
*/
public class FHIREntityConverter {
private static final Logger logger = LoggerFactory.getLogger(FHIREntityConverter.class);
private final Multimap<Class<? extends Base>, FHIRConverter<?, ?>> fhirResourceMap;
private final Multimap<Class<?>, FHIRConverter<?, ?>> javaClassMap;
private final Set<Integer> converterHash;
FHIREntityConverter() {
this.fhirResourceMap = ArrayListMultimap.create();
this.javaClassMap = ArrayListMultimap.create();
this.converterHash = new HashSet<>();
}
/**
* Attempt to register the given {@link FHIRConverter} with the entity converter
*
* @param converter - {@link FHIRConverter} to register
* @throws FHIRConverterException if a converter is already registered for the given FHIR/Java class pair
*/
public synchronized void addConverter(FHIRConverter<?, ?> converter) {
logger.debug("Attempting to add converter: {}", converter);
// See if we already have something like this
final int hash = Objects.hash(converter.getFHIRResource(), converter.getJavaClass());
if (this.converterHash.contains(hash)) {
throw new FHIRConverterException(String.format("Existing converter for %s and %s", converter.getFHIRResource().getName(), converter.getJavaClass().getName()));
}
this.fhirResourceMap.put(converter.getFHIRResource(), converter);
this.javaClassMap.put(converter.getJavaClass(), converter);
this.converterHash.add(hash);
}
/**
* Convert the given {@link Base} resource into a corresponding Java class
*
* @param targetClass - {@link Class} of {@link T} to convert FHIR resource into
* @param resource -{@link S} FHIR resource to convert
* @param <T> - {@link T} resulting Java class
* @param <S> - {@link S} generic type of FHIR resource
* @return - {@link T} converted Java object
* @throws MissingConverterException if no {@link FHIRConverter} is registered between the two classes
* @throws FHIRConverterException if the conversion process fails
*/
@SuppressWarnings("unchecked")
public <T, S extends Base> T fromFHIR(Class<T> targetClass, S resource) {
logger.debug("Finding converter from {} to {}", resource, targetClass);
final FHIRConverter<S, T> converter;
synchronized (this) {
converter = this.fhirResourceMap.get(resource.getClass())
.stream()
.filter(c -> c.getJavaClass().isAssignableFrom(targetClass))
.map(c -> (FHIRConverter<S, T>) c)
.findAny()
.orElseThrow(() -> new MissingConverterException(resource.getClass(), targetClass));
}
return handleConversion(() -> converter.fromFHIR(this, resource));
}
/**
* Convert the given Java object into a corresponding FHIR {@link Base} resource
*
* @param fhirClass - {@link T} target FHIR Resource to convert to
* @param source -{@link S} source Java object to convert
* @param <T> - {@link T} resulting FHIR Resource
* @param <S> - {@link S} generic type of Java source object
* @return - {@link T} converted FHIR Resource
* @throws MissingConverterException if no {@link FHIRConverter} is registered between the two classes
* @throws FHIRConverterException if the conversion process fails
*/
@SuppressWarnings("unchecked")
public <T extends Base, S> T toFHIR(Class<T> fhirClass, S source) {
logger.debug("Finding converter from {} to {}", source, fhirClass);
final FHIRConverter<T, S> converter;
synchronized (this) {
converter = this.javaClassMap.get(source.getClass())
.stream()
.filter(c -> c.getFHIRResource().isAssignableFrom(fhirClass))
.map(c -> (FHIRConverter<T, S>) c)
.findAny()
.orElseThrow(() -> new MissingConverterException(source.getClass(), fhirClass));
}
return handleConversion(() -> converter.toFHIR(this, source));
}
/**
* Create a new {@link FHIREntityConverter} using the default converters loaded by the {@link FHIRConverter} service loader
*
* @return - {@link FHIREntityConverter} with default set of converters
*/
public static FHIREntityConverter initialize() {
final List<FHIRConverter<?, ?>> converters = ServiceLoaderHelpers.getLoaderStream(FHIRConverter.class)
.map(l -> (FHIRConverter<?, ?>) l)
.collect(Collectors.toList());
return FHIREntityConverter.initialize(converters);
}
/**
* Create a new {@link FHIREntityConverter} with the given {@link Collection} of {@link FHIRConverter}s
*
* @param converters - {@link Collection} of {@link FHIRConverter} to register with converter
* @return - {@link FHIREntityConverter} with only the given {@link FHIRConverter}s registered
*/
static FHIREntityConverter initialize(Collection<FHIRConverter<?, ?>> converters) {
final FHIREntityConverter converter = new FHIREntityConverter();
converters
.forEach(converter::addConverter);
return converter;
}
private static <T> T handleConversion(Supplier<T> converter) {
try {
return converter.get();
} catch (DataTranslationException e) {
logger.error("Cannot convert resources.", e);
throw e;
} catch (Exception e) {
logger.error("Unknown exception thrown during conversion", e);
throw new FHIRConverterException("Cannot convert resources", e);
}
}
}