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