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.dao;
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.model.entity.PartitionablePartitionId;
026import ca.uhn.fhir.mdm.api.IMdmLink;
027import ca.uhn.fhir.mdm.api.MdmHistorySearchParameters;
028import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
029import ca.uhn.fhir.mdm.api.MdmLinkWithRevision;
030import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
031import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
032import ca.uhn.fhir.mdm.api.MdmQuerySearchParameters;
033import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
034import ca.uhn.fhir.mdm.dao.MdmLinkFactory;
035import ca.uhn.fhir.mdm.log.Logs;
036import ca.uhn.fhir.mdm.model.MdmTransactionContext;
037import ca.uhn.fhir.rest.api.Constants;
038import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
039import org.hl7.fhir.instance.model.api.IAnyResource;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.slf4j.Logger;
042import org.springframework.beans.factory.annotation.Autowired;
043import org.springframework.data.domain.Example;
044import org.springframework.data.domain.Page;
045import org.springframework.data.history.Revisions;
046import org.springframework.transaction.annotation.Propagation;
047import org.springframework.transaction.annotation.Transactional;
048
049import java.util.Collections;
050import java.util.Date;
051import java.util.List;
052import java.util.Optional;
053import javax.annotation.Nonnull;
054import javax.annotation.Nullable;
055
056public class MdmLinkDaoSvc<P extends IResourcePersistentId, M extends IMdmLink<P>> {
057
058        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
059
060        @Autowired
061        private IMdmLinkDao<P, M> myMdmLinkDao;
062
063        @Autowired
064        private MdmLinkFactory<M> myMdmLinkFactory;
065
066        @Autowired
067        private IIdHelperService<P> myIdHelperService;
068
069        @Autowired
070        private FhirContext myFhirContext;
071
072        @Transactional
073        public M createOrUpdateLinkEntity(
074                        IAnyResource theGoldenResource,
075                        IAnyResource theSourceResource,
076                        MdmMatchOutcome theMatchOutcome,
077                        MdmLinkSourceEnum theLinkSource,
078                        @Nullable MdmTransactionContext theMdmTransactionContext) {
079                M mdmLink = getOrCreateMdmLinkByGoldenResourceAndSourceResource(theGoldenResource, theSourceResource);
080                mdmLink.setLinkSource(theLinkSource);
081                mdmLink.setMatchResult(theMatchOutcome.getMatchResultEnum());
082                // Preserve these flags for link updates
083                mdmLink.setEidMatch(theMatchOutcome.isEidMatch() | mdmLink.isEidMatchPresent());
084                mdmLink.setHadToCreateNewGoldenResource(
085                                theMatchOutcome.isCreatedNewResource() | mdmLink.getHadToCreateNewGoldenResource());
086                mdmLink.setMdmSourceType(myFhirContext.getResourceType(theSourceResource));
087
088                setScoreProperties(theMatchOutcome, mdmLink);
089
090                // Add partition for the mdm link if it's available in the source resource
091                RequestPartitionId partitionId =
092                                (RequestPartitionId) theSourceResource.getUserData(Constants.RESOURCE_PARTITION_ID);
093                if (partitionId != null && partitionId.getFirstPartitionIdOrNull() != null) {
094                        mdmLink.setPartitionId(new PartitionablePartitionId(
095                                        partitionId.getFirstPartitionIdOrNull(), partitionId.getPartitionDate()));
096                }
097
098                String message = String.format(
099                                "Creating %s link from %s to Golden Resource %s.",
100                                mdmLink.getMatchResult(),
101                                theSourceResource.getIdElement().toUnqualifiedVersionless(),
102                                theGoldenResource.getIdElement().toUnqualifiedVersionless());
103                theMdmTransactionContext.addTransactionLogMessage(message);
104                ourLog.debug(message);
105                save(mdmLink);
106                return mdmLink;
107        }
108
109        private void setScoreProperties(MdmMatchOutcome theMatchOutcome, M mdmLink) {
110                if (theMatchOutcome.getScore() != null) {
111                        mdmLink.setScore(
112                                        mdmLink.getScore() != null
113                                                        ? Math.max(theMatchOutcome.getNormalizedScore(), mdmLink.getScore())
114                                                        : theMatchOutcome.getNormalizedScore());
115                }
116
117                if (theMatchOutcome.getVector() != null) {
118                        mdmLink.setVector(
119                                        mdmLink.getVector() != null
120                                                        ? Math.max(theMatchOutcome.getVector(), mdmLink.getVector())
121                                                        : theMatchOutcome.getVector());
122                }
123
124                mdmLink.setRuleCount(
125                                mdmLink.getRuleCount() != null
126                                                ? Math.max(theMatchOutcome.getMdmRuleCount(), mdmLink.getRuleCount())
127                                                : theMatchOutcome.getMdmRuleCount());
128        }
129
130        @Nonnull
131        public M getOrCreateMdmLinkByGoldenResourceAndSourceResource(
132                        IAnyResource theGoldenResource, IAnyResource theSourceResource) {
133                P goldenResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource);
134                P sourceResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource);
135                Optional<M> oExisting = getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourcePid, sourceResourcePid);
136                if (oExisting.isPresent()) {
137                        return oExisting.get();
138                } else {
139                        M newLink = myMdmLinkFactory.newMdmLink();
140                        newLink.setGoldenResourcePersistenceId(goldenResourcePid);
141                        newLink.setSourcePersistenceId(sourceResourcePid);
142                        return newLink;
143                }
144        }
145
146        /**
147         * Given a golden resource Pid and source Pid, return the mdm link that matches these criterias if exists
148         *
149         * @param theGoldenResourcePid
150         * @param theSourceResourcePid
151         * @return
152         * @deprecated This was deprecated in favour of using ResourcePersistenceId rather than longs
153         */
154        @Deprecated
155        public Optional<M> getLinkByGoldenResourcePidAndSourceResourcePid(
156                        Long theGoldenResourcePid, Long theSourceResourcePid) {
157                return getLinkByGoldenResourcePidAndSourceResourcePid(
158                                myIdHelperService.newPid(theGoldenResourcePid), myIdHelperService.newPid(theSourceResourcePid));
159        }
160
161        /**
162         * Given a golden resource Pid and source Pid, return the mdm link that matches these criterias if exists
163         * @param theGoldenResourcePid The ResourcePersistenceId of the golden resource
164         * @param theSourceResourcePid The ResourcepersistenceId of the Source resource
165         * @return The {@link IMdmLink} entity that matches these criteria if exists
166         */
167        public Optional<M> getLinkByGoldenResourcePidAndSourceResourcePid(P theGoldenResourcePid, P theSourceResourcePid) {
168                if (theSourceResourcePid == null || theGoldenResourcePid == null) {
169                        return Optional.empty();
170                }
171                M link = myMdmLinkFactory.newMdmLinkVersionless();
172                link.setSourcePersistenceId(theSourceResourcePid);
173                link.setGoldenResourcePersistenceId(theGoldenResourcePid);
174
175                // TODO - replace the use of example search
176                Example<M> example = Example.of(link);
177
178                return myMdmLinkDao.findOne(example);
179        }
180
181        /**
182         * Given a source resource Pid, and a match result, return all links that match these criteria.
183         *
184         * @param theSourcePid   the source of the relationship.
185         * @param theMatchResult the Match Result of the relationship
186         * @return a list of {@link IMdmLink} entities matching these criteria.
187         */
188        public List<M> getMdmLinksBySourcePidAndMatchResult(P theSourcePid, MdmMatchResultEnum theMatchResult) {
189                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
190                exampleLink.setSourcePersistenceId(theSourcePid);
191                exampleLink.setMatchResult(theMatchResult);
192                Example<M> example = Example.of(exampleLink);
193                return myMdmLinkDao.findAll(example);
194        }
195
196        /**
197         * Given a source Pid, return its Matched {@link IMdmLink}. There can only ever be at most one of these, but its possible
198         * the source has no matches, and may return an empty optional.
199         *
200         * @param theSourcePid The Pid of the source you wish to find the matching link for.
201         * @return the {@link IMdmLink} that contains the Match information for the source.
202         */
203        @Deprecated
204        @Transactional
205        public Optional<M> getMatchedLinkForSourcePid(P theSourcePid) {
206                return myMdmLinkDao.findBySourcePidAndMatchResult(theSourcePid, MdmMatchResultEnum.MATCH);
207        }
208
209        /**
210         * Given an IBaseResource, return its Matched {@link IMdmLink}. There can only ever be at most one of these, but its possible
211         * the source has no matches, and may return an empty optional.
212         *
213         * @param theSourceResource The IBaseResource representing the source you wish to find the matching link for.
214         * @return the {@link IMdmLink} that contains the Match information for the source.
215         */
216        public Optional<M> getMatchedLinkForSource(IBaseResource theSourceResource) {
217                return getMdmLinkWithMatchResult(theSourceResource, MdmMatchResultEnum.MATCH);
218        }
219
220        public Optional<M> getPossibleMatchedLinkForSource(IBaseResource theSourceResource) {
221                return getMdmLinkWithMatchResult(theSourceResource, MdmMatchResultEnum.POSSIBLE_MATCH);
222        }
223
224        @Nonnull
225        private Optional<M> getMdmLinkWithMatchResult(IBaseResource theSourceResource, MdmMatchResultEnum theMatchResult) {
226                P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource);
227                if (pid == null) {
228                        return Optional.empty();
229                }
230
231                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
232                exampleLink.setSourcePersistenceId(pid);
233                exampleLink.setMatchResult(theMatchResult);
234                Example<M> example = Example.of(exampleLink);
235                return myMdmLinkDao.findOne(example);
236        }
237
238        /**
239         * Given a golden resource a source and a match result, return the matching {@link IMdmLink}, if it exists.
240         *
241         * @param theGoldenResourcePid The Pid of the Golden Resource in the relationship
242         * @param theSourcePid         The Pid of the source in the relationship
243         * @param theMatchResult       The MatchResult you are looking for.
244         * @return an Optional {@link IMdmLink} containing the matched link if it exists.
245         */
246        public Optional<M> getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(
247                        Long theGoldenResourcePid, Long theSourcePid, MdmMatchResultEnum theMatchResult) {
248                return getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(
249                                myIdHelperService.newPid(theGoldenResourcePid), myIdHelperService.newPid(theSourcePid), theMatchResult);
250        }
251
252        public Optional<M> getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(
253                        P theGoldenResourcePid, P theSourcePid, MdmMatchResultEnum theMatchResult) {
254                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
255                exampleLink.setGoldenResourcePersistenceId(theGoldenResourcePid);
256                exampleLink.setSourcePersistenceId(theSourcePid);
257                exampleLink.setMatchResult(theMatchResult);
258                Example<M> example = Example.of(exampleLink);
259                return myMdmLinkDao.findOne(example);
260        }
261
262        /**
263         * Get all {@link IMdmLink} which have {@link MdmMatchResultEnum#POSSIBLE_DUPLICATE} as their match result.
264         *
265         * @return A list of {@link IMdmLink} that hold potential duplicate golden resources.
266         */
267        public List<M> getPossibleDuplicates() {
268                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
269                exampleLink.setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE);
270                Example<M> example = Example.of(exampleLink);
271                return myMdmLinkDao.findAll(example);
272        }
273
274        @Transactional
275        public Optional<M> findMdmLinkBySource(IBaseResource theSourceResource) {
276                @Nullable P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource);
277                if (pid == null) {
278                        return Optional.empty();
279                }
280                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
281                exampleLink.setSourcePersistenceId(pid);
282                Example<M> example = Example.of(exampleLink);
283                return myMdmLinkDao.findOne(example);
284        }
285        /**
286         * Delete a given {@link IMdmLink}. Note that this does not clear out the Golden resource.
287         * It is a simple entity delete.
288         *
289         * @param theMdmLink the {@link IMdmLink} to delete.
290         */
291        @Transactional(propagation = Propagation.REQUIRES_NEW)
292        public void deleteLink(M theMdmLink) {
293                myMdmLinkDao.validateMdmLink(theMdmLink);
294                myMdmLinkDao.delete(theMdmLink);
295        }
296
297        /**
298         * Given a Golden Resource, return all links in which they are the source Golden Resource of the {@link IMdmLink}
299         *
300         * @param theGoldenResource The {@link IBaseResource} Golden Resource who's links you would like to retrieve.
301         * @return A list of all {@link IMdmLink} entities in which theGoldenResource is the source Golden Resource
302         */
303        @Transactional
304        public List<M> findMdmLinksByGoldenResource(IBaseResource theGoldenResource) {
305                P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource);
306                if (pid == null) {
307                        return Collections.emptyList();
308                }
309                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
310                exampleLink.setGoldenResourcePersistenceId(pid);
311                Example<M> example = Example.of(exampleLink);
312                return myMdmLinkDao.findAll(example);
313        }
314
315        /**
316         * Persist an MDM link to the database.
317         *
318         * @param theMdmLink the link to save.
319         * @return the persisted {@link IMdmLink} entity.
320         */
321        public M save(M theMdmLink) {
322                M mdmLink = myMdmLinkDao.validateMdmLink(theMdmLink);
323                if (mdmLink.getCreated() == null) {
324                        mdmLink.setCreated(new Date());
325                }
326                mdmLink.setUpdated(new Date());
327                return myMdmLinkDao.save(mdmLink);
328        }
329
330        /**
331         * Given a list of criteria, return all links from the database which fits the criteria provided
332         *
333         * @param theMdmQuerySearchParameters The {@link MdmQuerySearchParameters} being searched.
334         * @return a list of {@link IMdmLink} entities which match the example.
335         */
336        public Page<M> executeTypedQuery(MdmQuerySearchParameters theMdmQuerySearchParameters) {
337                return myMdmLinkDao.search(theMdmQuerySearchParameters);
338        }
339
340        /**
341         * Given a source {@link IBaseResource}, return all {@link IMdmLink} entities in which this source is the source
342         * of the relationship. This will show you all links for a given Patient/Practitioner.
343         *
344         * @param theSourceResource the source resource to find links for.
345         * @return all links for the source.
346         */
347        @Transactional
348        public List<M> findMdmLinksBySourceResource(IBaseResource theSourceResource) {
349                P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource);
350                if (pid == null) {
351                        return Collections.emptyList();
352                }
353                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
354                exampleLink.setSourcePersistenceId(pid);
355                Example<M> example = Example.of(exampleLink);
356                return myMdmLinkDao.findAll(example);
357        }
358
359        /**
360         * Finds all {@link IMdmLink} entities in which theGoldenResource's PID is the source
361         * of the relationship.
362         *
363         * @param theGoldenResource the source resource to find links for.
364         * @return all links for the source.
365         */
366        public List<M> findMdmMatchLinksByGoldenResource(IBaseResource theGoldenResource) {
367                P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource);
368                if (pid == null) {
369                        return Collections.emptyList();
370                }
371                M exampleLink = myMdmLinkFactory.newMdmLinkVersionless();
372                exampleLink.setGoldenResourcePersistenceId(pid);
373                exampleLink.setMatchResult(MdmMatchResultEnum.MATCH);
374                Example<M> example = Example.of(exampleLink);
375                return myMdmLinkDao.findAll(example);
376        }
377
378        /**
379         * Factory delegation method, whenever you need a new MdmLink, use this factory method.
380         * //TODO Should we make the constructor private for MdmLink? or work out some way to ensure they can only be instantiated via factory.
381         *
382         * @return A new {@link IMdmLink}.
383         */
384        public IMdmLink newMdmLink() {
385                return myMdmLinkFactory.newMdmLink();
386        }
387
388        public Optional<M> getMatchedOrPossibleMatchedLinkForSource(IAnyResource theResource) {
389                // TODO KHS instead of two queries, just do one query with an OR
390                Optional<M> retval = getMatchedLinkForSource(theResource);
391                if (!retval.isPresent()) {
392                        retval = getPossibleMatchedLinkForSource(theResource);
393                }
394                return retval;
395        }
396
397        public Optional<M> getLinkByGoldenResourceAndSourceResource(
398                        @Nullable IAnyResource theGoldenResource, @Nullable IAnyResource theSourceResource) {
399                if (theGoldenResource == null || theSourceResource == null) {
400                        return Optional.empty();
401                }
402                return getLinkByGoldenResourcePidAndSourceResourcePid(
403                                myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource),
404                                myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource));
405        }
406
407        @Transactional(propagation = Propagation.MANDATORY)
408        public void deleteLinksWithAnyReferenceToPids(List<P> theGoldenResourcePids) {
409                myMdmLinkDao.deleteLinksWithAnyReferenceToPids(theGoldenResourcePids);
410        }
411
412        // TODO: LD:  delete for good on the next bump
413        @Deprecated(since = "6.5.7", forRemoval = true)
414        public Revisions<Long, M> findMdmLinkHistory(M mdmLink) {
415                return myMdmLinkDao.findHistory(mdmLink.getId());
416        }
417
418        @Transactional
419        public List<MdmLinkWithRevision<M>> findMdmLinkHistory(MdmHistorySearchParameters theMdmHistorySearchParameters) {
420                return myMdmLinkDao.getHistoryForIds(theMdmHistorySearchParameters);
421        }
422}