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