
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; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.interceptor.model.RequestPartitionId; 029import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 030import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 031import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 032import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; 033import ca.uhn.fhir.jpa.cache.IResourceVersionSvc; 034import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap; 035import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 036import ca.uhn.fhir.jpa.model.entity.ResourceTable; 037import ca.uhn.fhir.jpa.model.entity.StorageSettings; 038import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 039import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil; 040import ca.uhn.fhir.model.api.IQueryParameterAnd; 041import ca.uhn.fhir.model.api.StorageResponseCodeEnum; 042import ca.uhn.fhir.rest.api.QualifiedParamList; 043import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 044import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 045import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 046import ca.uhn.fhir.rest.api.server.RequestDetails; 047import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 048import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 049import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 050import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 051import ca.uhn.fhir.rest.param.QualifierDetails; 052import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 053import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 054import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 055import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 056import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 057import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 058import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 059import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 060import ca.uhn.fhir.util.BundleUtil; 061import ca.uhn.fhir.util.FhirTerser; 062import ca.uhn.fhir.util.HapiExtensions; 063import ca.uhn.fhir.util.IMetaTagSorter; 064import ca.uhn.fhir.util.MetaUtil; 065import ca.uhn.fhir.util.OperationOutcomeUtil; 066import ca.uhn.fhir.util.ResourceReferenceInfo; 067import ca.uhn.fhir.util.StopWatch; 068import ca.uhn.fhir.util.UrlUtil; 069import com.google.common.annotations.VisibleForTesting; 070import jakarta.annotation.Nonnull; 071import jakarta.annotation.Nullable; 072import org.hl7.fhir.instance.model.api.IBase; 073import org.hl7.fhir.instance.model.api.IBaseBundle; 074import org.hl7.fhir.instance.model.api.IBaseExtension; 075import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 076import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 077import org.hl7.fhir.instance.model.api.IBaseReference; 078import org.hl7.fhir.instance.model.api.IBaseResource; 079import org.hl7.fhir.instance.model.api.IIdType; 080import org.hl7.fhir.r4.model.InstantType; 081import org.slf4j.Logger; 082import org.slf4j.LoggerFactory; 083import org.springframework.beans.factory.annotation.Autowired; 084import org.springframework.transaction.annotation.Propagation; 085import org.springframework.transaction.annotation.Transactional; 086 087import java.util.Collection; 088import java.util.Collections; 089import java.util.IdentityHashMap; 090import java.util.List; 091import java.util.Map; 092import java.util.Set; 093import java.util.function.Supplier; 094import java.util.stream.Collectors; 095import java.util.stream.Stream; 096 097import static org.apache.commons.lang3.StringUtils.defaultString; 098import static org.apache.commons.lang3.StringUtils.isNotBlank; 099 100public abstract class BaseStorageDao { 101 private static final Logger ourLog = LoggerFactory.getLogger(BaseStorageDao.class); 102 103 /** @deprecated moved to {@link OperationOutcomeUtil#OO_SEVERITY_ERROR} */ 104 @Deprecated(forRemoval = true, since = "8.4.0") 105 public static final String OO_SEVERITY_ERROR = OperationOutcomeUtil.OO_SEVERITY_ERROR; 106 /** @deprecated moved to {@link OperationOutcomeUtil#OO_SEVERITY_INFO} */ 107 @Deprecated(forRemoval = true, since = "8.4.0") 108 public static final String OO_SEVERITY_INFO = OperationOutcomeUtil.OO_SEVERITY_INFO; 109 /** @deprecated moved to {@link OperationOutcomeUtil#OO_SEVERITY_WARN} */ 110 @Deprecated(forRemoval = true, since = "8.4.0") 111 public static final String OO_SEVERITY_WARN = OperationOutcomeUtil.OO_SEVERITY_WARN; 112 113 private static final String PROCESSING_SUB_REQUEST = "BaseStorageDao.processingSubRequest"; 114 115 protected static final String MESSAGE_KEY_DELETE_RESOURCE_NOT_EXISTING = "deleteResourceNotExisting"; 116 protected static final String MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED = "deleteResourceAlreadyDeleted"; 117 public static final String OO_ISSUE_CODE_INFORMATIONAL = "informational"; 118 119 @Autowired 120 protected ISearchParamRegistry mySearchParamRegistry; 121 122 @Autowired 123 protected FhirContext myFhirContext; 124 125 @Autowired 126 protected DaoRegistry myDaoRegistry; 127 128 @Autowired 129 protected IResourceVersionSvc myResourceVersionSvc; 130 131 @Autowired 132 protected JpaStorageSettings myStorageSettings; 133 134 @Autowired 135 protected IMetaTagSorter myMetaTagSorter; 136 137 @VisibleForTesting 138 public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { 139 mySearchParamRegistry = theSearchParamRegistry; 140 } 141 142 @VisibleForTesting 143 public void setMyMetaTagSorter(IMetaTagSorter theMetaTagSorter) { 144 myMetaTagSorter = theMetaTagSorter; 145 } 146 147 /** 148 * May be overridden by subclasses to validate resources prior to storage 149 * 150 * @param theResource The resource that is about to be stored 151 * @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead 152 */ 153 protected void preProcessResourceForStorage(IBaseResource theResource) { 154 // nothing 155 } 156 157 /** 158 * May be overridden by subclasses to validate resources prior to storage 159 * 160 * @param theResource The resource that is about to be stored 161 * @since 5.3.0 162 */ 163 protected void preProcessResourceForStorage( 164 IBaseResource theResource, 165 RequestDetails theRequestDetails, 166 TransactionDetails theTransactionDetails, 167 boolean thePerformIndexing) { 168 169 verifyResourceTypeIsAppropriateForDao(theResource); 170 171 verifyResourceIdIsValid(theResource); 172 173 verifyBundleTypeIsAppropriateForStorage(theResource); 174 175 if (!getStorageSettings().getTreatBaseUrlsAsLocal().isEmpty()) { 176 replaceAbsoluteReferencesWithRelative(theResource, myFhirContext.newTerser()); 177 } 178 179 performAutoVersioning(theResource, thePerformIndexing); 180 181 myMetaTagSorter.sort(theResource.getMeta()); 182 } 183 184 /** 185 * Sanity check - Is this resource the right type for this DAO? 186 */ 187 private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) { 188 String type = getContext().getResourceType(theResource); 189 if (getResourceName() != null && !getResourceName().equals(type)) { 190 throw new InvalidRequestException(Msg.code(520) 191 + getContext() 192 .getLocalizer() 193 .getMessageSanitized( 194 BaseStorageDao.class, "incorrectResourceType", type, getResourceName())); 195 } 196 } 197 198 /** 199 * Verify that the resource ID is actually valid according to FHIR's rules 200 */ 201 private void verifyResourceIdIsValid(IBaseResource theResource) { 202 if (theResource.getIdElement().hasResourceType()) { 203 String expectedType = getContext().getResourceType(theResource); 204 if (!expectedType.equals(theResource.getIdElement().getResourceType())) { 205 throw new InvalidRequestException(Msg.code(2616) 206 + getContext() 207 .getLocalizer() 208 .getMessageSanitized( 209 BaseStorageDao.class, 210 "failedToCreateWithInvalidIdWrongResourceType", 211 theResource.getIdElement().toUnqualifiedVersionless())); 212 } 213 } 214 215 if (theResource.getIdElement().hasIdPart()) { 216 if (!theResource.getIdElement().isIdPartValid()) { 217 throw new InvalidRequestException(Msg.code(521) 218 + getContext() 219 .getLocalizer() 220 .getMessageSanitized( 221 BaseStorageDao.class, 222 "failedToCreateWithInvalidId", 223 theResource.getIdElement().getIdPart())); 224 } 225 } 226 } 227 228 /** 229 * Verify that we're not storing a Bundle with a disallowed bundle type 230 */ 231 private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) { 232 if (theResource instanceof IBaseBundle) { 233 Set<String> allowedBundleTypes = getStorageSettings().getBundleTypesAllowedForStorage(); 234 String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource); 235 bundleType = defaultString(bundleType); 236 if (!allowedBundleTypes.contains(bundleType)) { 237 String message = myFhirContext 238 .getLocalizer() 239 .getMessage( 240 BaseStorageDao.class, 241 "invalidBundleTypeForStorage", 242 (isNotBlank(bundleType) ? bundleType : "(missing)")); 243 throw new UnprocessableEntityException(Msg.code(522) + message); 244 } 245 } 246 } 247 248 /** 249 * Replace absolute references with relative ones if configured to do so 250 */ 251 private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource, FhirTerser theTerser) { 252 List<ResourceReferenceInfo> refs = theTerser.getAllResourceReferences(theResource); 253 for (ResourceReferenceInfo nextRef : refs) { 254 IIdType refId = nextRef.getResourceReference().getReferenceElement(); 255 if (refId != null && refId.hasBaseUrl()) { 256 if (getStorageSettings().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) { 257 IIdType newRefId = refId.toUnqualified(); 258 nextRef.getResourceReference().setReference(newRefId.getValue()); 259 } 260 } 261 } 262 } 263 264 /** 265 * Handle {@link JpaStorageSettings#getAutoVersionReferenceAtPaths() auto-populate-versions} 266 * <p> 267 * We only do this if thePerformIndexing is true because if it's false, that means 268 * we're in a FHIR transaction during the first phase of write operation processing, 269 * meaning that the versions of other resources may not have need updatd yet. For example 270 * we're about to store an Observation with a reference to a Patient, and that Patient 271 * is also being updated in the same transaction, during the first "no index" phase, 272 * the Patient will not yet have its version number incremented, so it would be wrong 273 * to use that value. During the second phase it is correct. 274 * <p> 275 * Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning 276 * and it is the one that takes care of the placeholder IDs. Look for the other caller of 277 * {@link #extractReferencesToAutoVersion(FhirContext, StorageSettings, IBaseResource)} 278 * to find this. 279 */ 280 private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) { 281 if (thePerformIndexing) { 282 Set<IBaseReference> referencesToVersion = 283 extractReferencesToAutoVersion(myFhirContext, myStorageSettings, theResource); 284 for (IBaseReference nextReference : referencesToVersion) { 285 IIdType referenceElement = nextReference.getReferenceElement(); 286 if (!referenceElement.hasBaseUrl()) { 287 288 ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds( 289 RequestPartitionId.allPartitions(), Collections.singletonList(referenceElement)); 290 291 // 3 cases: 292 // 1) there exists a resource in the db with some version (use this version) 293 // 2) no resource exists, but we will create one (eventually). The version is 1 294 // 3) no resource exists, and none will be made -> throw 295 Long version; 296 if (resourceVersionMap.containsKey(referenceElement)) { 297 // the resource exists... latest id 298 // will be the value in the IResourcePersistentId 299 version = resourceVersionMap 300 .getResourcePersistentId(referenceElement) 301 .getVersion(); 302 } else if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) { 303 // if idToPID doesn't contain object 304 // but autcreateplaceholders is on 305 // then the version will be 1 (the first version) 306 version = 1L; 307 } else { 308 // resource not found 309 // and no autocreateplaceholders set... 310 // we throw 311 throw new ResourceNotFoundException(Msg.code(523) + referenceElement); 312 } 313 String newTargetReference = 314 referenceElement.withVersion(version.toString()).getValue(); 315 nextReference.setReference(newTargetReference); 316 } 317 } 318 } 319 } 320 321 protected DaoMethodOutcome toMethodOutcome( 322 RequestDetails theRequest, 323 @Nonnull final IBasePersistedResource theEntity, 324 @Nonnull IBaseResource theResource, 325 @Nullable String theMatchUrl, 326 @Nonnull RestOperationTypeEnum theOperationType) { 327 DaoMethodOutcome outcome = new DaoMethodOutcome(); 328 329 IResourcePersistentId persistentId = theEntity.getPersistentId(); 330 persistentId.setAssociatedResourceId(theResource.getIdElement()); 331 332 outcome.setPersistentId(persistentId); 333 outcome.setMatchUrl(theMatchUrl); 334 outcome.setOperationType(theOperationType); 335 336 if (theEntity instanceof ResourceTable) { 337 if (((ResourceTable) theEntity).isUnchangedInCurrentOperation()) { 338 outcome.setNop(true); 339 } 340 } 341 342 IIdType id = null; 343 if (theResource.getIdElement().getValue() != null) { 344 id = theResource.getIdElement(); 345 } 346 if (id == null) { 347 id = theEntity.getIdDt(); 348 if (getContext().getVersion().getVersion().isRi()) { 349 id = getContext().getVersion().newIdType().setValue(id.getValue()); 350 } 351 } 352 353 outcome.setId(id); 354 if (theEntity.getDeleted() == null) { 355 outcome.setResource(theResource); 356 } 357 outcome.setEntity(theEntity); 358 359 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 360 if (outcome.getResource() != null) { 361 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource()); 362 IInterceptorBroadcaster compositeBroadcaster = 363 CompositeInterceptorBroadcaster.newCompositeBroadcaster(getInterceptorBroadcaster(), theRequest); 364 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 365 HookParams params = new HookParams() 366 .add(IPreResourceAccessDetails.class, accessDetails) 367 .add(RequestDetails.class, theRequest) 368 .addIfMatchesType(ServletRequestDetails.class, theRequest); 369 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 370 if (accessDetails.isDontReturnResourceAtIndex(0)) { 371 outcome.setResource(null); 372 } 373 } 374 } 375 376 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 377 // Note that this will only fire if someone actually goes to use the 378 // resource in a response (it's their responsibility to call 379 // outcome.fireResourceViewCallback()) 380 outcome.registerResourceViewCallback(() -> { 381 if (outcome.getResource() != null) { 382 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 383 getInterceptorBroadcaster(), theRequest); 384 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 385 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 386 HookParams params = new HookParams() 387 .add(IPreResourceShowDetails.class, showDetails) 388 .add(RequestDetails.class, theRequest) 389 .addIfMatchesType(ServletRequestDetails.class, theRequest); 390 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 391 outcome.setResource(showDetails.getResource(0)); 392 } 393 } 394 }); 395 396 return outcome; 397 } 398 399 protected DaoMethodOutcome toMethodOutcomeLazy( 400 RequestDetails theRequest, 401 IResourcePersistentId theResourcePersistentId, 402 @Nonnull final Supplier<LazyDaoMethodOutcome.EntityAndResource> theEntity, 403 Supplier<IIdType> theIdSupplier) { 404 LazyDaoMethodOutcome outcome = new LazyDaoMethodOutcome(theResourcePersistentId); 405 406 outcome.setEntitySupplier(theEntity); 407 outcome.setIdSupplier(theIdSupplier); 408 outcome.setEntitySupplierUseCallback(() -> { 409 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 410 if (outcome.getResource() != null) { 411 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 412 getInterceptorBroadcaster(), theRequest); 413 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 414 SimplePreResourceAccessDetails accessDetails = 415 new SimplePreResourceAccessDetails(outcome.getResource()); 416 HookParams params = new HookParams() 417 .add(IPreResourceAccessDetails.class, accessDetails) 418 .add(RequestDetails.class, theRequest) 419 .addIfMatchesType(ServletRequestDetails.class, theRequest); 420 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 421 if (accessDetails.isDontReturnResourceAtIndex(0)) { 422 outcome.setResource(null); 423 } 424 } 425 } 426 427 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 428 // Note that this will only fire if someone actually goes to use the 429 // resource in a response (it's their responsibility to call 430 // outcome.fireResourceViewCallback()) 431 outcome.registerResourceViewCallback(() -> { 432 if (outcome.getResource() != null) { 433 IInterceptorBroadcaster compositeBroadcaster = 434 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 435 getInterceptorBroadcaster(), theRequest); 436 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 437 SimplePreResourceShowDetails showDetails = 438 new SimplePreResourceShowDetails(outcome.getResource()); 439 HookParams params = new HookParams() 440 .add(IPreResourceShowDetails.class, showDetails) 441 .add(RequestDetails.class, theRequest) 442 .addIfMatchesType(ServletRequestDetails.class, theRequest); 443 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 444 outcome.setResource(showDetails.getResource(0)); 445 } 446 } 447 }); 448 }); 449 450 return outcome; 451 } 452 453 protected void doCallHooks( 454 TransactionDetails theTransactionDetails, 455 RequestDetails theRequestDetails, 456 Pointcut thePointcut, 457 HookParams theParams) { 458 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) { 459 theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams); 460 } else { 461 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 462 getInterceptorBroadcaster(), theRequestDetails); 463 compositeBroadcaster.callHooks(thePointcut, theParams); 464 } 465 } 466 467 protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); 468 469 public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) { 470 return createOperationOutcome(OperationOutcomeUtil.OO_SEVERITY_ERROR, theMessage, theCode); 471 } 472 473 public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) { 474 return createInfoOperationOutcome(theMessage, null); 475 } 476 477 public IBaseOperationOutcome createInfoOperationOutcome( 478 String theMessage, @Nullable StorageResponseCodeEnum theStorageResponseCode) { 479 return OperationOutcomeUtil.createOperationOutcome( 480 OperationOutcomeUtil.OO_SEVERITY_INFO, 481 theMessage, 482 OperationOutcomeUtil.OO_ISSUE_CODE_INFORMATIONAL, 483 getContext(), 484 theStorageResponseCode); 485 } 486 487 private IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) { 488 return OperationOutcomeUtil.createOperationOutcome(theSeverity, theMessage, theCode, getContext(), null); 489 } 490 491 @Nonnull 492 public IBaseOperationOutcome createWarnOperationOutcome( 493 String theMsg, String theCode, StorageResponseCodeEnum theResponseCodeEnum) { 494 return OperationOutcomeUtil.createOperationOutcome( 495 OperationOutcomeUtil.OO_SEVERITY_WARN, theMsg, theCode, getContext(), theResponseCodeEnum); 496 } 497 498 /** 499 * Creates a base method outcome for a delete request for the provided ID. 500 * <p> 501 * Additional information may be set on the outcome. 502 * 503 * @param theResourceId - the id of the object being deleted. Eg: Patient/123 504 */ 505 protected DaoMethodOutcome createMethodOutcomeForResourceId( 506 String theResourceId, String theMessageKey, StorageResponseCodeEnum theStorageResponseCode) { 507 DaoMethodOutcome outcome = new DaoMethodOutcome(); 508 509 IIdType id = getContext().getVersion().newIdType(); 510 id.setValue(theResourceId); 511 outcome.setId(id); 512 513 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, theMessageKey, id); 514 String severity = "information"; 515 String code = "informational"; 516 IBaseOperationOutcome oo = OperationOutcomeUtil.createOperationOutcome( 517 severity, message, code, getContext(), theStorageResponseCode); 518 outcome.setOperationOutcome(oo); 519 520 return outcome; 521 } 522 523 @Nonnull 524 protected ResourceGoneException createResourceGoneException(IBasePersistedResource theResourceEntity) { 525 StringBuilder b = new StringBuilder(); 526 b.append("Resource was deleted at "); 527 b.append(new InstantType(theResourceEntity.getDeleted()).getValueAsString()); 528 ResourceGoneException retVal = new ResourceGoneException(b.toString()); 529 retVal.setResourceId(theResourceEntity.getIdDt()); 530 return retVal; 531 } 532 533 /** 534 * Provide the JpaStorageSettings 535 */ 536 protected abstract JpaStorageSettings getStorageSettings(); 537 538 /** 539 * Returns the resource type for this DAO, or null if this is a system-level DAO 540 */ 541 @Nullable 542 protected abstract String getResourceName(); 543 544 /** 545 * Provides the FHIR context 546 */ 547 protected abstract FhirContext getContext(); 548 549 @Transactional(propagation = Propagation.SUPPORTS) 550 public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) { 551 if (theSource == null || theSource.isEmpty()) { 552 return; 553 } 554 555 ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams( 556 getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 557 558 Set<String> paramNames = theSource.keySet(); 559 for (String nextParamName : paramNames) { 560 QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName); 561 RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName()); 562 if (param == null) { 563 Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 564 getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 565 RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam( 566 getResourceName(), 567 qualifiedParamName.getParamName(), 568 ISearchParamRegistry.SearchParamLookupContextEnum.ALL); 569 if (notEnabledForSearchParam != null) { 570 String msg = getContext() 571 .getLocalizer() 572 .getMessageSanitized( 573 BaseStorageDao.class, 574 "invalidSearchParameterNotEnabledForSearch", 575 qualifiedParamName.getParamName(), 576 getResourceName(), 577 validNames); 578 throw new InvalidRequestException(Msg.code(2539) + msg); 579 } else { 580 String msg = getContext() 581 .getLocalizer() 582 .getMessageSanitized( 583 BaseStorageDao.class, 584 "invalidSearchParameter", 585 qualifiedParamName.getParamName(), 586 getResourceName(), 587 validNames); 588 throw new InvalidRequestException(Msg.code(524) + msg); 589 } 590 } 591 592 // Should not be null since the check above would have caught it 593 RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam( 594 getResourceName(), 595 qualifiedParamName.getParamName(), 596 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 597 598 for (String nextValue : theSource.get(nextParamName)) { 599 QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape( 600 qualifiedParamName.getWholeQualifier(), nextValue); 601 List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam); 602 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 603 mySearchParamRegistry, getContext(), paramDef, nextParamName, paramList); 604 theTarget.add(qualifiedParamName.getParamName(), parsedParam); 605 } 606 } 607 } 608 609 protected void populateOperationOutcomeForUpdate( 610 @Nullable StopWatch theItemStopwatch, 611 DaoMethodOutcome theMethodOutcome, 612 String theMatchUrl, 613 RestOperationTypeEnum theOperationType, 614 TransactionDetails theTransactionDetails) { 615 String msg; 616 StorageResponseCodeEnum outcome; 617 618 if (theOperationType == RestOperationTypeEnum.PATCH) { 619 620 if (theMatchUrl != null) { 621 if (theMethodOutcome.isNop()) { 622 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH_NO_CHANGE; 623 msg = getContext() 624 .getLocalizer() 625 .getMessageSanitized( 626 BaseStorageDao.class, 627 "successfulPatchConditionalNoChange", 628 theMethodOutcome.getId(), 629 UrlUtil.sanitizeUrlPart(theMatchUrl), 630 theMethodOutcome.getId()); 631 } else { 632 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH; 633 msg = getContext() 634 .getLocalizer() 635 .getMessageSanitized( 636 BaseStorageDao.class, 637 "successfulPatchConditional", 638 theMethodOutcome.getId(), 639 UrlUtil.sanitizeUrlPart(theMatchUrl), 640 theMethodOutcome.getId()); 641 } 642 } else { 643 if (theMethodOutcome.isNop()) { 644 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH_NO_CHANGE; 645 msg = getContext() 646 .getLocalizer() 647 .getMessageSanitized( 648 BaseStorageDao.class, "successfulPatchNoChange", theMethodOutcome.getId()); 649 } else { 650 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH; 651 msg = getContext() 652 .getLocalizer() 653 .getMessageSanitized(BaseStorageDao.class, "successfulPatch", theMethodOutcome.getId()); 654 } 655 } 656 657 } else if (theOperationType == RestOperationTypeEnum.CREATE) { 658 659 if (theMatchUrl == null) { 660 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE; 661 msg = getContext() 662 .getLocalizer() 663 .getMessageSanitized(BaseStorageDao.class, "successfulCreate", theMethodOutcome.getId()); 664 } else if (theMethodOutcome.isNop()) { 665 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH; 666 msg = getContext() 667 .getLocalizer() 668 .getMessageSanitized( 669 BaseStorageDao.class, 670 "successfulCreateConditionalWithMatch", 671 theMethodOutcome.getId(), 672 UrlUtil.sanitizeUrlPart(theMatchUrl)); 673 } else { 674 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_NO_CONDITIONAL_MATCH; 675 msg = getContext() 676 .getLocalizer() 677 .getMessageSanitized( 678 BaseStorageDao.class, 679 "successfulCreateConditionalNoMatch", 680 theMethodOutcome.getId(), 681 UrlUtil.sanitizeUrlPart(theMatchUrl)); 682 } 683 684 } else if (theMethodOutcome.isNop()) { 685 686 if (theMatchUrl != null) { 687 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH_NO_CHANGE; 688 msg = getContext() 689 .getLocalizer() 690 .getMessageSanitized( 691 BaseStorageDao.class, 692 "successfulUpdateConditionalNoChangeWithMatch", 693 theMethodOutcome.getId(), 694 theMatchUrl); 695 } else { 696 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CHANGE; 697 msg = getContext() 698 .getLocalizer() 699 .getMessageSanitized( 700 BaseStorageDao.class, "successfulUpdateNoChange", theMethodOutcome.getId()); 701 } 702 703 } else { 704 705 if (theMatchUrl != null) { 706 if (theMethodOutcome.getCreated() == Boolean.TRUE) { 707 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CONDITIONAL_MATCH; 708 msg = getContext() 709 .getLocalizer() 710 .getMessageSanitized( 711 BaseStorageDao.class, 712 "successfulUpdateConditionalNoMatch", 713 theMethodOutcome.getId()); 714 } else { 715 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH; 716 msg = getContext() 717 .getLocalizer() 718 .getMessageSanitized( 719 BaseStorageDao.class, 720 "successfulUpdateConditionalWithMatch", 721 theMethodOutcome.getId(), 722 theMatchUrl); 723 } 724 } else if (theMethodOutcome.getCreated() == Boolean.TRUE) { 725 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_AS_CREATE; 726 msg = getContext() 727 .getLocalizer() 728 .getMessageSanitized( 729 BaseStorageDao.class, "successfulUpdateAsCreate", theMethodOutcome.getId()); 730 } else { 731 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE; 732 msg = getContext() 733 .getLocalizer() 734 .getMessageSanitized(BaseStorageDao.class, "successfulUpdate", theMethodOutcome.getId()); 735 } 736 } 737 738 if (theItemStopwatch != null) { 739 String msgSuffix = getContext() 740 .getLocalizer() 741 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", theItemStopwatch.getMillis()); 742 msg = msg + " " + msgSuffix; 743 } 744 745 IBaseOperationOutcome oo = createInfoOperationOutcome(msg, outcome); 746 747 if (theTransactionDetails != null) { 748 List<IIdType> autoCreatedPlaceholderResources = 749 theTransactionDetails.getAutoCreatedPlaceholderResourcesAndClear(); 750 for (IIdType next : autoCreatedPlaceholderResources) { 751 msg = addIssueToOperationOutcomeForAutoCreatedPlaceholder(getContext(), next, oo); 752 } 753 } 754 755 theMethodOutcome.setOperationOutcome(oo); 756 ourLog.debug(msg); 757 } 758 759 public static String addIssueToOperationOutcomeForAutoCreatedPlaceholder( 760 FhirContext theFhirContext, IIdType thePlaceholderId, IBaseOperationOutcome theOperationOutcomeToPopulate) { 761 String msg; 762 msg = theFhirContext 763 .getLocalizer() 764 .getMessageSanitized(BaseStorageDao.class, "successfulAutoCreatePlaceholder", thePlaceholderId); 765 String detailSystem = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getSystem(); 766 String detailCode = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getCode(); 767 String detailDescription = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getDisplay(); 768 IBase issue = OperationOutcomeUtil.addIssue( 769 theFhirContext, 770 theOperationOutcomeToPopulate, 771 OperationOutcomeUtil.OO_SEVERITY_INFO, 772 msg, 773 null, 774 OperationOutcomeUtil.OO_ISSUE_CODE_INFORMATIONAL, 775 detailSystem, 776 detailCode, 777 detailDescription); 778 if (issue instanceof IBaseHasExtensions) { 779 IBaseExtension<?, ?> resourceIdExtension = ((IBaseHasExtensions) issue).addExtension(); 780 resourceIdExtension.setUrl(HapiExtensions.EXTENSION_PLACEHOLDER_ID); 781 resourceIdExtension.setValue(theFhirContext.getVersion().newIdType(thePlaceholderId.getValue())); 782 } 783 784 return msg; 785 } 786 787 /** 788 * Extracts a list of references that have versions in their ID whose versions should not be stripped 789 * 790 * @return A set of references that should not have their client-given versions stripped according to the 791 * versioned references settings. 792 */ 793 public static Set<IBaseReference> extractReferencesToAvoidReplacement( 794 FhirContext theFhirContext, IBaseResource theResource) { 795 if (!theFhirContext 796 .getParserOptions() 797 .getDontStripVersionsFromReferencesAtPaths() 798 .isEmpty()) { 799 String theResourceType = theFhirContext.getResourceType(theResource); 800 Set<String> versionReferencesPaths = theFhirContext 801 .getParserOptions() 802 .getDontStripVersionsFromReferencesAtPathsByResourceType(theResourceType); 803 return getReferencesWithOrWithoutVersionId(versionReferencesPaths, theFhirContext, theResource, false); 804 } 805 return Collections.emptySet(); 806 } 807 808 /** 809 * Extracts a list of references that should be auto-versioned. 810 * 811 * @return A set of references that should be versioned according to both storage settings 812 * and auto-version reference extensions, or it may also be empty. 813 */ 814 @Nonnull 815 public static Set<IBaseReference> extractReferencesToAutoVersion( 816 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 817 Set<IBaseReference> referencesToAutoVersionFromConfig = 818 getReferencesToAutoVersionFromConfig(theFhirContext, theStorageSettings, theResource); 819 820 Set<IBaseReference> referencesToAutoVersionFromExtensions = 821 getReferencesToAutoVersionFromExtension(theFhirContext, theResource); 822 823 return Stream.concat(referencesToAutoVersionFromConfig.stream(), referencesToAutoVersionFromExtensions.stream()) 824 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 825 .keySet(); 826 } 827 828 /** 829 * Extracts a list of references that should be auto-versioned according to 830 * <code>auto-version-references-at-path</code> extensions. 831 * @see HapiExtensions#EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH 832 */ 833 @Nonnull 834 private static Set<IBaseReference> getReferencesToAutoVersionFromExtension( 835 FhirContext theFhirContext, IBaseResource theResource) { 836 String resourceType = theFhirContext.getResourceType(theResource); 837 Set<String> autoVersionReferencesAtPaths = 838 MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType); 839 840 if (!autoVersionReferencesAtPaths.isEmpty()) { 841 return getReferencesWithOrWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource, true); 842 } 843 return Collections.emptySet(); 844 } 845 846 /** 847 * Extracts a list of references that should be auto-versioned according to storage configuration. 848 * @see StorageSettings#getAutoVersionReferenceAtPaths() 849 */ 850 @Nonnull 851 private static Set<IBaseReference> getReferencesToAutoVersionFromConfig( 852 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 853 if (!theStorageSettings.getAutoVersionReferenceAtPaths().isEmpty()) { 854 String resourceName = theFhirContext.getResourceType(theResource); 855 Set<String> autoVersionReferencesPaths = 856 theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName); 857 return getReferencesWithOrWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource, true); 858 } 859 return Collections.emptySet(); 860 } 861 862 /** 863 * Extracts references from given resource and filters references by those with versions, or those without versions. 864 * 865 * @param theVersionReferencesPaths the paths from which to extract references from 866 * @param theFhirContext the FHIR context 867 * @param theResource the resource from which to extract references from 868 * @param theShouldFilterByRefsWithoutVersionId If true, this method will return only references without a version. If false, this method will return only references with a version. 869 * @return Set of references contained in the resource with or without versions 870 */ 871 private static Set<IBaseReference> getReferencesWithOrWithoutVersionId( 872 Set<String> theVersionReferencesPaths, 873 FhirContext theFhirContext, 874 IBaseResource theResource, 875 boolean theShouldFilterByRefsWithoutVersionId) { 876 return theVersionReferencesPaths.stream() 877 .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class)) 878 .flatMap(Collection::stream) 879 .filter(reference -> 880 reference.getReferenceElement().hasVersionIdPart() ^ theShouldFilterByRefsWithoutVersionId) 881 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 882 .keySet(); 883 } 884 885 public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 886 if (theRequestDetails != null) { 887 theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); 888 } 889 } 890 891 public static void markRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 892 if (theRequestDetails != null) { 893 theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); 894 } 895 } 896}