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