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