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