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