001package ca.uhn.fhir.jpa.mdm.svc; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server - Master Data Management 006 * %% 007 * Copyright (C) 2014 - 2023 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.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 026import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; 027import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper; 028import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; 029import ca.uhn.fhir.mdm.api.IMdmLink; 030import ca.uhn.fhir.mdm.api.IMdmLinkSvc; 031import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 032import ca.uhn.fhir.mdm.api.MdmMatchOutcome; 033import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 034import ca.uhn.fhir.mdm.log.Logs; 035import ca.uhn.fhir.mdm.model.MdmTransactionContext; 036import ca.uhn.fhir.mdm.util.GoldenResourceHelper; 037import ca.uhn.fhir.mdm.util.MdmResourceUtil; 038import ca.uhn.fhir.rest.api.Constants; 039import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 040import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 041import org.hl7.fhir.instance.model.api.IAnyResource; 042import org.hl7.fhir.instance.model.api.IIdType; 043import org.slf4j.Logger; 044import org.springframework.beans.factory.annotation.Autowired; 045import org.springframework.stereotype.Service; 046import org.springframework.transaction.annotation.Transactional; 047 048import java.util.ArrayList; 049import java.util.List; 050import java.util.Optional; 051 052@Service 053public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { 054 055 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 056 057 @Autowired 058 GoldenResourceHelper myGoldenResourceHelper; 059 @Autowired 060 MdmLinkDaoSvc myMdmLinkDaoSvc; 061 @Autowired 062 IMdmLinkSvc myMdmLinkSvc; 063 @Autowired 064 IIdHelperService myIdHelperService; 065 @Autowired 066 MdmResourceDaoSvc myMdmResourceDaoSvc; 067 @Autowired 068 MdmPartitionHelper myMdmPartitionHelper; 069 070 @Override 071 @Transactional 072 public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theMergedResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) { 073 String resourceType = theMdmTransactionContext.getResourceType(); 074 075 if (theMergedResource != null) { 076 if (myGoldenResourceHelper.hasIdentifier(theMergedResource)) { 077 throw new IllegalArgumentException(Msg.code(751) + "Manually merged resource can not contain identifiers"); 078 } 079 myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theMergedResource, theMdmTransactionContext); 080 myGoldenResourceHelper.mergeIndentifierFields(theToGoldenResource, theMergedResource, theMdmTransactionContext); 081 082 theMergedResource.setId(theToGoldenResource.getId()); 083 theToGoldenResource = (IAnyResource) myMdmResourceDaoSvc.upsertGoldenResource(theMergedResource, resourceType).getResource(); 084 } else { 085 myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext); 086 myGoldenResourceHelper.mergeNonIdentiferFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext); 087 //Save changes to the golden resource 088 myMdmResourceDaoSvc.upsertGoldenResource(theToGoldenResource, resourceType); 089 } 090 091 // check if the golden resource and the source resource are in the same partition, throw error if not 092 myMdmPartitionHelper.validateResourcesInSamePartition(theFromGoldenResource, theToGoldenResource); 093 094 //Merge the links from the FROM to the TO resource. Clean up dangling links. 095 mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, theFromGoldenResource.getIdElement(), theMdmTransactionContext); 096 097 //Create the new REDIRECT link 098 addMergeLink(theToGoldenResource, theFromGoldenResource, resourceType, theMdmTransactionContext); 099 100 //Strip the golden resource tag from the now-deprecated resource. 101 myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType); 102 103 //Add the REDIRECT tag to that same deprecated resource. 104 MdmResourceUtil.setGoldenResourceRedirected(theFromGoldenResource); 105 106 //Save the deprecated resource. 107 myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType); 108 109 log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless() 110 + " into " + theToGoldenResource.getIdElement().toVersionless()); 111 return theToGoldenResource; 112 } 113 114 /** 115 * This connects 2 golden resources (GR and TR here) 116 * 117 * 1 Deletes any current links: TR, ?, ?, GR 118 * 2 Creates a new link: GR, MANUAL, REDIRECT, TR 119 * 120 * Before: 121 * TR -> GR 122 * 123 * After: 124 * GR -> TR 125 */ 126 private void addMergeLink( 127 IAnyResource theGoldenResource, 128 IAnyResource theTargetResource, 129 String theResourceType, 130 MdmTransactionContext theMdmTransactionContext 131 ) { 132 myMdmLinkSvc.deleteLink(theGoldenResource, theTargetResource, 133 theMdmTransactionContext); 134 135 myMdmLinkDaoSvc.createOrUpdateLinkEntity( 136 theTargetResource, // golden / LHS 137 theGoldenResource, // source / RHS 138 new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.REDIRECT), 139 MdmLinkSourceEnum.MANUAL, 140 theMdmTransactionContext // mdm transaction context 141 ); 142 } 143 144 private RequestPartitionId getPartitionIdForResource(IAnyResource theResource) { 145 RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID); 146 // TODO - this seems to be null on the put with 147 // client id (forced id). Is this a bug? 148 if (partitionId == null) { 149 partitionId = RequestPartitionId.allPartitions(); 150 } 151 return partitionId; 152 } 153 154 /** 155 * Helper method which performs merger of links between resources, and cleans up dangling links afterwards. 156 * <p> 157 * For each incomingLink, either ignore it, move it, or replace the original one 158 * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it. 159 * 2.a If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource 160 * 2.b If the link does not exist on the TO resource, but is actually self-referential, it will just be removed 161 * 3. If an incoming link is MANUAL, and there's a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one. 162 * 4. Manual link collisions cause invalid request exception. 163 * 164 * @param theFromResource 165 * @param theToResource 166 * @param theToResourcePid 167 * @param theMdmTransactionContext 168 */ 169 private void mergeGoldenResourceLinks( 170 IAnyResource theFromResource, 171 IAnyResource theToResource, 172 IIdType theToResourcePid, 173 MdmTransactionContext theMdmTransactionContext 174 ) { 175 // fromLinks - links from theFromResource to any resource 176 List<? extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); 177 // toLinks - links from theToResource to any resource 178 List<? extends IMdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); 179 List<IMdmLink> toDelete = new ArrayList<>(); 180 181 IResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds( 182 getPartitionIdForResource(theToResource), 183 theToResource.getIdElement().getResourceType(), 184 theToResource.getIdElement().getIdPart() 185 ); 186 187 // reassign links: 188 // to <- from 189 for (IMdmLink fromLink : fromLinks) { 190 Optional<? extends IMdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink); 191 if (optionalToLink.isPresent()) { 192 193 // The original links already contain this target, so move it over to the toResource 194 IMdmLink toLink = optionalToLink.get(); 195 if (fromLink.isManual()) { 196 switch (toLink.getLinkSource()) { 197 case AUTO: 198 //3 199 log(theMdmTransactionContext, String.format("MANUAL overrides AUT0. Deleting link %s", toLink.toString())); 200 myMdmLinkDaoSvc.deleteLink(toLink); 201 break; 202 case MANUAL: 203 if (fromLink.getMatchResult() != toLink.getMatchResult()) { 204 throw new InvalidRequestException(Msg.code(752) + "A MANUAL " + fromLink.getMatchResult() + " link may not be merged into a MANUAL " + toLink.getMatchResult() + " link for the same target"); 205 } 206 } 207 } else { 208 // 1 209 toDelete.add(fromLink); 210 continue; 211 } 212 } 213 214 if (fromLink.getSourcePersistenceId().equals(goldenResourcePid)) { 215 // 2.b if the link is going to be self-referential we'll just delete it 216 // (ie, do not link back to itself) 217 myMdmLinkDaoSvc.deleteLink(fromLink); 218 } else { 219 // 2.a The original TO links didn't contain this target, so move it over to the toGoldenResource. 220 fromLink.setGoldenResourcePersistenceId(goldenResourcePid); 221 ourLog.trace("Saving link {}", fromLink); 222 myMdmLinkDaoSvc.save(fromLink); 223 } 224 } 225 226 // 1 Delete dangling links 227 toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link)); 228 } 229 230 private Optional<? extends IMdmLink> findFirstLinkWithMatchingSource(List<? extends IMdmLink> theMdmLinks, IMdmLink theLinkWithSourceToMatch) { 231 return theMdmLinks.stream() 232 .filter(mdmLink -> mdmLink.getSourcePersistenceId().equals(theLinkWithSourceToMatch.getSourcePersistenceId())) 233 .findFirst(); 234 } 235 236 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 237 theMdmTransactionContext.addTransactionLogMessage(theMessage); 238 ourLog.debug(theMessage); 239 } 240}