CMSgov/dpc-app

View on GitHub
dpc-common/src/main/java/gov/cms/dpc/fhir/converters/entities/ConsentEntityConverter.java

Summary

Maintainability
A
2 hrs
Test Coverage
package gov.cms.dpc.fhir.converters.entities;

import gov.cms.dpc.common.consent.entities.ConsentEntity;
import gov.cms.dpc.fhir.DPCIdentifierSystem;
import gov.cms.dpc.fhir.FHIRExtractors;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.utilities.xhtml.NodeType;
import org.hl7.fhir.utilities.xhtml.XhtmlNode;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static gov.cms.dpc.common.consent.entities.ConsentEntity.*;

/**
 * A utility class for converting a ConsentEntity into a FHIR Consent Resource.
 */
public class ConsentEntityConverter {
    /*
     * "magic string" uris for a PolicyRule, as defined
     * <a href="http://hl7.org/fhir/STU3/consent.html#6.2.5">by Hl7</a>
     */
    public static final String OPT_IN_MAGIC = "http://hl7.org/fhir/ConsentPolicy/opt-in";
    public static final String OPT_OUT_MAGIC = "http://hl7.org/fhir/ConsentPolicy/opt-out";
    public static final String SYSTEM_LOINC = "http://loinc.org";

    private ConsentEntityConverter() {}

    private static String patientIdentifier(DPCIdentifierSystem type, String value) {
        return String.format("%s|%s ", type.getSystem(), value);
    }

    private static Narrative narrativeText(String inOrOut, String hicn, String mbi) {
        boolean noHicn = hicn == null || hicn.isEmpty();
        boolean noMbi = mbi == null || mbi.isEmpty();

        StringBuilder sb = new StringBuilder("Consent status of ");
        sb.append(inOrOut);
        sb.append(" for the patient with identifiers [");
        sb.append(noHicn ? "" : patientIdentifier(DPCIdentifierSystem.HICN, hicn));
        sb.append((!noHicn && !noMbi) ? "], [" : "");
        sb.append(noMbi ? "" : patientIdentifier(DPCIdentifierSystem.MBI, mbi));
        sb.append("]");

        Narrative text = new Narrative();
        text.setDiv(new XhtmlNode(NodeType.Text).setValue(sb.toString()));
        text.setStatus(Narrative.NarrativeStatus.GENERATED);
        return text;
    }

    private static Reference patient(String fhirURL, String mbi) {
        if (mbi == null || mbi.isBlank()) {
            throw new IllegalArgumentException("ConsentEntity must have an MBI value");
        }

        return new Reference().setReference(String.format("%s/Patient?identity=|%s", fhirURL, mbi));
    }


    private static String policyRule(String value) {
        String code;
        if (OPT_IN.equals(value)) {
            code = OPT_IN_MAGIC;
        } else if (OPT_OUT.equals(value)) {
            code = OPT_OUT_MAGIC;
        } else {
            throw new IllegalArgumentException(String.format("invalid value %s; should be %s or %s", value, OPT_IN, OPT_OUT));
        }

        return code;
    }

    private static String policyUriToCode(String uri) {
        if (OPT_IN_MAGIC.equals(uri)) {
            return OPT_IN;
        } else if (OPT_OUT_MAGIC.equals(uri)) {
            return OPT_OUT;
        }
        throw new WebApplicationException(String.format("Policy rule must be %s or %s.", OPT_IN_MAGIC, OPT_OUT_MAGIC), Response.Status.BAD_REQUEST);
    }

    private static List<CodeableConcept> category(String loincCode) {
        // there must code to look up the code systems used in these CodeableConcept values. What is it?
        CodeableConcept category = new CodeableConcept();
        category.addCoding().setSystem(SYSTEM_LOINC).setCode(loincCode).setDisplay(ConsentEntity.CATEGORY_DISPLAY);
        return List.of(category);
    }

    private static String categoriesToLoincCode(List<CodeableConcept> categories) {
        if (categories == null || categories.size() != 1) {
            throw new WebApplicationException("Must include one category", HttpStatus.UNPROCESSABLE_ENTITY_422);
        }

        CodeableConcept category = categories.get(0);
        List<Coding> codings = category.getCoding();
        if (codings == null || codings.size() != 1) {
            throw new WebApplicationException("Category must have one coding", HttpStatus.UNPROCESSABLE_ENTITY_422);
        }

        Coding coding = category.getCodingFirstRep();
        if (!SYSTEM_LOINC.equals(coding.getSystem()) || !CATEGORY_LOINC_CODE.equals(coding.getCode())) {
            throw new WebApplicationException(String.format("Category coding must have system %s and code %s", SYSTEM_LOINC, CATEGORY_LOINC_CODE), HttpStatus.UNPROCESSABLE_ENTITY_422);
        }

        return coding.getCode();
    }

    private static String mbiFromPatientReference(String patientRefStr) {
        String mbi = "";
        Pattern patientIdPattern = Pattern.compile("/Patient\\?identity=\\|(?<mbi>\\d[a-zA-Z][a-zA-Z0-9]\\d[a-zA-Z][a-zA-Z0-9]\\d[a-zA-Z]{2}\\d{2})");
        Matcher matcher = patientIdPattern.matcher(patientRefStr);
        if (matcher.find()) {
            mbi = matcher.group("mbi");
        }
        return mbi;
    }

    @SuppressWarnings("JdkObsolete") // Date class is used by FHIR stu3 Consent model
    public static ConsentEntity fromFhir(Consent consent) {
        if (consent == null) {
            throw new WebApplicationException("No consent resource provided", Response.Status.BAD_REQUEST);
        }

        ConsentEntity entity = new ConsentEntity();

        String consentId = consent.getId();
        if (!StringUtils.isBlank(consentId)) {
            entity.setId(FHIRExtractors.getEntityUUID(consentId));
        }

        if (!Consent.ConsentState.ACTIVE.equals(consent.getStatus())) {
            throw new WebApplicationException("Only active consent records are accepted", HttpStatus.UNPROCESSABLE_ENTITY_422);
        }

        entity.setLoincCode(categoriesToLoincCode(consent.getCategory()));

        Reference patientRef = consent.getPatient();
        if (patientRef == null || StringUtils.isBlank(patientRef.getReference())) {
            throw new WebApplicationException("Consent resource must contain patient reference", Response.Status.BAD_REQUEST);
        }
        String mbi = mbiFromPatientReference(patientRef.getReference());
        if (StringUtils.isBlank(mbi)) {
            throw new WebApplicationException("Could not find MBI in patient reference", Response.Status.BAD_REQUEST);
        }
        entity.setMbi(mbi);

        Date dateTime = consent.getDateTime();
        LocalDate date = dateTime != null ? dateTime.toInstant().atOffset(ZoneOffset.UTC).toLocalDate() : LocalDate.now(ZoneOffset.UTC);
        entity.setEffectiveDate(date);
        entity.setPolicyCode(policyUriToCode(consent.getPolicyRule()));

        return entity;
    }

    @SuppressWarnings("JdkObsolete") // Date class is used by FHIR stu3 Consent model
    public static Consent toFhir(ConsentEntity consentEntity, String fhirURL) {
        Consent c = new Consent();

        c.setId(consentEntity.getId().toString());

        // there is no consent status in entity, so we are defaulting to active.
        c.setStatus(Consent.ConsentState.ACTIVE);

        c.setDateTime(Date.from(consentEntity.getEffectiveDate().atStartOfDay().atOffset(ZoneOffset.UTC).toInstant()));
        c.setText(narrativeText(consentEntity.getPolicyCode(), consentEntity.getHicn(), consentEntity.getMbi()));
        c.setPatient(patient(fhirURL, consentEntity.getMbi()));

        // PolicyRule is a CodeableConcept in r4 but is a uri in r3
        c.setPolicyRule(policyRule(consentEntity.getPolicyCode()));

        // scope is an r4 entity. in our data this is currently always "patient-privacy"
        // for the moment, we're not extending r3 to include it

        c.setCategory(category(consentEntity.getLoincCode()));

        return c;
    }
}