001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.dao.mdm;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
025import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
026import ca.uhn.fhir.jpa.dao.data.IMdmLinkJpaRepository;
027import ca.uhn.fhir.jpa.entity.HapiFhirEnversRevision;
028import ca.uhn.fhir.jpa.entity.MdmLink;
029import ca.uhn.fhir.jpa.model.dao.JpaPid;
030import ca.uhn.fhir.jpa.model.entity.EnversRevision;
031import ca.uhn.fhir.mdm.api.IMdmLink;
032import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
033import ca.uhn.fhir.mdm.api.MdmLinkWithRevision;
034import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
035import ca.uhn.fhir.mdm.api.paging.MdmPageRequest;
036import ca.uhn.fhir.mdm.api.params.MdmHistorySearchParameters;
037import ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters;
038import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
039import ca.uhn.fhir.mdm.model.MdmPidTuple;
040import ca.uhn.fhir.rest.api.SortOrderEnum;
041import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
042import jakarta.annotation.Nonnull;
043import jakarta.persistence.EntityManager;
044import jakarta.persistence.TypedQuery;
045import jakarta.persistence.criteria.CriteriaBuilder;
046import jakarta.persistence.criteria.CriteriaQuery;
047import jakarta.persistence.criteria.Expression;
048import jakarta.persistence.criteria.Order;
049import jakarta.persistence.criteria.Path;
050import jakarta.persistence.criteria.Predicate;
051import jakarta.persistence.criteria.Root;
052import org.apache.commons.collections4.CollectionUtils;
053import org.apache.commons.collections4.ListUtils;
054import org.apache.commons.lang3.ObjectUtils;
055import org.apache.commons.lang3.Validate;
056import org.hibernate.envers.AuditReader;
057import org.hibernate.envers.RevisionType;
058import org.hibernate.envers.query.AuditEntity;
059import org.hibernate.envers.query.AuditQueryCreator;
060import org.hibernate.envers.query.criteria.AuditCriterion;
061import org.hl7.fhir.instance.model.api.IIdType;
062import org.slf4j.Logger;
063import org.slf4j.LoggerFactory;
064import org.springframework.beans.factory.annotation.Autowired;
065import org.springframework.data.domain.Example;
066import org.springframework.data.domain.Page;
067import org.springframework.data.domain.PageImpl;
068import org.springframework.data.domain.PageRequest;
069import org.springframework.data.domain.Pageable;
070import org.springframework.data.history.Revisions;
071
072import java.util.ArrayList;
073import java.util.Collection;
074import java.util.Collections;
075import java.util.Date;
076import java.util.List;
077import java.util.Optional;
078import java.util.stream.Collectors;
079
080import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.GOLDEN_RESOURCE_NAME;
081import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.GOLDEN_RESOURCE_PID_NAME;
082import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.LINK_SOURCE_NAME;
083import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.MATCH_RESULT_NAME;
084import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.PARTITION_ID_NAME;
085import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.RESOURCE_TYPE_NAME;
086import static ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters.SOURCE_PID_NAME;
087
088public class MdmLinkDaoJpaImpl implements IMdmLinkDao<JpaPid, MdmLink> {
089        private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkDaoJpaImpl.class);
090
091        @Autowired
092        protected EntityManager myEntityManager;
093
094        @Autowired
095        IMdmLinkJpaRepository myMdmLinkDao;
096
097        @Autowired
098        private IIdHelperService<JpaPid> myIdHelperService;
099
100        @Autowired
101        private AuditReader myAuditReader;
102
103        @Override
104        public int deleteWithAnyReferenceToPid(JpaPid thePid) {
105                return myMdmLinkDao.deleteWithAnyReferenceToPid(thePid.getId());
106        }
107
108        @Override
109        public int deleteWithAnyReferenceToPidAndMatchResultNot(JpaPid thePid, MdmMatchResultEnum theMatchResult) {
110                return myMdmLinkDao.deleteWithAnyReferenceToPidAndMatchResultNot(thePid.getId(), theMatchResult);
111        }
112
113        @Override
114        public List<MdmPidTuple<JpaPid>> expandPidsFromGroupPidGivenMatchResult(
115                        JpaPid theGroupPid, MdmMatchResultEnum theMdmMatchResultEnum) {
116                return myMdmLinkDao
117                                .expandPidsFromGroupPidGivenMatchResult((theGroupPid).getId(), theMdmMatchResultEnum)
118                                .stream()
119                                .map(this::daoTupleToMdmTuple)
120                                .collect(Collectors.toList());
121        }
122
123        private MdmPidTuple<JpaPid> daoTupleToMdmTuple(IMdmLinkJpaRepository.MdmPidTuple theMdmPidTuple) {
124                return MdmPidTuple.fromGoldenAndSourceAndPartitionIds(
125                                JpaPid.fromId(theMdmPidTuple.getGoldenPid()),
126                                theMdmPidTuple.getGoldenPartitionId(),
127                                JpaPid.fromId(theMdmPidTuple.getSourcePid()),
128                                theMdmPidTuple.getSourcePartitionId());
129        }
130
131        @Override
132        public List<MdmPidTuple<JpaPid>> expandPidsBySourcePidAndMatchResult(
133                        JpaPid theSourcePid, MdmMatchResultEnum theMdmMatchResultEnum) {
134                return myMdmLinkDao.expandPidsBySourcePidAndMatchResult((theSourcePid).getId(), theMdmMatchResultEnum).stream()
135                                .map(this::daoTupleToMdmTuple)
136                                .collect(Collectors.toList());
137        }
138
139        @Override
140        public List<MdmLink> findLinksAssociatedWithGoldenResourceOfSourceResourceExcludingNoMatch(JpaPid theSourcePid) {
141                return myMdmLinkDao.findLinksAssociatedWithGoldenResourceOfSourceResourceExcludingMatchResult(
142                                (theSourcePid).getId(), MdmMatchResultEnum.NO_MATCH);
143        }
144
145        @Override
146        public List<MdmPidTuple<JpaPid>> expandPidsByGoldenResourcePidAndMatchResult(
147                        JpaPid theSourcePid, MdmMatchResultEnum theMdmMatchResultEnum) {
148                return myMdmLinkDao
149                                .expandPidsByGoldenResourcePidAndMatchResult((theSourcePid).getId(), theMdmMatchResultEnum)
150                                .stream()
151                                .map(this::daoTupleToMdmTuple)
152                                .collect(Collectors.toList());
153        }
154
155        @Override
156        public Collection<MdmPidTuple<JpaPid>> resolveGoldenResources(List<JpaPid> theSourcePids) {
157                return myMdmLinkDao
158                                .expandPidsByGoldenResourcePidsOrSourcePidsAndMatchResult(
159                                                JpaPid.toLongList(theSourcePids), MdmMatchResultEnum.MATCH)
160                                .stream()
161                                .map(this::daoTupleToMdmTuple)
162                                .distinct()
163                                .collect(Collectors.toList());
164        }
165
166        @Override
167        public List<JpaPid> findPidByResourceNameAndThreshold(
168                        String theResourceName, Date theHighThreshold, Pageable thePageable) {
169                return myMdmLinkDao.findPidByResourceNameAndThreshold(theResourceName, theHighThreshold, thePageable).stream()
170                                .map(JpaPid::fromId)
171                                .collect(Collectors.toList());
172        }
173
174        @Override
175        public List<JpaPid> findPidByResourceNameAndThresholdAndPartitionId(
176                        String theResourceName, Date theHighThreshold, List<Integer> thePartitionIds, Pageable thePageable) {
177                return myMdmLinkDao
178                                .findPidByResourceNameAndThresholdAndPartitionId(
179                                                theResourceName, theHighThreshold, thePartitionIds, thePageable)
180                                .stream()
181                                .map(JpaPid::fromId)
182                                .collect(Collectors.toList());
183        }
184
185        @Override
186        public List<MdmLink> findAllById(List<JpaPid> thePids) {
187                List<Long> theLongPids = thePids.stream().map(JpaPid::getId).collect(Collectors.toList());
188                return myMdmLinkDao.findAllById(theLongPids);
189        }
190
191        @Override
192        public Optional<MdmLink> findById(JpaPid thePid) {
193                return myMdmLinkDao.findById(thePid.getId());
194        }
195
196        @Override
197        public void deleteAll(List<MdmLink> theLinks) {
198                myMdmLinkDao.deleteAll(theLinks);
199        }
200
201        @Override
202        public List<MdmLink> findAll(Example<MdmLink> theExample) {
203                return myMdmLinkDao.findAll(theExample);
204        }
205
206        @Override
207        public List<MdmLink> findAll() {
208                return myMdmLinkDao.findAll();
209        }
210
211        @Override
212        public Long count() {
213                return myMdmLinkDao.count();
214        }
215
216        @Override
217        public void deleteAll() {
218                myMdmLinkDao.deleteAll();
219        }
220
221        @Override
222        public MdmLink save(MdmLink theMdmLink) {
223                return myMdmLinkDao.save(theMdmLink);
224        }
225
226        @Override
227        public Optional<MdmLink> findOne(Example<MdmLink> theExample) {
228                return myMdmLinkDao.findOne(theExample);
229        }
230
231        @Override
232        public void delete(MdmLink theMdmLink) {
233                myMdmLinkDao.delete(theMdmLink);
234        }
235
236        @Override
237        public MdmLink validateMdmLink(IMdmLink theMdmLink) throws UnprocessableEntityException {
238                if (theMdmLink instanceof MdmLink) {
239                        return (MdmLink) theMdmLink;
240                } else {
241                        throw new UnprocessableEntityException(Msg.code(2109) + "Unprocessable MdmLink implementation");
242                }
243        }
244
245        @Override
246        @Deprecated
247        public Page<MdmLink> search(
248                        IIdType theGoldenResourceId,
249                        IIdType theSourceId,
250                        MdmMatchResultEnum theMatchResult,
251                        MdmLinkSourceEnum theLinkSource,
252                        MdmPageRequest thePageRequest,
253                        List<Integer> thePartitionIds) {
254                MdmQuerySearchParameters mdmQuerySearchParameters = new MdmQuerySearchParameters(thePageRequest)
255                                .setGoldenResourceId(theGoldenResourceId)
256                                .setSourceId(theSourceId)
257                                .setMatchResult(theMatchResult)
258                                .setLinkSource(theLinkSource)
259                                .setPartitionIds(thePartitionIds);
260                return search(mdmQuerySearchParameters);
261        }
262
263        @Override
264        public Page<MdmLink> search(MdmQuerySearchParameters theParams) {
265                Long totalResults = countTotalResults(theParams);
266
267                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
268                CriteriaQuery<MdmLink> criteriaQuery = criteriaBuilder.createQuery(MdmLink.class);
269                Root<MdmLink> from = criteriaQuery.from(MdmLink.class);
270                List<Order> orderList = getOrderList(theParams, criteriaBuilder, from);
271
272                List<Predicate> andPredicates = buildPredicates(theParams, criteriaBuilder, from);
273
274                Predicate finalQuery = criteriaBuilder.and(andPredicates.toArray(new Predicate[0]));
275                if (!orderList.isEmpty()) {
276                        criteriaQuery.orderBy(orderList);
277                }
278
279                MdmPageRequest pageRequest = theParams.getPageRequest();
280                TypedQuery<MdmLink> typedQuery = myEntityManager.createQuery(criteriaQuery.where(finalQuery));
281                List<MdmLink> result = typedQuery
282                                .setFirstResult(pageRequest.getOffset())
283                                .setMaxResults(pageRequest.getCount())
284                                .getResultList();
285
286                return new PageImpl<>(result, PageRequest.of(pageRequest.getPage(), pageRequest.getCount()), totalResults);
287        }
288
289        private Long countTotalResults(MdmQuerySearchParameters theParams) {
290                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
291                CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
292                Root<MdmLink> from = countQuery.from(MdmLink.class);
293
294                List<Predicate> andPredicates = buildPredicates(theParams, criteriaBuilder, from);
295                Predicate finalQuery = criteriaBuilder.and(andPredicates.toArray(new Predicate[0]));
296
297                countQuery.select(criteriaBuilder.count(from)).where(finalQuery);
298
299                return myEntityManager.createQuery(countQuery).getSingleResult();
300        }
301
302        @Nonnull
303        private List<Predicate> buildPredicates(
304                        MdmQuerySearchParameters theParams, CriteriaBuilder criteriaBuilder, Root<MdmLink> from) {
305                List<Predicate> andPredicates = new ArrayList<>();
306                if (theParams.getGoldenResourceId() != null) {
307                        Predicate goldenResourcePredicate = criteriaBuilder.equal(
308                                        from.get(GOLDEN_RESOURCE_PID_NAME),
309                                        (myIdHelperService.getPidOrThrowException(
310                                                                        RequestPartitionId.allPartitions(), theParams.getGoldenResourceId()))
311                                                        .getId());
312                        andPredicates.add(goldenResourcePredicate);
313                }
314                if (theParams.getSourceId() != null) {
315                        Predicate sourceIdPredicate = criteriaBuilder.equal(
316                                        from.get(SOURCE_PID_NAME),
317                                        (myIdHelperService.getPidOrThrowException(
318                                                                        RequestPartitionId.allPartitions(), theParams.getSourceId()))
319                                                        .getId());
320                        andPredicates.add(sourceIdPredicate);
321                }
322                if (theParams.getMatchResult() != null) {
323                        Predicate matchResultPredicate =
324                                        criteriaBuilder.equal(from.get(MATCH_RESULT_NAME), theParams.getMatchResult());
325                        andPredicates.add(matchResultPredicate);
326                }
327                if (theParams.getLinkSource() != null) {
328                        Predicate linkSourcePredicate =
329                                        criteriaBuilder.equal(from.get(LINK_SOURCE_NAME), theParams.getLinkSource());
330                        andPredicates.add(linkSourcePredicate);
331                }
332                if (!CollectionUtils.isEmpty(theParams.getPartitionIds())) {
333                        Expression<Integer> exp = from.get(PARTITION_ID_NAME).get(PARTITION_ID_NAME);
334                        Predicate linkSourcePredicate = exp.in(theParams.getPartitionIds());
335                        andPredicates.add(linkSourcePredicate);
336                }
337
338                if (theParams.getResourceType() != null) {
339                        Predicate resourceTypePredicate = criteriaBuilder.equal(
340                                        from.get(GOLDEN_RESOURCE_NAME).get(RESOURCE_TYPE_NAME), theParams.getResourceType());
341                        andPredicates.add(resourceTypePredicate);
342                }
343
344                return andPredicates;
345        }
346
347        private List<Order> getOrderList(
348                        MdmQuerySearchParameters theParams, CriteriaBuilder criteriaBuilder, Root<MdmLink> from) {
349                if (CollectionUtils.isEmpty(theParams.getSort())) {
350                        return Collections.emptyList();
351                }
352
353                return theParams.getSort().stream()
354                                .map(sortSpec -> {
355                                        Path<Object> path = from.get(sortSpec.getParamName());
356                                        return sortSpec.getOrder() == SortOrderEnum.DESC
357                                                        ? criteriaBuilder.desc(path)
358                                                        : criteriaBuilder.asc(path);
359                                })
360                                .collect(Collectors.toList());
361        }
362
363        @Override
364        public Optional<MdmLink> findBySourcePidAndMatchResult(JpaPid theSourcePid, MdmMatchResultEnum theMatch) {
365                return myMdmLinkDao.findBySourcePidAndMatchResult((theSourcePid).getId(), theMatch);
366        }
367
368        @Override
369        public void deleteLinksWithAnyReferenceToPids(List<JpaPid> theResourcePersistentIds) {
370                List<Long> goldenResourcePids =
371                                theResourcePersistentIds.stream().map(JpaPid::getId).collect(Collectors.toList());
372                // Split into chunks of 500 so older versions of Oracle don't run into issues (500 = 1000 / 2 since the dao
373                // method uses the list twice in the sql predicate)
374                List<List<Long>> chunks = ListUtils.partition(goldenResourcePids, 500);
375                for (List<Long> chunk : chunks) {
376                        myMdmLinkDao.deleteLinksWithAnyReferenceToPids(chunk);
377                        myMdmLinkDao.deleteLinksHistoryWithAnyReferenceToPids(chunk);
378                }
379        }
380
381        // TODO: LD:  delete for good on the next bump
382        @Override
383        @Deprecated(since = "6.5.6", forRemoval = true)
384        public Revisions<Long, MdmLink> findHistory(JpaPid theMdmLinkPid) {
385                final Revisions<Long, MdmLink> revisions = myMdmLinkDao.findRevisions(theMdmLinkPid.getId());
386
387                revisions.forEach(revision -> ourLog.debug("MdmLink revision: {}", revision));
388
389                return revisions;
390        }
391
392        @Override
393        public List<MdmLinkWithRevision<MdmLink>> getHistoryForIds(
394                        MdmHistorySearchParameters theMdmHistorySearchParameters) {
395                final AuditQueryCreator auditQueryCreator = myAuditReader.createQuery();
396
397                try {
398                        final AuditCriterion goldenResourceIdCriterion = buildAuditCriterionOrNull(
399                                        theMdmHistorySearchParameters.getGoldenResourceIds(), GOLDEN_RESOURCE_PID_NAME);
400
401                        final AuditCriterion resourceIdCriterion =
402                                        buildAuditCriterionOrNull(theMdmHistorySearchParameters.getSourceIds(), SOURCE_PID_NAME);
403
404                        final AuditCriterion goldenResourceAndOrResourceIdCriterion;
405
406                        if (!theMdmHistorySearchParameters.getGoldenResourceIds().isEmpty()
407                                        && !theMdmHistorySearchParameters.getSourceIds().isEmpty()) {
408
409                                // Make sure the criterion does not contain empty IN clause, e.g. id IN (), which postgres (likely other
410                                // sql servers) do not like. Directly return empty result instead.
411                                if (ObjectUtils.anyNull(goldenResourceIdCriterion, resourceIdCriterion)) {
412                                        return new ArrayList<>();
413                                }
414                                goldenResourceAndOrResourceIdCriterion =
415                                                AuditEntity.and(goldenResourceIdCriterion, resourceIdCriterion);
416
417                        } else if (!theMdmHistorySearchParameters.getGoldenResourceIds().isEmpty()) {
418
419                                if (ObjectUtils.anyNull(goldenResourceIdCriterion)) {
420                                        return new ArrayList<>();
421                                }
422                                goldenResourceAndOrResourceIdCriterion = goldenResourceIdCriterion;
423
424                        } else if (!theMdmHistorySearchParameters.getSourceIds().isEmpty()) {
425
426                                if (ObjectUtils.anyNull(resourceIdCriterion)) {
427                                        return new ArrayList<>();
428                                }
429                                goldenResourceAndOrResourceIdCriterion = resourceIdCriterion;
430
431                        } else {
432                                throw new IllegalArgumentException(Msg.code(2298)
433                                                + "$mdm-link-history Golden resource and source query IDs cannot both be empty.");
434                        }
435
436                        @SuppressWarnings("unchecked")
437                        final List<Object[]> mdmLinksWithRevisions = auditQueryCreator
438                                        .forRevisionsOfEntity(MdmLink.class, false, false)
439                                        .add(goldenResourceAndOrResourceIdCriterion)
440                                        .addOrder(AuditEntity.property(GOLDEN_RESOURCE_PID_NAME).asc())
441                                        .addOrder(AuditEntity.property(SOURCE_PID_NAME).asc())
442                                        .addOrder(AuditEntity.revisionNumber().desc())
443                                        .getResultList();
444
445                        return mdmLinksWithRevisions.stream()
446                                        .map(this::buildRevisionFromObjectArray)
447                                        .collect(Collectors.toUnmodifiableList());
448                } catch (IllegalStateException exception) {
449                        ourLog.error("got an Exception when trying to invoke Envers:", exception);
450                        throw new IllegalStateException(
451                                        Msg.code(2291)
452                                                        + "Hibernate envers AuditReader is returning Service is not yet initialized but front-end validation has not caught the error that envers is disabled");
453                }
454        }
455
456        @Nonnull
457        private List<Long> convertToLongIds(List<IIdType> theMdmHistorySearchParameters) {
458                return myIdHelperService
459                                .resolveResourceIdentities(
460                                                RequestPartitionId.allPartitions(),
461                                                theMdmHistorySearchParameters,
462                                                ResolveIdentityMode.includeDeleted().cacheOk())
463                                .values()
464                                .stream()
465                                .map(t -> t.getPersistentId().getId())
466                                .collect(Collectors.toUnmodifiableList());
467        }
468
469        private AuditCriterion buildAuditCriterionOrNull(
470                        List<IIdType> theMdmHistorySearchParameterIds, String theProperty) {
471                List<Long> longIds = convertToLongIds(theMdmHistorySearchParameterIds);
472                return longIds.isEmpty() ? null : AuditEntity.property(theProperty).in(longIds);
473        }
474
475        private MdmLinkWithRevision<MdmLink> buildRevisionFromObjectArray(Object[] theArray) {
476                final Object mdmLinkUncast = theArray[0];
477                final Object revisionUncast = theArray[1];
478                final Object revisionTypeUncast = theArray[2];
479
480                Validate.isInstanceOf(MdmLink.class, mdmLinkUncast);
481                Validate.isInstanceOf(HapiFhirEnversRevision.class, revisionUncast);
482                Validate.isInstanceOf(RevisionType.class, revisionTypeUncast);
483
484                final HapiFhirEnversRevision revision = (HapiFhirEnversRevision) revisionUncast;
485
486                return new MdmLinkWithRevision<>(
487                                (MdmLink) mdmLinkUncast,
488                                new EnversRevision((RevisionType) revisionTypeUncast, revision.getRev(), revision.getRevtstmp()));
489        }
490}