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