All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc Maven / Gradle / Ivy

The newest version!
/*-
 * #%L
 * HAPI FHIR JPA Server - Master Data Management
 * %%
 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package ca.uhn.fhir.jpa.mdm.svc;

import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.mdm.models.FindGoldenResourceCandidatesParams;
import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateList;
import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateStrategyEnum;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.blocklist.svc.IBlockRuleEvaluationSvc;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
 * MdmMatchLinkSvc is the entrypoint for HAPI's MDM system. An incoming resource can call
 * updateMdmLinksForMdmSource and the underlying MDM system will take care of matching it to a GoldenResource,
 * or creating a new GoldenResource if a suitable one was not found.
 */
@Service
public class MdmMatchLinkSvc {

	private static final Logger ourLog = Logs.getMdmTroubleshootingLog();

	@Autowired
	private IMdmLinkSvc myMdmLinkSvc;

	@Autowired
	private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc;

	@Autowired
	private GoldenResourceHelper myGoldenResourceHelper;

	@Autowired
	private MdmEidUpdateService myEidUpdateService;

	@Autowired
	private IBlockRuleEvaluationSvc myBlockRuleEvaluationSvc;

	@Autowired
	private DaoRegistry myDaoRegistry;

	@Autowired
	private IMdmSurvivorshipService myMdmSurvivorshipService;

	/**
	 * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them,
	 * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json.
	 * Does nothing if resource is determined to be not managed by MDM.
	 *
	 * @param theResource              the incoming MDM source, which can be any supported MDM type.
	 * @param theMdmTransactionContext
	 * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource.
	 */
	@Transactional
	public MdmTransactionContext updateMdmLinksForMdmSource(
			IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
		if (MdmResourceUtil.isMdmAllowed(theResource)) {
			return doMdmUpdate(theResource, theMdmTransactionContext);
		} else {
			return null;
		}
	}

	private MdmTransactionContext doMdmUpdate(
			IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
		// we initialize to an empty list
		// we require a candidatestrategy, but it doesn't matter
		// because empty lists are effectively no matches
		// (and so the candidate strategy doesn't matter)
		CandidateList candidateList = new CandidateList(CandidateStrategyEnum.ANY);

		/*
		 * If a resource is blocked, we will not conduct
		 * MDM matching. But we will still create golden resources
		 * (so that future resources may match to it).
		 */
		boolean isResourceBlocked = myBlockRuleEvaluationSvc.isMdmMatchingBlocked(theResource);
		// we will mark the golden resource special for this
		theMdmTransactionContext.setIsBlocked(isResourceBlocked);

		if (!isResourceBlocked) {
			FindGoldenResourceCandidatesParams params =
					new FindGoldenResourceCandidatesParams(theResource, theMdmTransactionContext);
			candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(params);
		}

		if (isResourceBlocked || candidateList.isEmpty()) {
			handleMdmWithNoCandidates(theResource, theMdmTransactionContext);
		} else if (candidateList.exactlyOneMatch()) {
			handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext);
		} else {
			handleMdmWithMultipleCandidates(theResource, candidateList, theMdmTransactionContext);
		}
		return theMdmTransactionContext;
	}

	private void handleMdmWithMultipleCandidates(
			IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
		MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch();
		IResourcePersistentId sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid();
		boolean allSameGoldenResource = theCandidateList.stream()
				.allMatch(candidate -> candidate.getCandidateGoldenResourcePid().equals(sampleGoldenResourcePid));

		if (allSameGoldenResource) {
			log(
					theMdmTransactionContext,
					"MDM received multiple match candidates, but they are all linked to the same Golden Resource.");
			handleMdmWithSingleCandidate(theResource, firstMatch, theMdmTransactionContext);
		} else {
			log(
					theMdmTransactionContext,
					"MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");

			// Set them all as POSSIBLE_MATCH
			List goldenResources =
					createPossibleMatches(theResource, theCandidateList, theMdmTransactionContext);

			// Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource.
			IAnyResource firstGoldenResource = goldenResources.get(0);

			goldenResources.subList(1, goldenResources.size()).forEach(possibleDuplicateGoldenResource -> {
				MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_DUPLICATE;
				outcome.setEidMatch(theCandidateList.isEidMatch());
				myMdmLinkSvc.updateLink(
						firstGoldenResource,
						possibleDuplicateGoldenResource,
						outcome,
						MdmLinkSourceEnum.AUTO,
						theMdmTransactionContext);
			});
		}
	}

	private List createPossibleMatches(
			IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
		List goldenResources = new ArrayList<>();

		for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) {
			IAnyResource goldenResource =
					myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(
							matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType());

			MdmMatchOutcome outcome = new MdmMatchOutcome(
							matchedGoldenResourceCandidate.getMatchResult().getVector(),
							matchedGoldenResourceCandidate.getMatchResult().getScore())
					.setMdmRuleCount(
							matchedGoldenResourceCandidate.getMatchResult().getMdmRuleCount());

			outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH);
			outcome.setEidMatch(theCandidateList.isEidMatch());
			myMdmLinkSvc.updateLink(
					goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
			goldenResources.add(goldenResource);
		}

		return goldenResources;
	}

	private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
		log(
				theMdmTransactionContext,
				String.format(
						"There were no matched candidates for MDM, creating a new %s Golden Resource.",
						theResource.getIdElement().getResourceType()));
		IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(
				theResource, theMdmTransactionContext, myMdmSurvivorshipService);
		// TODO GGG :)
		// 1. Get the right helper
		// 2. Create source resource for the MDM source
		// 3. UPDATE MDM LINK TABLE

		myMdmLinkSvc.updateLink(
				newGoldenResource,
				theResource,
				MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH,
				MdmLinkSourceEnum.AUTO,
				theMdmTransactionContext);
	}

	private void handleMdmCreate(
			IAnyResource theTargetResource,
			MatchedGoldenResourceCandidate theGoldenResourceCandidate,
			MdmTransactionContext theMdmTransactionContext) {
		IAnyResource goldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(
				theGoldenResourceCandidate, theMdmTransactionContext.getResourceType());

		if (myGoldenResourceHelper.isPotentialDuplicate(goldenResource, theTargetResource)) {
			log(
					theMdmTransactionContext,
					"Duplicate detected based on the fact that both resources have different external EIDs.");
			IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(
					theTargetResource, theMdmTransactionContext, myMdmSurvivorshipService);

			myMdmLinkSvc.updateLink(
					newGoldenResource,
					theTargetResource,
					MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH,
					MdmLinkSourceEnum.AUTO,
					theMdmTransactionContext);
			myMdmLinkSvc.updateLink(
					newGoldenResource,
					goldenResource,
					MdmMatchOutcome.POSSIBLE_DUPLICATE,
					MdmLinkSourceEnum.AUTO,
					theMdmTransactionContext);
		} else {
			log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");

			if (theGoldenResourceCandidate.isMatch()) {
				myGoldenResourceHelper.handleExternalEidAddition(
						goldenResource, theTargetResource, theMdmTransactionContext);
				myEidUpdateService.applySurvivorshipRulesAndSaveGoldenResource(
						theTargetResource, goldenResource, theMdmTransactionContext);
			}

			myMdmLinkSvc.updateLink(
					goldenResource,
					theTargetResource,
					theGoldenResourceCandidate.getMatchResult(),
					MdmLinkSourceEnum.AUTO,
					theMdmTransactionContext);
		}
	}

	private void handleMdmWithSingleCandidate(
			IAnyResource theResource,
			MatchedGoldenResourceCandidate theGoldenResourceCandidate,
			MdmTransactionContext theMdmTransactionContext) {
		if (theMdmTransactionContext.getRestOperation().equals(MdmTransactionContext.OperationType.UPDATE_RESOURCE)) {
			log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");
			myEidUpdateService.handleMdmUpdate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
		} else {
			handleMdmCreate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
		}
	}

	private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
		theMdmTransactionContext.addTransactionLogMessage(theMessage);
		ourLog.debug(theMessage);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy