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