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