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