
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 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.index; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.RuntimeResourceDefinition; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.interceptor.api.HookParams; 029import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 030import ca.uhn.fhir.interceptor.api.Pointcut; 031import ca.uhn.fhir.interceptor.model.RequestPartitionId; 032import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 033import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 034import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 035import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 036import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 037import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 038import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 039import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 040import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 041import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; 042import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef; 043import ca.uhn.fhir.rest.api.server.RequestDetails; 044import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 045import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 046import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 047import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 048import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 049import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 050import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 051import ca.uhn.fhir.storage.interceptor.AutoCreatePlaceholderReferenceTargetRequest; 052import ca.uhn.fhir.storage.interceptor.AutoCreatePlaceholderReferenceTargetResponse; 053import ca.uhn.fhir.util.CanonicalIdentifier; 054import ca.uhn.fhir.util.HapiExtensions; 055import ca.uhn.fhir.util.TerserUtil; 056import jakarta.annotation.Nonnull; 057import jakarta.annotation.Nullable; 058import org.apache.commons.lang3.Validate; 059import org.apache.http.NameValuePair; 060import org.apache.http.client.utils.URLEncodedUtils; 061import org.hl7.fhir.instance.model.api.IBase; 062import org.hl7.fhir.instance.model.api.IBaseExtension; 063import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 064import org.hl7.fhir.instance.model.api.IBaseReference; 065import org.hl7.fhir.instance.model.api.IBaseResource; 066import org.hl7.fhir.instance.model.api.IIdType; 067import org.hl7.fhir.instance.model.api.IPrimitiveType; 068import org.springframework.beans.factory.annotation.Autowired; 069 070import java.nio.charset.StandardCharsets; 071import java.util.Date; 072import java.util.List; 073import java.util.Optional; 074 075import static org.apache.commons.lang3.StringUtils.isBlank; 076 077public class DaoResourceLinkResolver<T extends IResourcePersistentId<?>> implements IResourceLinkResolver { 078 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DaoResourceLinkResolver.class); 079 080 @Autowired 081 private JpaStorageSettings myStorageSettings; 082 083 @Autowired 084 private FhirContext myContext; 085 086 @Autowired 087 private IIdHelperService<T> myIdHelperService; 088 089 @Autowired 090 private DaoRegistry myDaoRegistry; 091 092 @Autowired 093 private IHapiTransactionService myTransactionService; 094 095 @Autowired 096 private IInterceptorBroadcaster myInterceptorBroadcaster; 097 098 @Override 099 public IResourceLookup findTargetResource( 100 @Nonnull RequestPartitionId theRequestPartitionId, 101 String theSourceResourceName, 102 PathAndRef thePathAndRef, 103 RequestDetails theRequest, 104 TransactionDetails theTransactionDetails) { 105 106 IBaseReference targetReference = thePathAndRef.getRef(); 107 String sourcePath = thePathAndRef.getPath(); 108 109 IIdType targetResourceId = targetReference.getReferenceElement(); 110 if (targetResourceId.isEmpty() && targetReference.getResource() != null) { 111 targetResourceId = targetReference.getResource().getIdElement(); 112 } 113 114 String resourceType = targetResourceId.getResourceType(); 115 RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(resourceType); 116 Class<? extends IBaseResource> type = resourceDef.getImplementingClass(); 117 118 T persistentId = null; 119 if (theTransactionDetails != null) { 120 T resolvedResourceId = (T) theTransactionDetails.getResolvedResourceId(targetResourceId); 121 if (resolvedResourceId != null 122 && resolvedResourceId.getId() != null 123 && resolvedResourceId.getAssociatedResourceId() != null) { 124 persistentId = resolvedResourceId; 125 } 126 } 127 128 IResourceLookup<?> resolvedResource; 129 String idPart = targetResourceId.getIdPart(); 130 try { 131 if (persistentId == null) { 132 133 // If we previously looked up the ID, and it was not found, don't bother 134 // looking it up again 135 if (theTransactionDetails != null 136 && theTransactionDetails.hasNullResolvedResourceId(targetResourceId)) { 137 throw new ResourceNotFoundException(Msg.code(2602)); 138 } 139 140 resolvedResource = myIdHelperService.resolveResourceIdentity( 141 theRequestPartitionId, 142 resourceType, 143 idPart, 144 ResolveIdentityMode.excludeDeleted().noCacheUnlessDeletesDisabled()); 145 ourLog.trace("Translated {}/{} to resource PID {}", type, idPart, resolvedResource); 146 } else { 147 resolvedResource = new ResourceLookupPersistentIdWrapper<>(persistentId); 148 } 149 } catch (ResourceNotFoundException e) { 150 151 Optional<IBasePersistedResource> createdTableOpt = createPlaceholderTargetIfConfiguredToDoSo( 152 type, targetReference, idPart, theRequest, theTransactionDetails); 153 if (!createdTableOpt.isPresent()) { 154 155 if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) { 156 return null; 157 } 158 159 RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(type); 160 String resName = missingResourceDef.getName(); 161 162 // Check if this was a deleted resource 163 try { 164 resolvedResource = myIdHelperService.resolveResourceIdentity( 165 theRequestPartitionId, 166 resourceType, 167 idPart, 168 ResolveIdentityMode.includeDeleted().noCacheUnlessDeletesDisabled()); 169 handleDeletedTarget(resourceType, idPart, sourcePath); 170 } catch (ResourceNotFoundException e2) { 171 resolvedResource = null; 172 } 173 174 if (resolvedResource == null) { 175 throw new InvalidRequestException(Msg.code(1094) + "Resource " + resName + "/" + idPart 176 + " not found, specified in path: " + sourcePath); 177 } 178 } 179 180 resolvedResource = createdTableOpt.get(); 181 } 182 183 ourLog.trace( 184 "Resolved resource of type {} as PID: {}", 185 resolvedResource.getResourceType(), 186 resolvedResource.getPersistentId()); 187 if (!validateResolvedResourceOrThrow(resourceType, resolvedResource, targetResourceId, idPart, sourcePath)) { 188 return null; 189 } 190 191 if (persistentId == null) { 192 Object id = resolvedResource.getPersistentId().getId(); 193 Integer partitionId = null; 194 if (resolvedResource.getPartitionId() != null) { 195 partitionId = resolvedResource.getPartitionId().getPartitionId(); 196 } 197 persistentId = myIdHelperService.newPid(id, partitionId); 198 persistentId.setAssociatedResourceId(targetResourceId); 199 if (theTransactionDetails != null) { 200 theTransactionDetails.addResolvedResourceId(targetResourceId, persistentId); 201 } 202 } 203 204 return resolvedResource; 205 } 206 207 /** 208 * Validates the resolved resource. 209 * If 'Enforce Referential Integrity on Write' is enabled: 210 * Throws <code>UnprocessableEntityException</code> when resource types do not match 211 * Throws <code>InvalidRequestException</code> when the resolved resource was deleted 212 * <p> 213 * Otherwise, return false when resource types do not match or resource was deleted 214 * and return true if the resolved resource is valid. 215 */ 216 private boolean validateResolvedResourceOrThrow( 217 String resourceType, 218 IResourceLookup resolvedResource, 219 IIdType targetResourceId, 220 String idPart, 221 String sourcePath) { 222 if (!resourceType.equals(resolvedResource.getResourceType())) { 223 ourLog.error( 224 "Resource with PID {} was of type {} and wanted {}", 225 resolvedResource.getPersistentId(), 226 resourceType, 227 resolvedResource.getResourceType()); 228 if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) { 229 return false; 230 } 231 throw new UnprocessableEntityException(Msg.code(1095) 232 + "Resource contains reference to unknown resource ID " + targetResourceId.getValue()); 233 } 234 235 if (resolvedResource.getDeleted() != null) { 236 return handleDeletedTarget(resolvedResource.getResourceType(), idPart, sourcePath); 237 } 238 return true; 239 } 240 241 private boolean handleDeletedTarget(String resType, String idPart, String sourcePath) { 242 if (!myStorageSettings.isEnforceReferentialIntegrityOnWrite()) { 243 return false; 244 } 245 String resName = resType; 246 throw new InvalidRequestException(Msg.code(1096) + "Resource " + resName + "/" + idPart 247 + " is deleted, specified in path: " + sourcePath); 248 } 249 250 @Nullable 251 @Override 252 public IBaseResource loadTargetResource( 253 @Nonnull RequestPartitionId theRequestPartitionId, 254 String theSourceResourceName, 255 PathAndRef thePathAndRef, 256 RequestDetails theRequest, 257 TransactionDetails theTransactionDetails) { 258 return myTransactionService 259 .withRequest(theRequest) 260 .withTransactionDetails(theTransactionDetails) 261 .withRequestPartitionId(theRequestPartitionId) 262 .execute(() -> { 263 IIdType targetId = thePathAndRef.getRef().getReferenceElement(); 264 IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(targetId.getResourceType()); 265 return dao.read(targetId, theRequest); 266 }); 267 } 268 269 /** 270 * @param theIdToAssignToPlaceholder If specified, the placeholder resource created will be given a specific ID 271 */ 272 public <T extends IBaseResource> Optional<IBasePersistedResource> createPlaceholderTargetIfConfiguredToDoSo( 273 Class<T> theType, 274 IBaseReference theReference, 275 @Nullable String theIdToAssignToPlaceholder, 276 RequestDetails theRequest, 277 TransactionDetails theTransactionDetails) { 278 IBasePersistedResource valueOf = null; 279 280 if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) { 281 RuntimeResourceDefinition missingResourceDef = myContext.getResourceDefinition(theType); 282 String resName = missingResourceDef.getName(); 283 284 @SuppressWarnings("unchecked") 285 T newResource = (T) missingResourceDef.newInstance(); 286 287 tryToAddPlaceholderExtensionToResource(newResource); 288 289 IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType); 290 ourLog.debug( 291 "Automatically creating empty placeholder resource: {}", 292 newResource.getIdElement().getValue()); 293 294 if (myStorageSettings.isPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets()) { 295 tryToCopyIdentifierFromReferenceToTargetResource(theReference, missingResourceDef, newResource); 296 } 297 298 if (theIdToAssignToPlaceholder != null) { 299 if (theTransactionDetails != null) { 300 String existingId = newResource.getIdElement().getValue(); 301 theTransactionDetails.addRollbackUndoAction(() -> newResource.setId(existingId)); 302 } 303 newResource.setId(resName + "/" + theIdToAssignToPlaceholder); 304 } 305 306 // Interceptor: STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE 307 IInterceptorBroadcaster interceptorBroadcaster = 308 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 309 if (interceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE)) { 310 AutoCreatePlaceholderReferenceTargetRequest request = 311 new AutoCreatePlaceholderReferenceTargetRequest(newResource); 312 HookParams params = new HookParams() 313 .add(AutoCreatePlaceholderReferenceTargetRequest.class, request) 314 .add(RequestDetails.class, theRequest) 315 .addIfMatchesType(ServletRequestDetails.class, theRequest); 316 AutoCreatePlaceholderReferenceTargetResponse response = 317 (AutoCreatePlaceholderReferenceTargetResponse) interceptorBroadcaster.callHooksAndReturnObject( 318 Pointcut.STORAGE_PRE_AUTO_CREATE_PLACEHOLDER_REFERENCE, params); 319 if (response != null) { 320 if (response.isDoNotCreateTarget()) { 321 return Optional.empty(); 322 } 323 } 324 325 // Sanity check: Make sure that interceptors haven't changed the ID 326 if (theIdToAssignToPlaceholder != null) { 327 Validate.isTrue( 328 theIdToAssignToPlaceholder.equals( 329 newResource.getIdElement().getIdPart()), 330 "Interceptors must not modify the ID of auto-created placeholder reference targets"); 331 } else { 332 Validate.isTrue( 333 isBlank(newResource.getIdElement().getIdPart()), 334 "Interceptors must not modify the ID of auto-created placeholder reference targets"); 335 } 336 } 337 338 if (theIdToAssignToPlaceholder != null) { 339 valueOf = placeholderResourceDao 340 .update(newResource, null, true, false, theRequest, theTransactionDetails) 341 .getEntity(); 342 } else { 343 valueOf = placeholderResourceDao.create(newResource, theRequest).getEntity(); 344 } 345 346 IResourcePersistentId persistentId = valueOf.getPersistentId(); 347 persistentId = myIdHelperService.newPid(persistentId.getId()); 348 persistentId.setAssociatedResourceId(valueOf.getIdDt()); 349 theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId); 350 theTransactionDetails.addAutoCreatedPlaceholderResource(newResource.getIdElement()); 351 } 352 353 return Optional.ofNullable(valueOf); 354 } 355 356 private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) { 357 if (newResource instanceof IBaseHasExtensions) { 358 IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension(); 359 extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER); 360 extension.setValue(myContext.newPrimitiveBoolean(true)); 361 } 362 } 363 364 private <T extends IBaseResource> void tryToCopyIdentifierFromReferenceToTargetResource( 365 IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) { 366 // boolean referenceHasIdentifier = theSourceReference.hasIdentifier(); 367 CanonicalIdentifier referenceMatchUrlIdentifier = extractIdentifierFromUrl( 368 theSourceReference.getReferenceElement().getValue()); 369 CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference); 370 371 if (referenceIdentifier == null && referenceMatchUrlIdentifier != null) { 372 addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier); 373 } else if (referenceIdentifier != null && referenceMatchUrlIdentifier == null) { 374 addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource); 375 } else if (referenceIdentifier != null && referenceMatchUrlIdentifier != null) { 376 if (referenceIdentifier.equals(referenceMatchUrlIdentifier)) { 377 addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource); 378 } else { 379 addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource); 380 addMatchUrlIdentifierToTargetResource( 381 theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier); 382 } 383 } 384 } 385 386 private <T extends IBaseResource> void addSubjectIdentifierToTargetResource( 387 IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) { 388 BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier"); 389 if (targetIdentifier != null) { 390 BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier"); 391 String identifierElementName = identifierElement.getName(); 392 boolean targetHasIdentifierElement = identifierElementName.equals("Identifier"); 393 if (targetHasIdentifierElement) { 394 395 BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>) 396 myContext.getElementDefinition(theSourceReference.getClass()); 397 BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier"); 398 Optional<IBase> identifierOpt = 399 referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference); 400 identifierOpt.ifPresent( 401 theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase)); 402 } 403 } 404 } 405 406 private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource( 407 RuntimeResourceDefinition theTargetResourceDef, 408 T theTargetResource, 409 CanonicalIdentifier referenceMatchUrlIdentifier) { 410 BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier"); 411 IBase identifierIBase = identifierDefinition 412 .getChildByName("identifier") 413 .newInstance(identifierDefinition.getInstanceConstructorArguments()); 414 IBase systemIBase = TerserUtil.newElement( 415 myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString()); 416 IBase valueIBase = TerserUtil.newElement( 417 myContext, 418 "string", 419 referenceMatchUrlIdentifier.getValueElement().getValueAsString()); 420 // Set system in the IBase Identifier 421 422 BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass()); 423 424 BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system"); 425 systemDefinition.getMutator().setValue(identifierIBase, systemIBase); 426 427 BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value"); 428 valueDefinition.getMutator().setValue(identifierIBase, valueIBase); 429 430 // Set Value in the IBase identifier 431 identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase); 432 } 433 434 private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) { 435 Optional<IBase> identifier = 436 myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class); 437 if (!identifier.isPresent()) { 438 return null; 439 } else { 440 CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier(); 441 Optional<IPrimitiveType> system = 442 myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class); 443 Optional<IPrimitiveType> value = 444 myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class); 445 446 system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString())); 447 value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString())); 448 return canonicalIdentifier; 449 } 450 } 451 452 /** 453 * Extracts the first available identifier from the URL part 454 * 455 * @param theValue Part of the URL to extract identifiers from 456 * @return Returns the first available identifier in the canonical form or null if URL contains no identifier param 457 * @throws IllegalArgumentException IllegalArgumentException is thrown in case identifier parameter can not be split using <code>system|value</code> pattern. 458 */ 459 protected CanonicalIdentifier extractIdentifierFromUrl(String theValue) { 460 int identifierIndex = theValue.indexOf("identifier="); 461 if (identifierIndex == -1) { 462 return null; 463 } 464 465 List<NameValuePair> params = 466 URLEncodedUtils.parse(theValue.substring(identifierIndex), StandardCharsets.UTF_8, '&', ';'); 467 Optional<NameValuePair> idOptional = 468 params.stream().filter(p -> p.getName().equals("identifier")).findFirst(); 469 if (!idOptional.isPresent()) { 470 return null; 471 } 472 473 NameValuePair id = idOptional.get(); 474 String identifierString = id.getValue(); 475 String[] split = identifierString.split("\\|"); 476 if (split.length != 2) { 477 throw new IllegalArgumentException(Msg.code(1097) + "Can't create a placeholder reference with identifier " 478 + theValue + ". It is not a valid identifier"); 479 } 480 481 CanonicalIdentifier identifier = new CanonicalIdentifier(); 482 identifier.setSystem(split[0]); 483 identifier.setValue(split[1]); 484 return identifier; 485 } 486 487 @Override 488 public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) { 489 myDaoRegistry.getDaoOrThrow(theType); 490 } 491 492 private static class ResourceLookupPersistentIdWrapper<P extends IResourcePersistentId> implements IResourceLookup { 493 private final P myPersistentId; 494 495 public ResourceLookupPersistentIdWrapper(P thePersistentId) { 496 myPersistentId = thePersistentId; 497 } 498 499 @Override 500 public String getResourceType() { 501 return myPersistentId.getAssociatedResourceId().getResourceType(); 502 } 503 504 @Override 505 public String getFhirId() { 506 return myPersistentId.getAssociatedResourceId().getIdPart(); 507 } 508 509 @Override 510 public Date getDeleted() { 511 return null; 512 } 513 514 @Override 515 public P getPersistentId() { 516 return myPersistentId; 517 } 518 519 @Override 520 public PartitionablePartitionId getPartitionId() { 521 return new PartitionablePartitionId(myPersistentId.getPartitionId(), null); 522 } 523 } 524}