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