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 jakarta.validation.constraints.NotNull;
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.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 List<JpaPid> findPidByResourceNameAndThreshold(
156                        String theResourceName, Date theHighThreshold, Pageable thePageable) {
157                return myMdmLinkDao.findPidByResourceNameAndThreshold(theResourceName, theHighThreshold, thePageable).stream()
158                                .map(JpaPid::fromId)
159                                .collect(Collectors.toList());
160        }
161
162        @Override
163        public List<JpaPid> findPidByResourceNameAndThresholdAndPartitionId(
164                        String theResourceName, Date theHighThreshold, List<Integer> thePartitionIds, Pageable thePageable) {
165                return myMdmLinkDao
166                                .findPidByResourceNameAndThresholdAndPartitionId(
167                                                theResourceName, theHighThreshold, thePartitionIds, thePageable)
168                                .stream()
169                                .map(JpaPid::fromId)
170                                .collect(Collectors.toList());
171        }
172
173        @Override
174        public List<MdmLink> findAllById(List<JpaPid> thePids) {
175                List<Long> theLongPids = thePids.stream().map(JpaPid::getId).collect(Collectors.toList());
176                return myMdmLinkDao.findAllById(theLongPids);
177        }
178
179        @Override
180        public Optional<MdmLink> findById(JpaPid thePid) {
181                return myMdmLinkDao.findById(thePid.getId());
182        }
183
184        @Override
185        public void deleteAll(List<MdmLink> theLinks) {
186                myMdmLinkDao.deleteAll(theLinks);
187        }
188
189        @Override
190        public List<MdmLink> findAll(Example<MdmLink> theExample) {
191                return myMdmLinkDao.findAll(theExample);
192        }
193
194        @Override
195        public List<MdmLink> findAll() {
196                return myMdmLinkDao.findAll();
197        }
198
199        @Override
200        public Long count() {
201                return myMdmLinkDao.count();
202        }
203
204        @Override
205        public void deleteAll() {
206                myMdmLinkDao.deleteAll();
207        }
208
209        @Override
210        public MdmLink save(MdmLink theMdmLink) {
211                return myMdmLinkDao.save(theMdmLink);
212        }
213
214        @Override
215        public Optional<MdmLink> findOne(Example<MdmLink> theExample) {
216                return myMdmLinkDao.findOne(theExample);
217        }
218
219        @Override
220        public void delete(MdmLink theMdmLink) {
221                myMdmLinkDao.delete(theMdmLink);
222        }
223
224        @Override
225        public MdmLink validateMdmLink(IMdmLink theMdmLink) throws UnprocessableEntityException {
226                if (theMdmLink instanceof MdmLink) {
227                        return (MdmLink) theMdmLink;
228                } else {
229                        throw new UnprocessableEntityException(Msg.code(2109) + "Unprocessable MdmLink implementation");
230                }
231        }
232
233        @Override
234        @Deprecated
235        public Page<MdmLink> search(
236                        IIdType theGoldenResourceId,
237                        IIdType theSourceId,
238                        MdmMatchResultEnum theMatchResult,
239                        MdmLinkSourceEnum theLinkSource,
240                        MdmPageRequest thePageRequest,
241                        List<Integer> thePartitionIds) {
242                MdmQuerySearchParameters mdmQuerySearchParameters = new MdmQuerySearchParameters(thePageRequest)
243                                .setGoldenResourceId(theGoldenResourceId)
244                                .setSourceId(theSourceId)
245                                .setMatchResult(theMatchResult)
246                                .setLinkSource(theLinkSource)
247                                .setPartitionIds(thePartitionIds);
248                return search(mdmQuerySearchParameters);
249        }
250
251        @Override
252        public Page<MdmLink> search(MdmQuerySearchParameters theParams) {
253                Long totalResults = countTotalResults(theParams);
254
255                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
256                CriteriaQuery<MdmLink> criteriaQuery = criteriaBuilder.createQuery(MdmLink.class);
257                Root<MdmLink> from = criteriaQuery.from(MdmLink.class);
258                List<Order> orderList = getOrderList(theParams, criteriaBuilder, from);
259
260                List<Predicate> andPredicates = buildPredicates(theParams, criteriaBuilder, from);
261
262                Predicate finalQuery = criteriaBuilder.and(andPredicates.toArray(new Predicate[0]));
263                if (!orderList.isEmpty()) {
264                        criteriaQuery.orderBy(orderList);
265                }
266
267                MdmPageRequest pageRequest = theParams.getPageRequest();
268                TypedQuery<MdmLink> typedQuery = myEntityManager.createQuery(criteriaQuery.where(finalQuery));
269                List<MdmLink> result = typedQuery
270                                .setFirstResult(pageRequest.getOffset())
271                                .setMaxResults(pageRequest.getCount())
272                                .getResultList();
273
274                return new PageImpl<>(result, PageRequest.of(pageRequest.getPage(), pageRequest.getCount()), totalResults);
275        }
276
277        private Long countTotalResults(MdmQuerySearchParameters theParams) {
278                CriteriaBuilder criteriaBuilder = myEntityManager.getCriteriaBuilder();
279                CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
280                Root<MdmLink> from = countQuery.from(MdmLink.class);
281
282                List<Predicate> andPredicates = buildPredicates(theParams, criteriaBuilder, from);
283                Predicate finalQuery = criteriaBuilder.and(andPredicates.toArray(new Predicate[0]));
284
285                countQuery.select(criteriaBuilder.count(from)).where(finalQuery);
286
287                return myEntityManager.createQuery(countQuery).getSingleResult();
288        }
289
290        @NotNull
291        private List<Predicate> buildPredicates(
292                        MdmQuerySearchParameters theParams, CriteriaBuilder criteriaBuilder, Root<MdmLink> from) {
293                List<Predicate> andPredicates = new ArrayList<>();
294                if (theParams.getGoldenResourceId() != null) {
295                        Predicate goldenResourcePredicate = criteriaBuilder.equal(
296                                        from.get(GOLDEN_RESOURCE_PID_NAME),
297                                        (myIdHelperService.getPidOrThrowException(
298                                                                        RequestPartitionId.allPartitions(), theParams.getGoldenResourceId()))
299                                                        .getId());
300                        andPredicates.add(goldenResourcePredicate);
301                }
302                if (theParams.getSourceId() != null) {
303                        Predicate sourceIdPredicate = criteriaBuilder.equal(
304                                        from.get(SOURCE_PID_NAME),
305                                        (myIdHelperService.getPidOrThrowException(
306                                                                        RequestPartitionId.allPartitions(), theParams.getSourceId()))
307                                                        .getId());
308                        andPredicates.add(sourceIdPredicate);
309                }
310                if (theParams.getMatchResult() != null) {
311                        Predicate matchResultPredicate =
312                                        criteriaBuilder.equal(from.get(MATCH_RESULT_NAME), theParams.getMatchResult());
313                        andPredicates.add(matchResultPredicate);
314                }
315                if (theParams.getLinkSource() != null) {
316                        Predicate linkSourcePredicate =
317                                        criteriaBuilder.equal(from.get(LINK_SOURCE_NAME), theParams.getLinkSource());
318                        andPredicates.add(linkSourcePredicate);
319                }
320                if (!CollectionUtils.isEmpty(theParams.getPartitionIds())) {
321                        Expression<Integer> exp = from.get(PARTITION_ID_NAME).get(PARTITION_ID_NAME);
322                        Predicate linkSourcePredicate = exp.in(theParams.getPartitionIds());
323                        andPredicates.add(linkSourcePredicate);
324                }
325
326                if (theParams.getResourceType() != null) {
327                        Predicate resourceTypePredicate = criteriaBuilder.equal(
328                                        from.get(GOLDEN_RESOURCE_NAME).get(RESOURCE_TYPE_NAME), theParams.getResourceType());
329                        andPredicates.add(resourceTypePredicate);
330                }
331
332                return andPredicates;
333        }
334
335        private List<Order> getOrderList(
336                        MdmQuerySearchParameters theParams, CriteriaBuilder criteriaBuilder, Root<MdmLink> from) {
337                if (CollectionUtils.isEmpty(theParams.getSort())) {
338                        return Collections.emptyList();
339                }
340
341                return theParams.getSort().stream()
342                                .map(sortSpec -> {
343                                        Path<Object> path = from.get(sortSpec.getParamName());
344                                        return sortSpec.getOrder() == SortOrderEnum.DESC
345                                                        ? criteriaBuilder.desc(path)
346                                                        : criteriaBuilder.asc(path);
347                                })
348                                .collect(Collectors.toList());
349        }
350
351        @Override
352        public Optional<MdmLink> findBySourcePidAndMatchResult(JpaPid theSourcePid, MdmMatchResultEnum theMatch) {
353                return myMdmLinkDao.findBySourcePidAndMatchResult((theSourcePid).getId(), theMatch);
354        }
355
356        @Override
357        public void deleteLinksWithAnyReferenceToPids(List<JpaPid> theResourcePersistentIds) {
358                List<Long> goldenResourcePids =
359                                theResourcePersistentIds.stream().map(JpaPid::getId).collect(Collectors.toList());
360                // Split into chunks of 500 so older versions of Oracle don't run into issues (500 = 1000 / 2 since the dao
361                // method uses the list twice in the sql predicate)
362                List<List<Long>> chunks = ListUtils.partition(goldenResourcePids, 500);
363                for (List<Long> chunk : chunks) {
364                        myMdmLinkDao.deleteLinksWithAnyReferenceToPids(chunk);
365                        myMdmLinkDao.deleteLinksHistoryWithAnyReferenceToPids(chunk);
366                }
367        }
368
369        // TODO: LD:  delete for good on the next bump
370        @Override
371        @Deprecated(since = "6.5.6", forRemoval = true)
372        public Revisions<Long, MdmLink> findHistory(JpaPid theMdmLinkPid) {
373                final Revisions<Long, MdmLink> revisions = myMdmLinkDao.findRevisions(theMdmLinkPid.getId());
374
375                revisions.forEach(revision -> ourLog.debug("MdmLink revision: {}", revision));
376
377                return revisions;
378        }
379
380        @Override
381        public List<MdmLinkWithRevision<MdmLink>> getHistoryForIds(
382                        MdmHistorySearchParameters theMdmHistorySearchParameters) {
383                final AuditQueryCreator auditQueryCreator = myAuditReader.createQuery();
384
385                try {
386                        final AuditCriterion goldenResourceIdCriterion = buildAuditCriterionOrNull(
387                                        theMdmHistorySearchParameters.getGoldenResourceIds(), GOLDEN_RESOURCE_PID_NAME);
388
389                        final AuditCriterion resourceIdCriterion =
390                                        buildAuditCriterionOrNull(theMdmHistorySearchParameters.getSourceIds(), SOURCE_PID_NAME);
391
392                        final AuditCriterion goldenResourceAndOrResourceIdCriterion;
393
394                        if (!theMdmHistorySearchParameters.getGoldenResourceIds().isEmpty()
395                                        && !theMdmHistorySearchParameters.getSourceIds().isEmpty()) {
396
397                                // Make sure the criterion does not contain empty IN clause, e.g. id IN (), which postgres (likely other
398                                // sql servers) do not like. Directly return empty result instead.
399                                if (ObjectUtils.anyNull(goldenResourceIdCriterion, resourceIdCriterion)) {
400                                        return new ArrayList<>();
401                                }
402                                goldenResourceAndOrResourceIdCriterion =
403                                                AuditEntity.and(goldenResourceIdCriterion, resourceIdCriterion);
404
405                        } else if (!theMdmHistorySearchParameters.getGoldenResourceIds().isEmpty()) {
406
407                                if (ObjectUtils.anyNull(goldenResourceIdCriterion)) {
408                                        return new ArrayList<>();
409                                }
410                                goldenResourceAndOrResourceIdCriterion = goldenResourceIdCriterion;
411
412                        } else if (!theMdmHistorySearchParameters.getSourceIds().isEmpty()) {
413
414                                if (ObjectUtils.anyNull(resourceIdCriterion)) {
415                                        return new ArrayList<>();
416                                }
417                                goldenResourceAndOrResourceIdCriterion = resourceIdCriterion;
418
419                        } else {
420                                throw new IllegalArgumentException(Msg.code(2298)
421                                                + "$mdm-link-history Golden resource and source query IDs cannot both be empty.");
422                        }
423
424                        @SuppressWarnings("unchecked")
425                        final List<Object[]> mdmLinksWithRevisions = auditQueryCreator
426                                        .forRevisionsOfEntity(MdmLink.class, false, false)
427                                        .add(goldenResourceAndOrResourceIdCriterion)
428                                        .addOrder(AuditEntity.property(GOLDEN_RESOURCE_PID_NAME).asc())
429                                        .addOrder(AuditEntity.property(SOURCE_PID_NAME).asc())
430                                        .addOrder(AuditEntity.revisionNumber().desc())
431                                        .getResultList();
432
433                        return mdmLinksWithRevisions.stream()
434                                        .map(this::buildRevisionFromObjectArray)
435                                        .collect(Collectors.toUnmodifiableList());
436                } catch (IllegalStateException exception) {
437                        ourLog.error("got an Exception when trying to invoke Envers:", exception);
438                        throw new IllegalStateException(
439                                        Msg.code(2291)
440                                                        + "Hibernate envers AuditReader is returning Service is not yet initialized but front-end validation has not caught the error that envers is disabled");
441                }
442        }
443
444        @Nonnull
445        private List<Long> convertToLongIds(List<IIdType> theMdmHistorySearchParameters) {
446                return myIdHelperService
447                                .getPidsOrThrowException(RequestPartitionId.allPartitions(), theMdmHistorySearchParameters)
448                                .stream()
449                                .map(JpaPid::getId)
450                                .collect(Collectors.toUnmodifiableList());
451        }
452
453        private AuditCriterion buildAuditCriterionOrNull(
454                        List<IIdType> theMdmHistorySearchParameterIds, String theProperty) {
455                List<Long> longIds = convertToLongIds(theMdmHistorySearchParameterIds);
456                return longIds.isEmpty() ? null : AuditEntity.property(theProperty).in(longIds);
457        }
458
459        private MdmLinkWithRevision<MdmLink> buildRevisionFromObjectArray(Object[] theArray) {
460                final Object mdmLinkUncast = theArray[0];
461                final Object revisionUncast = theArray[1];
462                final Object revisionTypeUncast = theArray[2];
463
464                Validate.isInstanceOf(MdmLink.class, mdmLinkUncast);
465                Validate.isInstanceOf(HapiFhirEnversRevision.class, revisionUncast);
466                Validate.isInstanceOf(RevisionType.class, revisionTypeUncast);
467
468                final HapiFhirEnversRevision revision = (HapiFhirEnversRevision) revisionUncast;
469
470                return new MdmLinkWithRevision<>(
471                                (MdmLink) mdmLinkUncast,
472                                new EnversRevision((RevisionType) revisionTypeUncast, revision.getRev(), revision.getRevtstmp()));
473        }
474}