All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.cql.engine.model.FHIRModelResolver Maven / Gradle / Ivy

The newest version!
/*
 * (C) Copyright IBM Corp. 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package com.ibm.fhir.cql.engine.model;

import java.lang.reflect.Field;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.opencds.cqf.cql.engine.exception.InvalidCast;
import org.opencds.cqf.cql.engine.model.ModelResolver;

import com.ibm.fhir.cql.helpers.DateHelper;
import com.ibm.fhir.model.annotation.ReferenceTarget;
import com.ibm.fhir.model.type.Age;
import com.ibm.fhir.model.type.Canonical;
import com.ibm.fhir.model.type.Code;
import com.ibm.fhir.model.type.Count;
import com.ibm.fhir.model.type.Distance;
import com.ibm.fhir.model.type.Duration;
import com.ibm.fhir.model.type.Id;
import com.ibm.fhir.model.type.Markdown;
import com.ibm.fhir.model.type.MoneyQuantity;
import com.ibm.fhir.model.type.Oid;
import com.ibm.fhir.model.type.PositiveInt;
import com.ibm.fhir.model.type.Quantity;
import com.ibm.fhir.model.type.Reference;
import com.ibm.fhir.model.type.SimpleQuantity;
import com.ibm.fhir.model.type.UnsignedInt;
import com.ibm.fhir.model.type.Uri;
import com.ibm.fhir.model.type.Url;
import com.ibm.fhir.model.type.Uuid;
import com.ibm.fhir.model.util.ModelSupport;
import com.ibm.fhir.model.util.ModelSupport.ElementInfo;
import com.ibm.fhir.model.visitor.Visitable;
import com.ibm.fhir.path.FHIRPathNode;
import com.ibm.fhir.path.FHIRPathSystemValue;
import com.ibm.fhir.path.evaluator.FHIRPathEvaluator;
import com.ibm.fhir.path.exception.FHIRPathException;

/**
 * This is an implementation of the CQL Engine Model Resolver that supports
 * model resolution for IBM FHIR Server model objects.
 */
public class FHIRModelResolver implements ModelResolver {
    private static final Logger log = Logger.getLogger(FHIRModelResolver.class.getName());

    public static final String BASE_PACKAGE_NAME = "com.ibm.fhir.model";
    public static final String RESOURCE_PACKAGE_NAME = BASE_PACKAGE_NAME + ".resource";
    public static final String TYPE_PACKAGE_NAME = BASE_PACKAGE_NAME + ".type";
    public static final String CODE_PACKAGE_NAME = TYPE_PACKAGE_NAME + ".code";
    
    
    
    public static final String[] ALL_PACKAGES = new String[] {
            RESOURCE_PACKAGE_NAME,
            TYPE_PACKAGE_NAME,
            CODE_PACKAGE_NAME }; 

    public static final Pattern idPattern = Pattern.compile("(^|.+\\.)id$");
    
    public static final Pattern urlPattern = Pattern.compile("(^|.+\\.)url$");


    private static final Map> TYPE_MAP = buildTypeMap();

    private String packageName = BASE_PACKAGE_NAME;

    @Override
    public String getPackageName() {
        return this.packageName;
    }

    private static Map> buildTypeMap() {
        Map> typeMap = new LinkedHashMap<>();

        // add all model classes (resource / data types)
        for (Class modelClass : ModelSupport.getModelClasses()) {
            String typeName = modelClass.getName()
                    .replace("com.ibm.fhir.model.resource.", "")
                    .replace("com.ibm.fhir.model.type.", "")
                    .replace("$", ".");
            if (ModelSupport.isPrimitiveType(modelClass)) {
                String lcfirst = typeName.substring(0, 1)
                        .toLowerCase()
                        .concat(typeName.substring(1));
                typeMap.put(lcfirst, modelClass);
            }
            typeMap.put(typeName, modelClass);
        }

        // add all code subtypes
        for (Class codeSubtype : ModelSupport.getCodeSubtypes()) {
            typeMap.put(codeSubtype.getSimpleName(), codeSubtype);
        }

        return Collections.unmodifiableMap(typeMap);
    }

    private String toKey(String typeName) {
        switch (typeName) {
        case "NutritiionOrderIntent":
            return "NutritionOrderIntent";
        case "Messageheader_Response_Request":
        case "messageheaderResponseRequest":
            return "MessageHeaderResponseRequest";
        case "ConfidentialityClassification":
        case "vConfidentialityClassification":
            return "DocumentConfidentiality";
        case "ContractResourceStatusCodes":
            return "ContractStatus";
        case "EventStatus":
            return "ProcedureStatus";
        case "FinancialResourceStatusCodes":
            return "ClaimResponseStatus";
        case "SampledDataDataType":
            return "string";
        case "ClaimProcessingCodes":
            return "RemittanceOutcome";
        case "ContractResourcePublicationStatusCodes":
            return "ContractPublicationStatus";
        case "MedicationStatusCodes":
            return "MedicationStatus";
        case "ImmunizationEvaluationStatusCodes":
            return "ImmunizationEvaluationStatus";
        case "ImmunizationStatusCodes":
            return "ImmunizationStatus";
        case "ExpressionLanguage":
            return "Expression";
        case "CurrencyCode":
        case "MimeType":
        case "RequestResourceType":
            return "code";
        case "strandType":
        case "orientationType":
        case "repositoryType":
        case "qualityType":
        case "status":
        case "sequenceType":
            return typeName.substring(0, 1)
                    .toUpperCase()
                    .concat(typeName.substring(1));
        }
        return typeName;
    }

    @Override
    public void setPackageName(String packageName) {
        this.packageName = packageName;
    }

    @Override
    public Object resolvePath(Object target, String path ) {
        String[] identifiers = path.split("\\.");
        for (String identifier : identifiers) {
            // handling indexes: i.e. item[0].code
            if (identifier.contains("[")) {
                int index = Character.getNumericValue(identifier.charAt(identifier.indexOf("[") + 1));
                target = resolveProperty(target, identifier.replaceAll("\\[\\d\\]", ""));
                target = ((ArrayList) target).get(index);
            } else {
                target = resolveProperty(target, identifier);
            }
        }
        
        return target;
    }
    
    private Object resolveProperty(Object target, String path) {
        Object value = null;
        
        if( target != null ) {
            if( target instanceof Visitable) {
                Visitable visitable = (Visitable) target;
                try {
                    // is there such a thing as a compile step? Do we need to cache the compile like we do with regex?
                    Collection result = FHIRPathEvaluator.evaluator().evaluate(visitable, path);
                    if( ! result.isEmpty() ) {
                        Class clazz = target.getClass();
                        if( Code.class.isAssignableFrom(clazz) ) {
                            clazz = Code.class;
                        }
                        
                        ElementInfo elementInfo = ModelSupport.getElementInfo(clazz, path);
                        if( elementInfo.isRepeating() ) {
                            value = result.stream().map( n -> unpack(n, path) ).collect(Collectors.toList());
                        } else { 
                            value = unpack( result.iterator().next(), path);
                        }
                    }
                } catch( FHIRPathException fpex ) {
                    // intentionally empty
                }
            }
        }
        
        return value;
    }
    
    protected Object unpack(FHIRPathNode node, String path) {
        Object result = null;
        
        if( node.isResourceNode() ) {
            result = node.asResourceNode().resource();
        } else if( node.isElementNode() ) {
            result = node.asElementNode().element();
        } else if( node.isSystemValue() ) {
            FHIRPathSystemValue system = node.asSystemValue();
            if( system.isBooleanValue() ) {
                result = system.asBooleanValue()._boolean();
            } else if( system.isNumberValue() ) {
                result = system.asNumberValue().number();
            } else if( system.isQuantityValue() ) { 
                result = system.asQuantityValue().value();
            } else if( system.isStringValue() ) {
                result = system.asStringValue().string();
            } else if( system.isTemporalValue() ) {
                result = system.asTemporalValue().temporal();
            } else {
                throw new IllegalArgumentException("Unexpected node type " + node.type().toString() );
            }
        } else if( node.isTermServiceNode() ) {
            throw new UnsupportedOperationException("TermServiceNode");
        } else if( node.isTypeInfoNode() ) {
            result = node.asTypeInfoNode().typeInfo();
        }
        
        result = patchResult(path, result);
        
        return result;
    }



    protected Object patchResult(String path, Object result) {
        // Patch the model to match the CQL translator modelinfo expectations
        if( result instanceof java.lang.String ) {
            if( urlPattern.matcher(path).matches() ) {
                result = Uri.of((String) result);
            } else if( idPattern.matcher(path).matches() ) {
                result = com.ibm.fhir.model.type.String.of((String) result);
            }
        } else if( result instanceof TemporalAccessor ) {
            TemporalAccessor ta = (TemporalAccessor) result;
            result = DateHelper.toCqlTemporal(ta);
        } else if ( result instanceof byte[] ) {
            result = Base64.getEncoder().encode((byte[])result);
        } else if ( result instanceof Id ) {
            result = ((Id)result).getValue();
        }
        return result;
    }

    @Override
    public Object getContextPath(String contextType, String targetType) {
        Object result = null;
        if (targetType != null && contextType != null) {

            if (contextType != null && !(contextType.equals("Unspecified") || contextType.equals("Population"))) {
                if (targetType != null && contextType.equals(targetType)) {
                    result = "id";
                } else {
                    Class clazz = resolveType(targetType);

                    if (clazz != null) {
                        Set possibleAnswers = new HashSet();
                        getReferenceTargets(clazz, contextType, "", possibleAnswers);

                        if (possibleAnswers.isEmpty()) {
                            throw new IllegalArgumentException(String.format("Unable to determine context path value for context '%s' in type '%s'", contextType, targetType));
                        } else if (possibleAnswers.size() == 1) {
                            result = possibleAnswers.iterator().next();
                        } else {
                            if (contextType.equals("Patient")) {
                                String[] bestGuesses = new String[] { "subject", "patient", "beneficiary" };
                                for (String guess : bestGuesses) {
                                    if (possibleAnswers.contains(guess)) {
                                        result = guess;
                                        break;
                                    }
                                }
                            }
                            if (result == null) {
                                throw new IllegalArgumentException(String.format("ContextPath '%s' is ambiguous for type '%s'. Possible answers are '%s'.", contextType, targetType, possibleAnswers.toString()));
                            }
                        }
                    }
                }
            }
        }
        return result;
    }

    private void getReferenceTargets(Class clazz, String contextType, String prefix, Set possibleAnswers) {
        while (clazz != null && clazz.getPackage().getName().startsWith(this.packageName)) {

            getReferenceTargetFromFields(clazz, contextType, prefix, possibleAnswers);

            for (Class subclass : clazz.getDeclaredClasses()) {
                if (!subclass.getName().endsWith("$Builder")) {
                    String newPrefix = getPrefixed(prefix, StringUtils.uncapitalize(subclass.getSimpleName()));
                    getReferenceTargets(subclass, contextType, newPrefix, possibleAnswers);
                }
            }

            clazz = clazz.getSuperclass();
        }
    }

    private void getReferenceTargetFromFields(Class clazz, String contextType, String prefix, Set possibleAnswers) {
        for (Field field : clazz.getDeclaredFields()) {
            if (field.getType().isAssignableFrom(Reference.class)) {

                ReferenceTarget refTarget = field.getAnnotation(ReferenceTarget.class);
                if (refTarget != null) {
                    for (String target : refTarget.value()) {
                        if (target.equals(contextType)) {
                            possibleAnswers.add(getPrefixed(prefix, field.getName()));
                            break;
                        }
                    }
                } else {
                    // This handles the "Any" reference type
                    possibleAnswers.add(getPrefixed(prefix, field.getName()));
                }
            }
        }
    }

    private String getPrefixed(String prefix, String suffix) {
        if (prefix != null && prefix.length() > 0) {
            return prefix + "." + suffix;
        } else {
            return suffix;
        }
    }

    @Override
    public Class resolveType(String typeName) {
        Class result = TYPE_MAP.get(toKey(typeName));
        if (result == null) {
            if (log.isLoggable(Level.WARNING)) {
                log.warning("Failed to resolve type '" + typeName + "'");
            }
        }
        return result;
    }

    @Override
    public Class resolveType(Object value) {
        Class result = null;

        if (value != null) {
            result = resolveType(value.getClass().getSimpleName());
        } else {
            result = Object.class;
        }

        return result;
    }

    @Override
    public Boolean is(Object value, Class type) {
        Boolean result = null;
        if (value != null) {
            result = type.isAssignableFrom(value.getClass());
        }

        return result;
    }

    @Override
    public Object as(Object value, Class type, boolean isStrict) {
        Object result = null;
        if (value != null) {
            if (type.isAssignableFrom(value.getClass())) {
                result = value;
            } else {
                if (value instanceof Uri) {
                    Uri uri = (Uri) value;
                    switch (type.getSimpleName()) {
                    case "Url":
                        result = Url.of(uri.getValue());
                        break;
                    case "Canonical":
                        result = Canonical.of(uri.getValue());
                        break;
                    case "Uuid":
                        result = Uuid.of(uri.getValue());
                        break;
                    case "Oid":
                        result = Oid.of(uri.getValue());
                    }
                } else if (value instanceof com.ibm.fhir.model.type.Integer) {
                    com.ibm.fhir.model.type.Integer integer = (com.ibm.fhir.model.type.Integer) value;
                    switch (type.getSimpleName()) {
                    case "PositiveInt":
                        result = PositiveInt.of(integer.getValue());
                        break;
                    case "UnsignedInt":
                        result = UnsignedInt.of(integer.getValue());
                        break;
                    }
                } else if (value instanceof com.ibm.fhir.model.type.String) {
                    com.ibm.fhir.model.type.String string = (com.ibm.fhir.model.type.String) value;
                    switch (type.getSimpleName()) {
                    case "Code":
                        result = Code.of(string.getValue());
                        break;
                    case "Markdown":
                        result = Markdown.of(string.getValue());
                        break;
                    case "Id":
                        result = Id.of(string.getValue());
                        break;
                    }
                } else if (value instanceof Quantity) {
                    Quantity quantity = (Quantity) value;
                    Quantity.Builder builder = null;
                    switch (type.getSimpleName()) {
                    case "Age":
                        builder = Age.builder();
                        break;
                    case "Distance":
                        builder = Distance.builder();
                        break;
                    case "Duration":
                        builder = Duration.builder();
                        break;
                    case "Count":
                        builder = Count.builder();
                        break;
                    case "SimpleQuantity":
                        builder = SimpleQuantity.builder();
                        break;
                    case "MoneyQuantity":
                        builder = MoneyQuantity.builder();
                        break;
                    }
                    if (builder != null) {
                        // TODO - what about units? Should we also set those. The original code doesn't
                        result = builder.code(quantity.getCode()).value(quantity.getValue()).build();
                    }
                }
            }
        }

        if (result == null && isStrict) {
            throw new InvalidCast(String.format("Cannot cast a value of type %s as %s.", value.getClass().getName(), type.getName()));
        }

        return result;
    }

    @Override
    public Object createInstance(String typeName) {
        throw new UnsupportedOperationException("IBM FHIR model types must be constructed with a value or children");
        // Object result = null;
        //
        // Class clazz = resolveType(typeName);
        // if (clazz != null) {
        // // IBM FHIR model objects are constructed using nested builder classes
        // // They require a value and it isn't clear how we would provide that given
        // // the inputs provided.
        // try {
        // Method builderMethod = clazz.getMethod("builder");
        // if( builderMethod != null ) {
        // Builder builder = (Builder) builderMethod.invoke(null);
        // builder.id( UUID.randomUUID().toString() );
        // result = builder.build();
        // }
        // } catch (Exception iaex) {
        // throw new RuntimeException(iaex);
        // }
        // }
        //
        // return result;
    }

    @Override
    public void setValue(Object target, String path, Object value) {
        // TODO - consider implementing a pattern where new objects are constructed
        // each time this method is called. The models have the ability
        // to convert themselves to a builder using a toBuilder() function
        // which can them be appended to as needed.
        throw new UnsupportedOperationException("IBM FHIR model types are immutable");
    }

    @Override
    public Boolean objectEqual(Object left, Object right) {
        Boolean result = null;
        if (left != null && right != null) {
            result = left.equals(right);
        }
        return result;
    }

    @Override
    public Boolean objectEquivalent(Object left, Object right) {
        Boolean result = null;

        if (left == null && right == null) {
            result = true;
        } else if (left == null) {
            result = false;
        } else {
            result = objectEqual(left, right);
        }

        return result;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy