001package ca.uhn.fhir.jpa.mdm.svc; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server - Master Data Management 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.interceptor.model.RequestPartitionId; 025import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 026import ca.uhn.fhir.jpa.dao.index.IdHelperService; 027import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; 028import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper; 029import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; 030import ca.uhn.fhir.mdm.api.IMdmLink; 031import ca.uhn.fhir.mdm.api.IMdmLinkSvc; 032import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 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.ResourcePersistentId; 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); 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 private void addMergeLink(IAnyResource theGoldenResource, IAnyResource theTargetResource, String theResourceType) { 115 IMdmLink mdmLink = myMdmLinkDaoSvc 116 .getOrCreateMdmLinkByGoldenResourceAndSourceResource(theGoldenResource, theTargetResource); 117 118 mdmLink 119 .setMdmSourceType(theResourceType) 120 .setMatchResult(MdmMatchResultEnum.REDIRECT) 121 .setLinkSource(MdmLinkSourceEnum.MANUAL); 122 myMdmLinkDaoSvc.save(mdmLink); 123 } 124 125 126 /** 127 * Helper method which performs merger of links between resources, and cleans up dangling links afterwards. 128 * <p> 129 * For each incomingLink, either ignore it, move it, or replace the original one 130 * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it. 131 * 2. If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource 132 * 3. If an incoming link is MANUAL, and theres a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one. 133 * 4. Manual link collisions cause invalid request exception. 134 * 135 * @param theFromResource 136 * @param theToResource 137 * @param theToResourcePid 138 * @param theMdmTransactionContext 139 */ 140 private void mergeGoldenResourceLinks(IAnyResource theFromResource, IAnyResource theToResource, IIdType theToResourcePid, MdmTransactionContext theMdmTransactionContext) { 141 List<? extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource 142 List<? extends IMdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); // toLinks - links going to theToResource 143 List<IMdmLink> toDelete = new ArrayList<>(); 144 ResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds((RequestPartitionId) theToResource.getUserData(Constants.RESOURCE_PARTITION_ID), theToResource.getIdElement().getResourceType(), theToResource.getIdElement().getIdPart()); 145 146 for (IMdmLink fromLink : fromLinks) { 147 Optional<? extends IMdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink); 148 if (optionalToLink.isPresent()) { 149 150 // The original links already contain this target, so move it over to the toResource 151 IMdmLink toLink = optionalToLink.get(); 152 if (fromLink.isManual()) { 153 switch (toLink.getLinkSource()) { 154 case AUTO: 155 //3 156 log(theMdmTransactionContext, String.format("MANUAL overrides AUT0. Deleting link %s", toLink.toString())); 157 myMdmLinkDaoSvc.deleteLink(toLink); 158 break; 159 case MANUAL: 160 if (fromLink.getMatchResult() != toLink.getMatchResult()) { 161 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"); 162 } 163 } 164 } else { 165 //1 166 toDelete.add(fromLink); 167 continue; 168 } 169 } 170 //2 The original TO links didn't contain this target, so move it over to the toGoldenResource 171 fromLink.setGoldenResourcePersistenceId(goldenResourcePid); 172 ourLog.trace("Saving link {}", fromLink); 173 myMdmLinkDaoSvc.save(fromLink); 174 } 175 //1 Delete dangling links 176 toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link)); 177 } 178 179 private Optional<? extends IMdmLink> findFirstLinkWithMatchingSource(List<? extends IMdmLink> theMdmLinks, IMdmLink theLinkWithSourceToMatch) { 180 return theMdmLinks.stream() 181 .filter(mdmLink -> mdmLink.getSourcePersistenceId().equals(theLinkWithSourceToMatch.getSourcePersistenceId())) 182 .findFirst(); 183 } 184 185 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 186 theMdmTransactionContext.addTransactionLogMessage(theMessage); 187 ourLog.debug(theMessage); 188 } 189}