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}