001/*- 002 * #%L 003 * HAPI FHIR - Master Data Management 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.mdm.svc; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.model.RequestPartitionId; 024import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 026import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 027import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 028import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc; 029import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 030import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 031import ca.uhn.fhir.mdm.api.paging.MdmPageRequest; 032import ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters; 033import ca.uhn.fhir.mdm.model.MdmTransactionContext; 034import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson; 035import ca.uhn.fhir.mdm.util.GoldenResourceHelper; 036import ca.uhn.fhir.rest.api.Constants; 037import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 038import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 039import ca.uhn.fhir.util.TerserUtil; 040import org.hl7.fhir.instance.model.api.IAnyResource; 041import org.hl7.fhir.instance.model.api.IBase; 042import org.hl7.fhir.instance.model.api.IBaseResource; 043import org.springframework.data.domain.Page; 044 045import java.util.ArrayList; 046import java.util.HashMap; 047import java.util.List; 048import java.util.Map; 049import java.util.regex.Pattern; 050import java.util.stream.Stream; 051 052public class MdmSurvivorshipSvcImpl implements IMdmSurvivorshipService { 053 private static final Pattern IS_UUID = 054 Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); 055 056 protected final FhirContext myFhirContext; 057 058 private final GoldenResourceHelper myGoldenResourceHelper; 059 060 private final DaoRegistry myDaoRegistry; 061 private final IMdmLinkQuerySvc myMdmLinkQuerySvc; 062 063 private final IIdHelperService<?> myIIdHelperService; 064 065 private final HapiTransactionService myTransactionService; 066 067 public MdmSurvivorshipSvcImpl( 068 FhirContext theFhirContext, 069 GoldenResourceHelper theResourceHelper, 070 DaoRegistry theDaoRegistry, 071 IMdmLinkQuerySvc theLinkQuerySvc, 072 IIdHelperService<?> theIIdHelperService, 073 HapiTransactionService theHapiTransactionService) { 074 myFhirContext = theFhirContext; 075 myGoldenResourceHelper = theResourceHelper; 076 myDaoRegistry = theDaoRegistry; 077 myMdmLinkQuerySvc = theLinkQuerySvc; 078 myIIdHelperService = theIIdHelperService; 079 myTransactionService = theHapiTransactionService; 080 } 081 082 // this logic is custom in smile vs hapi 083 @Override 084 public <T extends IBase> void applySurvivorshipRulesToGoldenResource( 085 T theTargetResource, T theGoldenResource, MdmTransactionContext theMdmTransactionContext) { 086 switch (theMdmTransactionContext.getRestOperation()) { 087 case MERGE_GOLDEN_RESOURCES: 088 TerserUtil.mergeFields( 089 myFhirContext, 090 (IBaseResource) theTargetResource, 091 (IBaseResource) theGoldenResource, 092 TerserUtil.EXCLUDE_IDS_AND_META); 093 break; 094 default: 095 TerserUtil.replaceFields( 096 myFhirContext, 097 (IBaseResource) theTargetResource, 098 (IBaseResource) theGoldenResource, 099 TerserUtil.EXCLUDE_IDS_AND_META); 100 break; 101 } 102 } 103 104 // This logic is the same for all implementations (including jpa or mongo) 105 @SuppressWarnings({"rawtypes", "unchecked"}) 106 @Override 107 public <T extends IBase> T rebuildGoldenResourceWithSurvivorshipRules( 108 T theGoldenResourceBase, MdmTransactionContext theMdmTransactionContext) { 109 IBaseResource goldenResource = (IBaseResource) theGoldenResourceBase; 110 111 // we want a list of source ids linked to this 112 // golden resource id; sorted and filtered for only MATCH results 113 Stream<IBaseResource> sourceResources = 114 getMatchedSourceIdsByLinkUpdateDate(goldenResource, theMdmTransactionContext); 115 116 IBaseResource toSave = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource( 117 (IAnyResource) goldenResource, 118 theMdmTransactionContext, 119 null // we don't want to apply survivorship - just create a new GoldenResource 120 ); 121 122 toSave.setId(goldenResource.getIdElement().toUnqualifiedVersionless()); 123 124 sourceResources.forEach(source -> { 125 applySurvivorshipRulesToGoldenResource(source, toSave, theMdmTransactionContext); 126 }); 127 128 // save it 129 IFhirResourceDao dao = myDaoRegistry.getResourceDao(goldenResource.fhirType()); 130 131 SystemRequestDetails requestDetails = new SystemRequestDetails(); 132 // if using partitions, we should save to the correct partition 133 Object resourcePartitionIdObj = toSave.getUserData(Constants.RESOURCE_PARTITION_ID); 134 if (resourcePartitionIdObj instanceof RequestPartitionId) { 135 RequestPartitionId partitionId = (RequestPartitionId) resourcePartitionIdObj; 136 requestDetails.setRequestPartitionId(partitionId); 137 } 138 dao.update(toSave, requestDetails); 139 140 return (T) toSave; 141 } 142 143 @SuppressWarnings("rawtypes") 144 private Stream<IBaseResource> getMatchedSourceIdsByLinkUpdateDate( 145 IBaseResource theGoldenResource, MdmTransactionContext theMdmTransactionContext) { 146 String resourceType = theGoldenResource.fhirType(); 147 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType); 148 149 MdmQuerySearchParameters searchParameters = new MdmQuerySearchParameters(new MdmPageRequest(0, 50, 50, 50)); 150 searchParameters.setGoldenResourceId(theGoldenResource.getIdElement()); 151 searchParameters.setSort("myUpdated"); 152 searchParameters.setMatchResult(MdmMatchResultEnum.MATCH); 153 Page<MdmLinkJson> linksQuery = myMdmLinkQuerySvc.queryLinks(searchParameters, theMdmTransactionContext); 154 155 // we want it ordered 156 List<String> sourceIds = new ArrayList<>(); 157 linksQuery.forEach(link -> { 158 String sourceId = link.getSourceId(); 159 // we want only the id part, not the resource type 160 sourceId = sourceId.replace(resourceType + "/", ""); 161 sourceIds.add(sourceId); 162 }); 163 Map<String, IResourcePersistentId> sourceIdToPid = new HashMap<>(); 164 if (!sourceIds.isEmpty()) { 165 // we cannot call resolveResourcePersistentIds if there are no ids to call it with 166 myTransactionService 167 .withRequest(new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.allPartitions())) 168 .execute(() -> { 169 Map<String, ? extends IResourcePersistentId> ids = 170 myIIdHelperService.resolveResourcePersistentIds( 171 RequestPartitionId.allPartitions(), resourceType, sourceIds); 172 sourceIdToPid.putAll(ids); 173 }); 174 } 175 176 return sourceIds.stream().map(id -> { 177 IResourcePersistentId<?> pid = sourceIdToPid.get(id); 178 return dao.readByPid(pid); 179 }); 180 } 181}