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