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}