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.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 027import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; 028import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper; 029import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 030import ca.uhn.fhir.mdm.api.IMdmLink; 031import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc; 032import ca.uhn.fhir.mdm.api.IMdmSettings; 033import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 034import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 035import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 036import ca.uhn.fhir.mdm.log.Logs; 037import ca.uhn.fhir.mdm.model.MdmTransactionContext; 038import ca.uhn.fhir.mdm.util.MdmResourceUtil; 039import ca.uhn.fhir.mdm.util.MessageHelper; 040import ca.uhn.fhir.rest.api.Constants; 041import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 043import ca.uhn.fhir.rest.server.provider.ProviderConstants; 044import org.hl7.fhir.instance.model.api.IAnyResource; 045import org.slf4j.Logger; 046import org.springframework.beans.factory.annotation.Autowired; 047import org.springframework.transaction.annotation.Transactional; 048 049import java.util.List; 050import java.util.Objects; 051import java.util.Optional; 052 053public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc { 054 055 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 056 057 @Autowired 058 FhirContext myFhirContext; 059 @Autowired 060 IIdHelperService myIdHelperService; 061 @Autowired 062 MdmLinkDaoSvc myMdmLinkDaoSvc; 063 @Autowired 064 MdmResourceDaoSvc myMdmResourceDaoSvc; 065 @Autowired 066 MdmMatchLinkSvc myMdmMatchLinkSvc; 067 @Autowired 068 IMdmSettings myMdmSettings; 069 @Autowired 070 MessageHelper myMessageHelper; 071 @Autowired 072 IMdmSurvivorshipService myMdmSurvivorshipService; 073 @Autowired 074 MdmPartitionHelper myMdmPartitionHelper; 075 076 @Transactional 077 @Override 078 public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) { 079 String sourceType = myFhirContext.getResourceType(theSourceResource); 080 081 validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType); 082 083 IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); 084 IResourcePersistentId sourceResourceId = myIdHelperService.getPidOrThrowException(theSourceResource); 085 086 // check if the golden resource and the source resource are in the same partition, throw error if not 087 myMdmPartitionHelper.validateResourcesInSamePartition(theGoldenResource, theSourceResource); 088 089 Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, sourceResourceId); 090 if (optionalMdmLink.isEmpty()) { 091 throw new InvalidRequestException(Msg.code(738) + myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource)); 092 } 093 094 IMdmLink mdmLink = optionalMdmLink.get(); 095 096 validateNoMatchPresentWhenAcceptingPossibleMatch(theSourceResource, goldenResourceId, theMatchResult); 097 098 if (mdmLink.getMatchResult() == theMatchResult) { 099 ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ". Nothing to do."); 100 return theGoldenResource; 101 } 102 103 ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + "."); 104 mdmLink.setMatchResult(theMatchResult); 105 mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); 106 107 // Add partition for the mdm link if it doesn't exist 108 RequestPartitionId goldenResourcePartitionId = (RequestPartitionId) theGoldenResource.getUserData(Constants.RESOURCE_PARTITION_ID); 109 if (goldenResourcePartitionId != null && goldenResourcePartitionId.hasPartitionIds() && goldenResourcePartitionId.getFirstPartitionIdOrNull() != null && 110 (mdmLink.getPartitionId() == null || mdmLink.getPartitionId().getPartitionId() == null)) { 111 mdmLink.setPartitionId(new PartitionablePartitionId(goldenResourcePartitionId.getFirstPartitionIdOrNull(), goldenResourcePartitionId.getPartitionDate())); 112 } 113 myMdmLinkDaoSvc.save(mdmLink); 114 115 if (theMatchResult == MdmMatchResultEnum.MATCH) { 116 // only apply survivorship rules in case of a match 117 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theSourceResource, theGoldenResource, theMdmContext); 118 } 119 120 myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType()); 121 if (theMatchResult == MdmMatchResultEnum.NO_MATCH) { 122 // Need to find a new Golden Resource to link this target to 123 myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext); 124 } 125 return theGoldenResource; 126 } 127 128 /** 129 * When updating POSSIBLE_MATCH link to a MATCH we need to validate that a MATCH to a different golden resource 130 * doesn't exist, because a resource mustn't be a MATCH to more than one golden resource 131 */ 132 private void validateNoMatchPresentWhenAcceptingPossibleMatch(IAnyResource theSourceResource, 133 IResourcePersistentId theGoldenResourceId, MdmMatchResultEnum theMatchResult) { 134 135 // if theMatchResult != MATCH, we are not accepting POSSIBLE_MATCH so there is nothing to validate 136 if (theMatchResult != MdmMatchResultEnum.MATCH) { return; } 137 138 IResourcePersistentId sourceResourceId = myIdHelperService.getPidOrThrowException(theSourceResource); 139 List<? extends IMdmLink> mdmLinks = myMdmLinkDaoSvc 140 .getMdmLinksBySourcePidAndMatchResult(sourceResourceId, MdmMatchResultEnum.MATCH); 141 142 // if a link for a different golden resource exists, throw an exception 143 for (IMdmLink mdmLink : mdmLinks) { 144 if (mdmLink.getGoldenResourcePersistenceId() != theGoldenResourceId) { 145 IAnyResource existingGolden = myMdmResourceDaoSvc.readGoldenResourceByPid(mdmLink.getGoldenResourcePersistenceId(), mdmLink.getMdmSourceType()); 146 throw new InvalidRequestException(Msg.code(2218) + 147 myMessageHelper.getMessageForAlreadyAcceptedLink(existingGolden, theSourceResource)); 148 } 149 } 150 } 151 152 153 private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) { 154 String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord); 155 156 if (theMatchResult != MdmMatchResultEnum.NO_MATCH && 157 theMatchResult != MdmMatchResultEnum.MATCH) { 158 throw new InvalidRequestException(Msg.code(739) + myMessageHelper.getMessageForUnsupportedMatchResult()); 159 } 160 161 if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) { 162 throw new InvalidRequestException(Msg.code(740) + myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType)); 163 } 164 165 if (!myMdmSettings.isSupportedMdmType(theSourceType)) { 166 throw new InvalidRequestException(Msg.code(741) + myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType)); 167 } 168 169 if (!Objects.equals(goldenRecordType, theSourceType)) { 170 throw new InvalidRequestException(Msg.code(742) + myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType)); 171 } 172 173 if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) { 174 throw new InvalidRequestException(Msg.code(743) + myMessageHelper.getMessageForUnmanagedResource()); 175 } 176 177 if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) { 178 throw new InvalidRequestException(Msg.code(744) + myMessageHelper.getMessageForUnsupportedSourceResource()); 179 } 180 } 181 182 @Transactional 183 @Override 184 public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) { 185 validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource); 186 187 IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); 188 IResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource); 189 190 Optional<? extends IMdmLink> oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId); 191 if (oMdmLink.isEmpty()) { 192 throw new InvalidRequestException(Msg.code(745) + "No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless()); 193 } 194 195 IMdmLink mdmLink = oMdmLink.get(); 196 if (!mdmLink.isPossibleDuplicate()) { 197 throw new InvalidRequestException(Msg.code(746) + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE."); 198 } 199 mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); 200 mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); 201 myMdmLinkDaoSvc.save(mdmLink); 202 } 203 204 /** 205 * Ensure that the two resources are of the same type and both are managed by HAPI-MDM 206 */ 207 private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) { 208 String goldenResourceType = myFhirContext.getResourceType(theGoldenResource); 209 String targetType = myFhirContext.getResourceType(theTarget); 210 if (!goldenResourceType.equalsIgnoreCase(targetType)) { 211 throw new InvalidRequestException(Msg.code(747) + "First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be the same resource type as the second argument. Was " + goldenResourceType + "/" + targetType); 212 } 213 214 if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) { 215 throw new InvalidRequestException(Msg.code(748) + "Only MDM Managed Golden Resources may be updated via this operation. The resource provided is not tagged as managed by HAPI-MDM"); 216 } 217 } 218}