001/*- 002 * #%L 003 * HAPI FHIR JPA Server - Master Data Management 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.mdm.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.model.RequestPartitionId; 024import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 025import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 026import ca.uhn.fhir.mdm.api.IMdmLink; 027import ca.uhn.fhir.mdm.api.MdmHistorySearchParameters; 028import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 029import ca.uhn.fhir.mdm.api.MdmLinkWithRevision; 030import ca.uhn.fhir.mdm.api.MdmMatchOutcome; 031import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 032import ca.uhn.fhir.mdm.api.MdmQuerySearchParameters; 033import ca.uhn.fhir.mdm.dao.IMdmLinkDao; 034import ca.uhn.fhir.mdm.dao.MdmLinkFactory; 035import ca.uhn.fhir.mdm.log.Logs; 036import ca.uhn.fhir.mdm.model.MdmTransactionContext; 037import ca.uhn.fhir.rest.api.Constants; 038import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 039import org.hl7.fhir.instance.model.api.IAnyResource; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.slf4j.Logger; 042import org.springframework.beans.factory.annotation.Autowired; 043import org.springframework.data.domain.Example; 044import org.springframework.data.domain.Page; 045import org.springframework.data.history.Revisions; 046import org.springframework.transaction.annotation.Propagation; 047import org.springframework.transaction.annotation.Transactional; 048 049import java.util.Collections; 050import java.util.Date; 051import java.util.List; 052import java.util.Optional; 053import javax.annotation.Nonnull; 054import javax.annotation.Nullable; 055 056public class MdmLinkDaoSvc<P extends IResourcePersistentId, M extends IMdmLink<P>> { 057 058 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 059 060 @Autowired 061 private IMdmLinkDao<P, M> myMdmLinkDao; 062 063 @Autowired 064 private MdmLinkFactory<M> myMdmLinkFactory; 065 066 @Autowired 067 private IIdHelperService<P> myIdHelperService; 068 069 @Autowired 070 private FhirContext myFhirContext; 071 072 @Transactional 073 public M createOrUpdateLinkEntity( 074 IAnyResource theGoldenResource, 075 IAnyResource theSourceResource, 076 MdmMatchOutcome theMatchOutcome, 077 MdmLinkSourceEnum theLinkSource, 078 @Nullable MdmTransactionContext theMdmTransactionContext) { 079 M mdmLink = getOrCreateMdmLinkByGoldenResourceAndSourceResource(theGoldenResource, theSourceResource); 080 mdmLink.setLinkSource(theLinkSource); 081 mdmLink.setMatchResult(theMatchOutcome.getMatchResultEnum()); 082 // Preserve these flags for link updates 083 mdmLink.setEidMatch(theMatchOutcome.isEidMatch() | mdmLink.isEidMatchPresent()); 084 mdmLink.setHadToCreateNewGoldenResource( 085 theMatchOutcome.isCreatedNewResource() | mdmLink.getHadToCreateNewGoldenResource()); 086 mdmLink.setMdmSourceType(myFhirContext.getResourceType(theSourceResource)); 087 088 setScoreProperties(theMatchOutcome, mdmLink); 089 090 // Add partition for the mdm link if it's available in the source resource 091 RequestPartitionId partitionId = 092 (RequestPartitionId) theSourceResource.getUserData(Constants.RESOURCE_PARTITION_ID); 093 if (partitionId != null && partitionId.getFirstPartitionIdOrNull() != null) { 094 mdmLink.setPartitionId(new PartitionablePartitionId( 095 partitionId.getFirstPartitionIdOrNull(), partitionId.getPartitionDate())); 096 } 097 098 String message = String.format( 099 "Creating %s link from %s to Golden Resource %s.", 100 mdmLink.getMatchResult(), 101 theSourceResource.getIdElement().toUnqualifiedVersionless(), 102 theGoldenResource.getIdElement().toUnqualifiedVersionless()); 103 theMdmTransactionContext.addTransactionLogMessage(message); 104 ourLog.debug(message); 105 save(mdmLink); 106 return mdmLink; 107 } 108 109 private void setScoreProperties(MdmMatchOutcome theMatchOutcome, M mdmLink) { 110 if (theMatchOutcome.getScore() != null) { 111 mdmLink.setScore( 112 mdmLink.getScore() != null 113 ? Math.max(theMatchOutcome.getNormalizedScore(), mdmLink.getScore()) 114 : theMatchOutcome.getNormalizedScore()); 115 } 116 117 if (theMatchOutcome.getVector() != null) { 118 mdmLink.setVector( 119 mdmLink.getVector() != null 120 ? Math.max(theMatchOutcome.getVector(), mdmLink.getVector()) 121 : theMatchOutcome.getVector()); 122 } 123 124 mdmLink.setRuleCount( 125 mdmLink.getRuleCount() != null 126 ? Math.max(theMatchOutcome.getMdmRuleCount(), mdmLink.getRuleCount()) 127 : theMatchOutcome.getMdmRuleCount()); 128 } 129 130 @Nonnull 131 public M getOrCreateMdmLinkByGoldenResourceAndSourceResource( 132 IAnyResource theGoldenResource, IAnyResource theSourceResource) { 133 P goldenResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource); 134 P sourceResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource); 135 Optional<M> oExisting = getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourcePid, sourceResourcePid); 136 if (oExisting.isPresent()) { 137 return oExisting.get(); 138 } else { 139 M newLink = myMdmLinkFactory.newMdmLink(); 140 newLink.setGoldenResourcePersistenceId(goldenResourcePid); 141 newLink.setSourcePersistenceId(sourceResourcePid); 142 return newLink; 143 } 144 } 145 146 /** 147 * Given a golden resource Pid and source Pid, return the mdm link that matches these criterias if exists 148 * 149 * @param theGoldenResourcePid 150 * @param theSourceResourcePid 151 * @return 152 * @deprecated This was deprecated in favour of using ResourcePersistenceId rather than longs 153 */ 154 @Deprecated 155 public Optional<M> getLinkByGoldenResourcePidAndSourceResourcePid( 156 Long theGoldenResourcePid, Long theSourceResourcePid) { 157 return getLinkByGoldenResourcePidAndSourceResourcePid( 158 myIdHelperService.newPid(theGoldenResourcePid), myIdHelperService.newPid(theSourceResourcePid)); 159 } 160 161 /** 162 * Given a golden resource Pid and source Pid, return the mdm link that matches these criterias if exists 163 * @param theGoldenResourcePid The ResourcePersistenceId of the golden resource 164 * @param theSourceResourcePid The ResourcepersistenceId of the Source resource 165 * @return The {@link IMdmLink} entity that matches these criteria if exists 166 */ 167 public Optional<M> getLinkByGoldenResourcePidAndSourceResourcePid(P theGoldenResourcePid, P theSourceResourcePid) { 168 if (theSourceResourcePid == null || theGoldenResourcePid == null) { 169 return Optional.empty(); 170 } 171 M link = myMdmLinkFactory.newMdmLinkVersionless(); 172 link.setSourcePersistenceId(theSourceResourcePid); 173 link.setGoldenResourcePersistenceId(theGoldenResourcePid); 174 175 // TODO - replace the use of example search 176 Example<M> example = Example.of(link); 177 178 return myMdmLinkDao.findOne(example); 179 } 180 181 /** 182 * Given a source resource Pid, and a match result, return all links that match these criteria. 183 * 184 * @param theSourcePid the source of the relationship. 185 * @param theMatchResult the Match Result of the relationship 186 * @return a list of {@link IMdmLink} entities matching these criteria. 187 */ 188 public List<M> getMdmLinksBySourcePidAndMatchResult(P theSourcePid, MdmMatchResultEnum theMatchResult) { 189 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 190 exampleLink.setSourcePersistenceId(theSourcePid); 191 exampleLink.setMatchResult(theMatchResult); 192 Example<M> example = Example.of(exampleLink); 193 return myMdmLinkDao.findAll(example); 194 } 195 196 /** 197 * Given a source Pid, return its Matched {@link IMdmLink}. There can only ever be at most one of these, but its possible 198 * the source has no matches, and may return an empty optional. 199 * 200 * @param theSourcePid The Pid of the source you wish to find the matching link for. 201 * @return the {@link IMdmLink} that contains the Match information for the source. 202 */ 203 @Deprecated 204 @Transactional 205 public Optional<M> getMatchedLinkForSourcePid(P theSourcePid) { 206 return myMdmLinkDao.findBySourcePidAndMatchResult(theSourcePid, MdmMatchResultEnum.MATCH); 207 } 208 209 /** 210 * Given an IBaseResource, return its Matched {@link IMdmLink}. There can only ever be at most one of these, but its possible 211 * the source has no matches, and may return an empty optional. 212 * 213 * @param theSourceResource The IBaseResource representing the source you wish to find the matching link for. 214 * @return the {@link IMdmLink} that contains the Match information for the source. 215 */ 216 public Optional<M> getMatchedLinkForSource(IBaseResource theSourceResource) { 217 return getMdmLinkWithMatchResult(theSourceResource, MdmMatchResultEnum.MATCH); 218 } 219 220 public Optional<M> getPossibleMatchedLinkForSource(IBaseResource theSourceResource) { 221 return getMdmLinkWithMatchResult(theSourceResource, MdmMatchResultEnum.POSSIBLE_MATCH); 222 } 223 224 @Nonnull 225 private Optional<M> getMdmLinkWithMatchResult(IBaseResource theSourceResource, MdmMatchResultEnum theMatchResult) { 226 P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource); 227 if (pid == null) { 228 return Optional.empty(); 229 } 230 231 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 232 exampleLink.setSourcePersistenceId(pid); 233 exampleLink.setMatchResult(theMatchResult); 234 Example<M> example = Example.of(exampleLink); 235 return myMdmLinkDao.findOne(example); 236 } 237 238 /** 239 * Given a golden resource a source and a match result, return the matching {@link IMdmLink}, if it exists. 240 * 241 * @param theGoldenResourcePid The Pid of the Golden Resource in the relationship 242 * @param theSourcePid The Pid of the source in the relationship 243 * @param theMatchResult The MatchResult you are looking for. 244 * @return an Optional {@link IMdmLink} containing the matched link if it exists. 245 */ 246 public Optional<M> getMdmLinksByGoldenResourcePidSourcePidAndMatchResult( 247 Long theGoldenResourcePid, Long theSourcePid, MdmMatchResultEnum theMatchResult) { 248 return getMdmLinksByGoldenResourcePidSourcePidAndMatchResult( 249 myIdHelperService.newPid(theGoldenResourcePid), myIdHelperService.newPid(theSourcePid), theMatchResult); 250 } 251 252 public Optional<M> getMdmLinksByGoldenResourcePidSourcePidAndMatchResult( 253 P theGoldenResourcePid, P theSourcePid, MdmMatchResultEnum theMatchResult) { 254 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 255 exampleLink.setGoldenResourcePersistenceId(theGoldenResourcePid); 256 exampleLink.setSourcePersistenceId(theSourcePid); 257 exampleLink.setMatchResult(theMatchResult); 258 Example<M> example = Example.of(exampleLink); 259 return myMdmLinkDao.findOne(example); 260 } 261 262 /** 263 * Get all {@link IMdmLink} which have {@link MdmMatchResultEnum#POSSIBLE_DUPLICATE} as their match result. 264 * 265 * @return A list of {@link IMdmLink} that hold potential duplicate golden resources. 266 */ 267 public List<M> getPossibleDuplicates() { 268 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 269 exampleLink.setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE); 270 Example<M> example = Example.of(exampleLink); 271 return myMdmLinkDao.findAll(example); 272 } 273 274 @Transactional 275 public Optional<M> findMdmLinkBySource(IBaseResource theSourceResource) { 276 @Nullable P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource); 277 if (pid == null) { 278 return Optional.empty(); 279 } 280 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 281 exampleLink.setSourcePersistenceId(pid); 282 Example<M> example = Example.of(exampleLink); 283 return myMdmLinkDao.findOne(example); 284 } 285 /** 286 * Delete a given {@link IMdmLink}. Note that this does not clear out the Golden resource. 287 * It is a simple entity delete. 288 * 289 * @param theMdmLink the {@link IMdmLink} to delete. 290 */ 291 @Transactional(propagation = Propagation.REQUIRES_NEW) 292 public void deleteLink(M theMdmLink) { 293 myMdmLinkDao.validateMdmLink(theMdmLink); 294 myMdmLinkDao.delete(theMdmLink); 295 } 296 297 /** 298 * Given a Golden Resource, return all links in which they are the source Golden Resource of the {@link IMdmLink} 299 * 300 * @param theGoldenResource The {@link IBaseResource} Golden Resource who's links you would like to retrieve. 301 * @return A list of all {@link IMdmLink} entities in which theGoldenResource is the source Golden Resource 302 */ 303 @Transactional 304 public List<M> findMdmLinksByGoldenResource(IBaseResource theGoldenResource) { 305 P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource); 306 if (pid == null) { 307 return Collections.emptyList(); 308 } 309 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 310 exampleLink.setGoldenResourcePersistenceId(pid); 311 Example<M> example = Example.of(exampleLink); 312 return myMdmLinkDao.findAll(example); 313 } 314 315 /** 316 * Persist an MDM link to the database. 317 * 318 * @param theMdmLink the link to save. 319 * @return the persisted {@link IMdmLink} entity. 320 */ 321 public M save(M theMdmLink) { 322 M mdmLink = myMdmLinkDao.validateMdmLink(theMdmLink); 323 if (mdmLink.getCreated() == null) { 324 mdmLink.setCreated(new Date()); 325 } 326 mdmLink.setUpdated(new Date()); 327 return myMdmLinkDao.save(mdmLink); 328 } 329 330 /** 331 * Given a list of criteria, return all links from the database which fits the criteria provided 332 * 333 * @param theMdmQuerySearchParameters The {@link MdmQuerySearchParameters} being searched. 334 * @return a list of {@link IMdmLink} entities which match the example. 335 */ 336 public Page<M> executeTypedQuery(MdmQuerySearchParameters theMdmQuerySearchParameters) { 337 return myMdmLinkDao.search(theMdmQuerySearchParameters); 338 } 339 340 /** 341 * Given a source {@link IBaseResource}, return all {@link IMdmLink} entities in which this source is the source 342 * of the relationship. This will show you all links for a given Patient/Practitioner. 343 * 344 * @param theSourceResource the source resource to find links for. 345 * @return all links for the source. 346 */ 347 @Transactional 348 public List<M> findMdmLinksBySourceResource(IBaseResource theSourceResource) { 349 P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource); 350 if (pid == null) { 351 return Collections.emptyList(); 352 } 353 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 354 exampleLink.setSourcePersistenceId(pid); 355 Example<M> example = Example.of(exampleLink); 356 return myMdmLinkDao.findAll(example); 357 } 358 359 /** 360 * Finds all {@link IMdmLink} entities in which theGoldenResource's PID is the source 361 * of the relationship. 362 * 363 * @param theGoldenResource the source resource to find links for. 364 * @return all links for the source. 365 */ 366 public List<M> findMdmMatchLinksByGoldenResource(IBaseResource theGoldenResource) { 367 P pid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource); 368 if (pid == null) { 369 return Collections.emptyList(); 370 } 371 M exampleLink = myMdmLinkFactory.newMdmLinkVersionless(); 372 exampleLink.setGoldenResourcePersistenceId(pid); 373 exampleLink.setMatchResult(MdmMatchResultEnum.MATCH); 374 Example<M> example = Example.of(exampleLink); 375 return myMdmLinkDao.findAll(example); 376 } 377 378 /** 379 * Factory delegation method, whenever you need a new MdmLink, use this factory method. 380 * //TODO Should we make the constructor private for MdmLink? or work out some way to ensure they can only be instantiated via factory. 381 * 382 * @return A new {@link IMdmLink}. 383 */ 384 public IMdmLink newMdmLink() { 385 return myMdmLinkFactory.newMdmLink(); 386 } 387 388 public Optional<M> getMatchedOrPossibleMatchedLinkForSource(IAnyResource theResource) { 389 // TODO KHS instead of two queries, just do one query with an OR 390 Optional<M> retval = getMatchedLinkForSource(theResource); 391 if (!retval.isPresent()) { 392 retval = getPossibleMatchedLinkForSource(theResource); 393 } 394 return retval; 395 } 396 397 public Optional<M> getLinkByGoldenResourceAndSourceResource( 398 @Nullable IAnyResource theGoldenResource, @Nullable IAnyResource theSourceResource) { 399 if (theGoldenResource == null || theSourceResource == null) { 400 return Optional.empty(); 401 } 402 return getLinkByGoldenResourcePidAndSourceResourcePid( 403 myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource), 404 myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource)); 405 } 406 407 @Transactional(propagation = Propagation.MANDATORY) 408 public void deleteLinksWithAnyReferenceToPids(List<P> theGoldenResourcePids) { 409 myMdmLinkDao.deleteLinksWithAnyReferenceToPids(theGoldenResourcePids); 410 } 411 412 // TODO: LD: delete for good on the next bump 413 @Deprecated(since = "6.5.7", forRemoval = true) 414 public Revisions<Long, M> findMdmLinkHistory(M mdmLink) { 415 return myMdmLinkDao.findHistory(mdmLink.getId()); 416 } 417 418 @Transactional 419 public List<MdmLinkWithRevision<M>> findMdmLinkHistory(MdmHistorySearchParameters theMdmHistorySearchParameters) { 420 return myMdmLinkDao.getHistoryForIds(theMdmHistorySearchParameters); 421 } 422}