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}