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}