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