001package ca.uhn.fhir.jpa.mdm.svc.candidate; 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.mdm.api.IMdmLink; 028import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; 029import ca.uhn.fhir.mdm.api.MatchedTarget; 030import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 031import ca.uhn.fhir.mdm.log.Logs; 032import ca.uhn.fhir.rest.api.Constants; 033import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 034import org.hl7.fhir.instance.model.api.IAnyResource; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036import org.slf4j.Logger; 037import org.springframework.beans.factory.annotation.Autowired; 038import org.springframework.stereotype.Service; 039 040import java.util.ArrayList; 041import java.util.List; 042import java.util.Optional; 043import java.util.stream.Collectors; 044 045@Service 046public class FindCandidateByExampleSvc extends BaseCandidateFinder { 047 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 048 @Autowired 049 IIdHelperService myIdHelperService; 050 @Autowired 051 private FhirContext myFhirContext; 052 @Autowired 053 private MdmLinkDaoSvc myMdmLinkDaoSvc; 054 @Autowired 055 private IMdmMatchFinderSvc myMdmMatchFinderSvc; 056 057 /** 058 * Attempt to find matching Golden Resources by resolving them from similar Matching target resources. Runs MDM logic 059 * over the existing target resources, then finds their entries in the MdmLink table, and returns all the matches 060 * found therein. 061 * 062 * @param theTarget the {@link IBaseResource} which we want to find candidate Golden Resources for. 063 * @return an Optional list of {@link MatchedGoldenResourceCandidate} indicating matches. 064 */ 065 @Override 066 protected List<MatchedGoldenResourceCandidate> findMatchGoldenResourceCandidates(IAnyResource theTarget) { 067 List<MatchedGoldenResourceCandidate> retval = new ArrayList<>(); 068 069 List<ResourcePersistentId> goldenResourcePidsToExclude = getNoMatchGoldenResourcePids(theTarget); 070 071 List<MatchedTarget> matchedCandidates = myMdmMatchFinderSvc.getMatchedTargets(myFhirContext.getResourceType(theTarget), theTarget, (RequestPartitionId) theTarget.getUserData(Constants.RESOURCE_PARTITION_ID)); 072 073 // Convert all possible match targets to their equivalent Golden Resources by looking up in the MdmLink table, 074 // while ensuring that the matches aren't in our NO_MATCH list. 075 // The data flow is as follows -> 076 // MatchedTargetCandidate -> Golden Resource -> MdmLink -> MatchedGoldenResourceCandidate 077 matchedCandidates = matchedCandidates.stream().filter(mc -> mc.isMatch() || mc.isPossibleMatch()).collect(Collectors.toList()); 078 List<String> skippedLogMessages = new ArrayList<>(); 079 List<String> matchedLogMessages = new ArrayList<>(); 080 for (MatchedTarget match : matchedCandidates) { 081 Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), match.getTarget())); 082 if (!optionalMdmLink.isPresent()) { 083 if (ourLog.isDebugEnabled()) { 084 skippedLogMessages.add(String.format("%s does not link to a Golden Resource (it may be a Golden Resource itself). Removing candidate.", match.getTarget().getIdElement().toUnqualifiedVersionless())); 085 } 086 continue; 087 } 088 089 090 IMdmLink matchMdmLink = optionalMdmLink.get(); 091 if (goldenResourcePidsToExclude.contains(matchMdmLink.getGoldenResourcePersistenceId())) { 092 skippedLogMessages.add(String.format("Skipping MDM on candidate Golden Resource with PID %s due to manual NO_MATCH", matchMdmLink.getGoldenResourcePersistenceId().toString())); 093 continue; 094 } 095 096 MatchedGoldenResourceCandidate candidate = new MatchedGoldenResourceCandidate(matchMdmLink.getGoldenResourcePersistenceId(), match.getMatchResult()); 097 098 if (ourLog.isDebugEnabled()) { 099 matchedLogMessages.add(String.format("Navigating from matched resource %s to its Golden Resource %s", match.getTarget().getIdElement().toUnqualifiedVersionless(), matchMdmLink.getGoldenResourcePersistenceId().toString())); 100 } 101 102 retval.add(candidate); 103 } 104 105 if (ourLog.isDebugEnabled()) { 106 for (String logMessage: skippedLogMessages) { 107 ourLog.debug(logMessage); 108 } 109 for (String logMessage: matchedLogMessages) { 110 ourLog.debug(logMessage); 111 } 112 } 113 return retval; 114 } 115 116 private List<ResourcePersistentId> getNoMatchGoldenResourcePids(IBaseResource theBaseResource) { 117 ResourcePersistentId targetPid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theBaseResource); 118 return myMdmLinkDaoSvc.getMdmLinksBySourcePidAndMatchResult(targetPid, MdmMatchResultEnum.NO_MATCH) 119 .stream() 120 .map(IMdmLink::getGoldenResourcePersistenceId) 121 .collect(Collectors.toList()); 122 } 123 124 @Override 125 protected CandidateStrategyEnum getStrategy() { 126 return CandidateStrategyEnum.SCORE; 127 } 128}