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