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.context.FhirContext; 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.jpa.mdm.util.MdmPartitionHelper; 028import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.mdm.api.IMdmLink; 031import ca.uhn.fhir.mdm.api.IMdmLinkSvc; 032import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc; 033import ca.uhn.fhir.mdm.api.IMdmSettings; 034import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 035import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 036import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 037import ca.uhn.fhir.mdm.log.Logs; 038import ca.uhn.fhir.mdm.model.MdmTransactionContext; 039import ca.uhn.fhir.mdm.util.MdmResourceUtil; 040import ca.uhn.fhir.mdm.util.MessageHelper; 041import ca.uhn.fhir.rest.api.Constants; 042import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 043import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 044import ca.uhn.fhir.rest.server.provider.ProviderConstants; 045import org.hl7.fhir.instance.model.api.IAnyResource; 046import org.slf4j.Logger; 047import org.springframework.beans.factory.annotation.Autowired; 048import org.springframework.transaction.annotation.Transactional; 049 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 IMdmLinkSvc myMdmLinkSvc; 065 @Autowired 066 MdmResourceDaoSvc myMdmResourceDaoSvc; 067 @Autowired 068 MdmMatchLinkSvc myMdmMatchLinkSvc; 069 @Autowired 070 IMdmSettings myMdmSettings; 071 @Autowired 072 MessageHelper myMessageHelper; 073 @Autowired 074 IMdmSurvivorshipService myMdmSurvivorshipService; 075 @Autowired 076 MdmPartitionHelper myMdmPartitionHelper; 077 078 @Transactional 079 @Override 080 public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) { 081 String sourceType = myFhirContext.getResourceType(theSourceResource); 082 083 validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType); 084 085 ResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); 086 ResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theSourceResource); 087 088 // check if the golden resource and the source resource are in the same partition, throw error if not 089 myMdmPartitionHelper.validateResourcesInSamePartition(theGoldenResource, theSourceResource); 090 091 Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId); 092 if (!optionalMdmLink.isPresent()) { 093 throw new InvalidRequestException(Msg.code(738) + myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource)); 094 } 095 096 IMdmLink mdmLink = optionalMdmLink.get(); 097 if (mdmLink.getMatchResult() == theMatchResult) { 098 ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ". Nothing to do."); 099 return theGoldenResource; 100 } 101 102 ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + "."); 103 mdmLink.setMatchResult(theMatchResult); 104 mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); 105 106 // Add partition for the mdm link if it doesn't exist 107 RequestPartitionId goldenResourcePartitionId = (RequestPartitionId) theGoldenResource.getUserData(Constants.RESOURCE_PARTITION_ID); 108 if (goldenResourcePartitionId != null && goldenResourcePartitionId.hasPartitionIds() && goldenResourcePartitionId.getFirstPartitionIdOrNull() != null && 109 (mdmLink.getPartitionId() == null || mdmLink.getPartitionId().getPartitionId() == null)) { 110 mdmLink.setPartitionId(new PartitionablePartitionId(goldenResourcePartitionId.getFirstPartitionIdOrNull(), goldenResourcePartitionId.getPartitionDate())); 111 } 112 myMdmLinkDaoSvc.save(mdmLink); 113 114 if (theMatchResult == MdmMatchResultEnum.MATCH) { 115 // only apply survivorship rules in case of a match 116 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theSourceResource, theGoldenResource, theMdmContext); 117 } 118 119 myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType()); 120 if (theMatchResult == MdmMatchResultEnum.NO_MATCH) { 121 // Need to find a new Golden Resource to link this target to 122 myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext); 123 } 124 return theGoldenResource; 125 } 126 127 private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) { 128 String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord); 129 130 if (theMatchResult != MdmMatchResultEnum.NO_MATCH && 131 theMatchResult != MdmMatchResultEnum.MATCH) { 132 throw new InvalidRequestException(Msg.code(739) + myMessageHelper.getMessageForUnsupportedMatchResult()); 133 } 134 135 if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) { 136 throw new InvalidRequestException(Msg.code(740) + myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType)); 137 } 138 139 if (!myMdmSettings.isSupportedMdmType(theSourceType)) { 140 throw new InvalidRequestException(Msg.code(741) + myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType)); 141 } 142 143 if (!Objects.equals(goldenRecordType, theSourceType)) { 144 throw new InvalidRequestException(Msg.code(742) + myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType)); 145 } 146 147 if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) { 148 throw new InvalidRequestException(Msg.code(743) + myMessageHelper.getMessageForUnmanagedResource()); 149 } 150 151 if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) { 152 throw new InvalidRequestException(Msg.code(744) + myMessageHelper.getMessageForUnsupportedSourceResource()); 153 } 154 } 155 156 @Transactional 157 @Override 158 public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) { 159 validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource); 160 161 ResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); 162 ResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource); 163 164 Optional<? extends IMdmLink> oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId); 165 if (!oMdmLink.isPresent()) { 166 throw new InvalidRequestException(Msg.code(745) + "No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless()); 167 } 168 169 IMdmLink mdmLink = oMdmLink.get(); 170 if (!mdmLink.isPossibleDuplicate()) { 171 throw new InvalidRequestException(Msg.code(746) + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE."); 172 } 173 mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); 174 mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); 175 myMdmLinkDaoSvc.save(mdmLink); 176 } 177 178 /** 179 * Ensure that the two resources are of the same type and both are managed by HAPI-MDM 180 */ 181 private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) { 182 String goldenResourceType = myFhirContext.getResourceType(theGoldenResource); 183 String targetType = myFhirContext.getResourceType(theTarget); 184 if (!goldenResourceType.equalsIgnoreCase(targetType)) { 185 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); 186 } 187 188 if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) { 189 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"); 190 } 191 } 192}