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.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.rest.api.server.storage.IResourcePersistentId; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import org.hl7.fhir.instance.model.api.IAnyResource; 037import org.slf4j.Logger; 038import org.springframework.beans.factory.annotation.Autowired; 039import org.springframework.stereotype.Service; 040import org.springframework.transaction.annotation.Transactional; 041 042import javax.annotation.Nonnull; 043import java.util.List; 044import java.util.Optional; 045 046/** 047 * This class is in charge of managing MdmLinks between Golden Resources and source resources 048 */ 049@Service 050public class MdmLinkSvcImpl implements IMdmLinkSvc { 051 052 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 053 054 @Autowired 055 private MdmResourceDaoSvc myMdmResourceDaoSvc; 056 @Autowired 057 private MdmLinkDaoSvc myMdmLinkDaoSvc; 058 @Autowired 059 private IIdHelperService myIdHelperService; 060 061 @Override 062 @Transactional 063 public void updateLink(@Nonnull IAnyResource theGoldenResource, @Nonnull IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) { 064 if (theMatchOutcome.isPossibleDuplicate() && goldenResourceLinkedAsNoMatch(theGoldenResource, theSourceResource)) { 065 log(theMdmTransactionContext, theGoldenResource.getIdElement().toUnqualifiedVersionless() + 066 " is linked as NO_MATCH with " + 067 theSourceResource.getIdElement().toUnqualifiedVersionless() + 068 " not linking as POSSIBLE_DUPLICATE."); 069 return; 070 } 071 072 MdmMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum(); 073 validateRequestIsLegal(theGoldenResource, theSourceResource, matchResultEnum, theLinkSource); 074 075 myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType()); 076 IMdmLink link = createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext); 077 theMdmTransactionContext.addMdmLink(link); 078 } 079 080 private boolean goldenResourceLinkedAsNoMatch(IAnyResource theGoldenResource, IAnyResource theSourceResource) { 081 IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); 082 IResourcePersistentId sourceId = myIdHelperService.getPidOrThrowException(theSourceResource); 083 // TODO perf collapse into one query 084 return myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenResourceId, sourceId, MdmMatchResultEnum.NO_MATCH).isPresent() || 085 myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(sourceId, goldenResourceId, MdmMatchResultEnum.NO_MATCH).isPresent(); 086 } 087 088 @Override 089 public void deleteLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmTransactionContext theMdmTransactionContext) { 090 if (theGoldenResource == null) { 091 return; 092 } 093 Optional<? extends IMdmLink> optionalMdmLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theSourceResource); 094 if (optionalMdmLink.isPresent()) { 095 IMdmLink mdmLink = optionalMdmLink.get(); 096 log(theMdmTransactionContext, "Deleting MdmLink [" + theGoldenResource.getIdElement().toVersionless() + " -> " + theSourceResource.getIdElement().toVersionless() + "] with result: " + mdmLink.getMatchResult()); 097 myMdmLinkDaoSvc.deleteLink(mdmLink); 098 theMdmTransactionContext.addMdmLink(mdmLink); 099 } 100 } 101 102 @Override 103 @Transactional 104 public void deleteLinksWithAnyReferenceTo(List<IResourcePersistentId> theGoldenResourceIds) { 105 myMdmLinkDaoSvc.deleteLinksWithAnyReferenceToPids(theGoldenResourceIds); 106 } 107 108 /** 109 * Helper function which runs various business rules about what types of requests are allowed. 110 */ 111 private void validateRequestIsLegal(IAnyResource theGoldenResource, IAnyResource theResource, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) { 112 Optional<? extends IMdmLink> oExistingLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theResource); 113 if (oExistingLink.isPresent() && systemIsAttemptingToModifyManualLink(theLinkSource, oExistingLink.get())) { 114 throw new InternalErrorException(Msg.code(760) + "MDM system is not allowed to modify links on manually created links"); 115 } 116 117 if (systemIsAttemptingToAddNoMatch(theLinkSource, theMatchResult)) { 118 throw new InternalErrorException(Msg.code(761) + "MDM system is not allowed to automatically NO_MATCH a resource"); 119 } 120 } 121 122 /** 123 * Helper function which detects when the MDM system is attempting to add a NO_MATCH link, which is not allowed. 124 */ 125 private boolean systemIsAttemptingToAddNoMatch(MdmLinkSourceEnum theLinkSource, MdmMatchResultEnum theMatchResult) { 126 return theLinkSource == MdmLinkSourceEnum.AUTO && theMatchResult == MdmMatchResultEnum.NO_MATCH; 127 } 128 129 /** 130 * Helper function to let us catch when System MDM rules are attempting to override a manually defined link. 131 */ 132 private boolean systemIsAttemptingToModifyManualLink(MdmLinkSourceEnum theIncomingSource, IMdmLink theExistingSource) { 133 return theIncomingSource == MdmLinkSourceEnum.AUTO && theExistingSource.isManual(); 134 } 135 136 private Optional<? extends IMdmLink> getMdmLinkForGoldenResourceSourceResourcePair(@Nonnull IAnyResource theGoldenResource, @Nonnull IAnyResource theCandidate) { 137 if (theGoldenResource.getIdElement().getIdPart() == null || theCandidate.getIdElement().getIdPart() == null) { 138 return Optional.empty(); 139 } else { 140 return myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid( 141 myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource), 142 myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theCandidate) 143 ); 144 } 145 } 146 147 private IMdmLink createOrUpdateLinkEntity(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) { 148 return myMdmLinkDaoSvc.createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext); 149 } 150 151 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 152 theMdmTransactionContext.addTransactionLogMessage(theMessage); 153 ourLog.debug(theMessage); 154 } 155}