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