001package ca.uhn.fhir.jpa.batch.processor;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.fhirpath.IFhirPath;
027import ca.uhn.fhir.jpa.batch.config.BatchConstants;
028import ca.uhn.fhir.jpa.batch.log.Logs;
029import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc;
030import ca.uhn.fhir.util.ExtensionUtil;
031import ca.uhn.fhir.util.HapiExtensions;
032import ca.uhn.fhir.util.SearchParameterUtil;
033import org.apache.commons.lang3.StringUtils;
034import org.hl7.fhir.instance.model.api.IBaseExtension;
035import org.hl7.fhir.instance.model.api.IBaseReference;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037import org.jetbrains.annotations.NotNull;
038import org.slf4j.Logger;
039import org.springframework.batch.item.ItemProcessor;
040import org.springframework.beans.factory.annotation.Autowired;
041import org.springframework.beans.factory.annotation.Value;
042import org.springframework.lang.NonNull;
043
044import java.util.List;
045import java.util.Optional;
046
047import static ca.uhn.fhir.jpa.batch.config.BatchConstants.PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES;
048
049/**
050 * Reusable Item Processor which attaches an extension to any outgoing resource. This extension will contain a resource
051 * reference to the golden resource patient of the given resources' patient. (e.g. Observation.subject, Immunization.patient, etc)
052 */
053public class GoldenResourceAnnotatingProcessor implements ItemProcessor<List<IBaseResource>, List<IBaseResource>> {
054         private static final Logger ourLog = Logs.getBatchTroubleshootingLog();
055
056
057        @Value("#{stepExecutionContext['resourceType']}")
058        private String myResourceType;
059
060        @Autowired
061        private FhirContext myContext;
062
063        @Autowired
064        private MdmExpansionCacheSvc myMdmExpansionCacheSvc;
065
066        @Value("#{jobParameters['" + BatchConstants.EXPAND_MDM_PARAMETER + "'] ?: false}")
067        private boolean myMdmEnabled;
068
069
070        private RuntimeSearchParam myRuntimeSearchParam;
071
072        private String myPatientFhirPath;
073
074        private IFhirPath myFhirPath;
075
076        private void populateRuntimeSearchParam() {
077                Optional<RuntimeSearchParam> oPatientSearchParam= SearchParameterUtil.getOnlyPatientSearchParamForResourceType(myContext, myResourceType);
078                if (!oPatientSearchParam.isPresent()) {
079                        String errorMessage = String.format("[%s] has  no search parameters that are for patients, so it is invalid for Group Bulk Export!", myResourceType);
080                        throw new IllegalArgumentException(Msg.code(1279) + errorMessage);
081                } else {
082                        myRuntimeSearchParam = oPatientSearchParam.get();
083                }
084        }
085
086        @Override
087        public List<IBaseResource> process(@NonNull List<IBaseResource> theIBaseResources) throws Exception {
088                if (shouldAnnotateResource()) {
089                        lazyLoadSearchParamsAndFhirPath();
090                        theIBaseResources.forEach(this::annotateBackwardsReferences);
091                }
092                return theIBaseResources;
093        }
094
095        private void lazyLoadSearchParamsAndFhirPath() {
096                if (myRuntimeSearchParam == null) {
097                        populateRuntimeSearchParam();
098                }
099                if (myPatientFhirPath == null) {
100                        populatePatientFhirPath();
101                }
102        }
103
104        /**
105         * If the resource is added via a forward-reference from a patient, e.g. Patient.managingOrganization, we have no way to fetch the patient at this point in time. 
106         * This is a shortcoming of including the forward reference types in a Group/Patient bulk export.
107         * 
108         * @return true if the resource should be annotated with the golden resource patient reference
109         */
110        private boolean shouldAnnotateResource() {
111                return myMdmEnabled && !PATIENT_BULK_EXPORT_FORWARD_REFERENCE_RESOURCE_TYPES.contains(myResourceType);
112        }
113
114        private void annotateBackwardsReferences(IBaseResource iBaseResource) {
115                Optional<String> patientReference = getPatientReference(iBaseResource);
116                if (patientReference.isPresent()) {
117                        addGoldenResourceExtension(iBaseResource, patientReference.get());
118                } else {
119                        ourLog.error("Failed to find the patient reference information for resource {}. This is a bug, " +
120                                "as all resources which can be exported via Group Bulk Export must reference a patient.", iBaseResource);
121                }
122        }
123
124        private Optional<String> getPatientReference(IBaseResource iBaseResource) {
125                if (myResourceType.equalsIgnoreCase("Patient")) {
126                        return Optional.of(iBaseResource.getIdElement().getIdPart());
127                } else {
128                        Optional<IBaseReference> optionalReference = getFhirParser().evaluateFirst(iBaseResource, myPatientFhirPath, IBaseReference.class);
129                        if (optionalReference.isPresent()) {
130                                return optionalReference.map(theIBaseReference -> theIBaseReference.getReferenceElement().getIdPart());
131                        } else {
132                                return Optional.empty();
133                        }
134                }
135        }
136
137        private void addGoldenResourceExtension(IBaseResource iBaseResource, String sourceResourceId) {
138                String goldenResourceId = myMdmExpansionCacheSvc.getGoldenResourceId(sourceResourceId);
139                IBaseExtension<?, ?> extension = ExtensionUtil.getOrCreateExtension(iBaseResource, HapiExtensions.ASSOCIATED_GOLDEN_RESOURCE_EXTENSION_URL);
140                if (!StringUtils.isBlank(goldenResourceId)) {
141                        ExtensionUtil.setExtension(myContext, extension, "reference", prefixPatient(goldenResourceId));
142                }
143        }
144
145        private String prefixPatient(String theResourceId) {
146                return "Patient/" + theResourceId;
147        }
148
149        private IFhirPath getFhirParser() {
150                if (myFhirPath == null) {
151                        myFhirPath = myContext.newFhirPath();
152                }
153                return myFhirPath;
154        }
155
156        private String populatePatientFhirPath() {
157                if (myPatientFhirPath == null) {
158                        myPatientFhirPath = myRuntimeSearchParam.getPath();
159                        // GGG: Yes this is a stupid hack, but by default this runtime search param will return stuff like
160                        // Observation.subject.where(resolve() is Patient) which unfortunately our FHIRpath evaluator doesn't play nicely with
161                        // our FHIRPath evaluator.
162                        if (myPatientFhirPath.contains(".where")) {
163                                myPatientFhirPath = myPatientFhirPath.substring(0, myPatientFhirPath.indexOf(".where"));
164                        }
165                }
166                return myPatientFhirPath;
167        }
168}