
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}