001/*- 002 * #%L 003 * HAPI FHIR - Master Data Management 004 * %% 005 * Copyright (C) 2014 - 2025 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.api.svc.ResolveIdentityMode; 028import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 029import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 030import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc; 031import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 032import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 033import ca.uhn.fhir.mdm.api.paging.MdmPageRequest; 034import ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters; 035import ca.uhn.fhir.mdm.model.MdmTransactionContext; 036import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson; 037import ca.uhn.fhir.mdm.util.GoldenResourceHelper; 038import ca.uhn.fhir.rest.api.Constants; 039import ca.uhn.fhir.rest.api.server.RequestDetails; 040import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 041import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 042import ca.uhn.fhir.util.TerserUtil; 043import org.hl7.fhir.instance.model.api.IAnyResource; 044import org.hl7.fhir.instance.model.api.IBase; 045import org.hl7.fhir.instance.model.api.IBaseResource; 046import org.hl7.fhir.instance.model.api.IIdType; 047import org.springframework.data.domain.Page; 048 049import java.util.ArrayList; 050import java.util.HashMap; 051import java.util.List; 052import java.util.Map; 053import java.util.regex.Pattern; 054import java.util.stream.Stream; 055 056public class MdmSurvivorshipSvcImpl implements IMdmSurvivorshipService { 057 private static final Pattern IS_UUID = 058 Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); 059 060 protected final FhirContext myFhirContext; 061 062 private final GoldenResourceHelper myGoldenResourceHelper; 063 064 private final DaoRegistry myDaoRegistry; 065 private final IMdmLinkQuerySvc myMdmLinkQuerySvc; 066 067 private final IIdHelperService<?> myIIdHelperService; 068 069 private final HapiTransactionService myTransactionService; 070 071 public MdmSurvivorshipSvcImpl( 072 FhirContext theFhirContext, 073 GoldenResourceHelper theResourceHelper, 074 DaoRegistry theDaoRegistry, 075 IMdmLinkQuerySvc theLinkQuerySvc, 076 IIdHelperService<?> theIIdHelperService, 077 HapiTransactionService theHapiTransactionService) { 078 myFhirContext = theFhirContext; 079 myGoldenResourceHelper = theResourceHelper; 080 myDaoRegistry = theDaoRegistry; 081 myMdmLinkQuerySvc = theLinkQuerySvc; 082 myIIdHelperService = theIIdHelperService; 083 myTransactionService = theHapiTransactionService; 084 } 085 086 // this logic is custom in smile vs hapi 087 @Override 088 public <T extends IBase> void applySurvivorshipRulesToGoldenResource( 089 T theTargetResource, T theGoldenResource, MdmTransactionContext theMdmTransactionContext) { 090 switch (theMdmTransactionContext.getRestOperation()) { 091 case MERGE_GOLDEN_RESOURCES: 092 TerserUtil.mergeFields( 093 myFhirContext, 094 (IBaseResource) theTargetResource, 095 (IBaseResource) theGoldenResource, 096 TerserUtil.EXCLUDE_IDS_AND_META); 097 break; 098 default: 099 TerserUtil.replaceFields( 100 myFhirContext, 101 (IBaseResource) theTargetResource, 102 (IBaseResource) theGoldenResource, 103 TerserUtil.EXCLUDE_IDS_AND_META); 104 break; 105 } 106 } 107 108 // This logic is the same for all implementations (including jpa or mongo) 109 @SuppressWarnings({"rawtypes", "unchecked"}) 110 @Override 111 public <T extends IBase> T rebuildGoldenResourceWithSurvivorshipRules( 112 RequestDetails theRequestDetails, T theGoldenResourceBase, MdmTransactionContext theMdmTransactionContext) { 113 IBaseResource goldenResource = (IBaseResource) theGoldenResourceBase; 114 115 // we want a list of source ids linked to this 116 // golden resource id; sorted and filtered for only MATCH results 117 Stream<IBaseResource> sourceResources = 118 getMatchedSourceIdsByLinkUpdateDate(theRequestDetails, goldenResource, theMdmTransactionContext); 119 120 IBaseResource toSave = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource( 121 (IAnyResource) goldenResource, 122 theMdmTransactionContext, 123 null // we don't want to apply survivorship - just create a new GoldenResource 124 ); 125 126 toSave.setId(goldenResource.getIdElement().toUnqualifiedVersionless()); 127 128 sourceResources.forEach(source -> { 129 applySurvivorshipRulesToGoldenResource(source, toSave, theMdmTransactionContext); 130 }); 131 132 // save it 133 IFhirResourceDao dao = myDaoRegistry.getResourceDao(goldenResource.fhirType()); 134 135 SystemRequestDetails requestDetails = new SystemRequestDetails(); 136 // if using partitions, we should save to the correct partition 137 Object resourcePartitionIdObj = toSave.getUserData(Constants.RESOURCE_PARTITION_ID); 138 if (resourcePartitionIdObj instanceof RequestPartitionId) { 139 RequestPartitionId partitionId = (RequestPartitionId) resourcePartitionIdObj; 140 requestDetails.setRequestPartitionId(partitionId); 141 } 142 dao.update(toSave, requestDetails); 143 144 return (T) toSave; 145 } 146 147 @SuppressWarnings("rawtypes") 148 private Stream<IBaseResource> getMatchedSourceIdsByLinkUpdateDate( 149 RequestDetails theRequestDetails, 150 IBaseResource theGoldenResource, 151 MdmTransactionContext theMdmTransactionContext) { 152 String resourceType = theGoldenResource.fhirType(); 153 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType); 154 155 MdmQuerySearchParameters searchParameters = new MdmQuerySearchParameters(new MdmPageRequest(0, 50, 50, 50)); 156 searchParameters.setGoldenResourceId(theGoldenResource.getIdElement()); 157 searchParameters.setSort("myUpdated"); 158 searchParameters.setMatchResult(MdmMatchResultEnum.MATCH); 159 Page<MdmLinkJson> linksQuery = myMdmLinkQuerySvc.queryLinks(searchParameters, theMdmTransactionContext); 160 161 // we want it ordered 162 List<IIdType> sourceIds = new ArrayList<>(); 163 linksQuery.forEach(link -> { 164 String sourceId = link.getSourceId(); 165 if (!sourceId.contains("/")) { 166 sourceId = resourceType + "/" + sourceId; 167 } 168 IIdType id = myFhirContext.getVersion().newIdType(); 169 id.setValue(sourceId); 170 sourceIds.add(id); 171 }); 172 Map<String, IResourcePersistentId> sourceIdToPid = new HashMap<>(); 173 if (!sourceIds.isEmpty()) { 174 // we cannot call resolveResourcePersistentIds if there are no ids to call it with 175 myTransactionService 176 .withRequest(new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.allPartitions())) 177 .execute(() -> { 178 Map<IIdType, ? extends IResourceLookup<?>> ids = myIIdHelperService.resolveResourceIdentities( 179 RequestPartitionId.allPartitions(), 180 sourceIds, 181 ResolveIdentityMode.includeDeleted().cacheOk()); 182 for (Map.Entry<IIdType, ? extends IResourceLookup<?>> entry : ids.entrySet()) { 183 sourceIdToPid.put( 184 entry.getKey().getIdPart(), entry.getValue().getPersistentId()); 185 } 186 }); 187 } 188 189 return sourceIds.stream().map(id -> { 190 IResourcePersistentId<?> pid = sourceIdToPid.get(id.getIdPart()); 191 return dao.readByPid(pid); 192 }); 193 } 194}