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.candidate; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.model.RequestPartitionId; 024import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 025import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; 026import ca.uhn.fhir.mdm.api.IMdmLink; 027import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; 028import ca.uhn.fhir.mdm.api.MatchedTarget; 029import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 030import ca.uhn.fhir.mdm.log.Logs; 031import ca.uhn.fhir.mdm.util.MdmPartitionHelper; 032import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 033import org.hl7.fhir.instance.model.api.IAnyResource; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.slf4j.Logger; 036import org.springframework.beans.factory.annotation.Autowired; 037import org.springframework.stereotype.Service; 038 039import java.util.ArrayList; 040import java.util.HashSet; 041import java.util.List; 042import java.util.Optional; 043import java.util.Set; 044import java.util.stream.Collectors; 045 046@Service 047public class FindCandidateByExampleSvc<P extends IResourcePersistentId> extends BaseCandidateFinder { 048 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 049 050 @Autowired 051 IIdHelperService<P> myIdHelperService; 052 053 @Autowired 054 private FhirContext myFhirContext; 055 056 @Autowired 057 private MdmLinkDaoSvc<P, IMdmLink<P>> myMdmLinkDaoSvc; 058 059 @Autowired 060 private IMdmMatchFinderSvc myMdmMatchFinderSvc; 061 062 @Autowired 063 MdmPartitionHelper myMdmPartitionHelper; 064 065 /** 066 * Attempt to find matching Golden Resources by resolving them from similar Matching target resources. Runs MDM logic 067 * over the existing target resources, then finds their entries in the MdmLink table, and returns all the matches 068 * found therein. 069 * 070 * @param theTarget the {@link IBaseResource} which we want to find candidate Golden Resources for. 071 * @return an Optional list of {@link MatchedGoldenResourceCandidate} indicating matches. 072 */ 073 @Override 074 protected List<MatchedGoldenResourceCandidate> findMatchGoldenResourceCandidates(IAnyResource theTarget) { 075 List<MatchedGoldenResourceCandidate> retval = new ArrayList<>(); 076 077 List<P> goldenResourcePidsToExclude = getNoMatchGoldenResourcePids(theTarget); 078 079 List<MatchedTarget> matchedCandidates = myMdmMatchFinderSvc.getMatchedTargets( 080 myFhirContext.getResourceType(theTarget), 081 theTarget, 082 myMdmPartitionHelper.getRequestPartitionIdFromResourceForSearch(theTarget)); 083 084 // Convert all possible match targets to their equivalent Golden Resources by looking up in the MdmLink table, 085 // while ensuring that the matches aren't in our NO_MATCH list. 086 // The data flow is as follows -> 087 // MatchedTargetCandidate -> Golden Resource -> MdmLink -> MatchedGoldenResourceCandidate 088 matchedCandidates = matchedCandidates.stream() 089 .filter(mc -> mc.isMatch() || mc.isPossibleMatch()) 090 .collect(Collectors.toList()); 091 List<String> skippedLogMessages = new ArrayList<>(); 092 List<String> matchedLogMessages = new ArrayList<>(); 093 094 // we'll track the added ids so we don't add the same resources twice 095 // note, all these resources are the same type, so we only need the Long value 096 Set<String> currentIds = new HashSet<>(); 097 for (MatchedTarget match : matchedCandidates) { 098 Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid( 099 myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), match.getTarget())); 100 if (!optionalMdmLink.isPresent()) { 101 if (ourLog.isDebugEnabled()) { 102 skippedLogMessages.add(String.format( 103 "%s does not link to a Golden Resource (it may be a Golden Resource itself). Removing candidate.", 104 match.getTarget().getIdElement().toUnqualifiedVersionless())); 105 } 106 continue; 107 } 108 109 IMdmLink matchMdmLink = optionalMdmLink.get(); 110 if (goldenResourcePidsToExclude.contains(matchMdmLink.getGoldenResourcePersistenceId())) { 111 skippedLogMessages.add(String.format( 112 "Skipping MDM on candidate Golden Resource with PID %s due to manual NO_MATCH", 113 matchMdmLink.getGoldenResourcePersistenceId().toString())); 114 continue; 115 } 116 117 MatchedGoldenResourceCandidate candidate = new MatchedGoldenResourceCandidate( 118 matchMdmLink.getGoldenResourcePersistenceId(), match.getMatchResult()); 119 120 if (ourLog.isDebugEnabled()) { 121 matchedLogMessages.add(String.format( 122 "Navigating from matched resource %s to its Golden Resource %s", 123 match.getTarget().getIdElement().toUnqualifiedVersionless(), 124 matchMdmLink.getGoldenResourcePersistenceId().toString())); 125 } 126 127 // only add if it's not already in the list 128 // NB: we cannot use hash of IResourcePersistentId because 129 // BaseResourcePersistentId overrides this (and so is the same 130 // for any class with the same version) :( 131 if (currentIds.add( 132 String.valueOf(candidate.getCandidateGoldenResourcePid().getId()))) { 133 retval.add(candidate); 134 } 135 } 136 137 if (ourLog.isDebugEnabled()) { 138 for (String logMessage : skippedLogMessages) { 139 ourLog.debug(logMessage); 140 } 141 for (String logMessage : matchedLogMessages) { 142 ourLog.debug(logMessage); 143 } 144 } 145 return retval; 146 } 147 148 private List<P> getNoMatchGoldenResourcePids(IBaseResource theBaseResource) { 149 P targetPid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theBaseResource); 150 return myMdmLinkDaoSvc.getMdmLinksBySourcePidAndMatchResult(targetPid, MdmMatchResultEnum.NO_MATCH).stream() 151 .map(IMdmLink::getGoldenResourcePersistenceId) 152 .collect(Collectors.toList()); 153 } 154 155 @Override 156 protected CandidateStrategyEnum getStrategy() { 157 return CandidateStrategyEnum.SCORE; 158 } 159}