
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 public static final String OO_SEVERITY_ERROR = "error"; 104 public static final String OO_SEVERITY_INFO = "information"; 105 public static final String OO_SEVERITY_WARN = "warning"; 106 private static final String PROCESSING_SUB_REQUEST = "BaseStorageDao.processingSubRequest"; 107 108 protected static final String MESSAGE_KEY_DELETE_RESOURCE_NOT_EXISTING = "deleteResourceNotExisting"; 109 protected static final String MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED = "deleteResourceAlreadyDeleted"; 110 public static final String OO_ISSUE_CODE_INFORMATIONAL = "informational"; 111 112 @Autowired 113 protected ISearchParamRegistry mySearchParamRegistry; 114 115 @Autowired 116 protected FhirContext myFhirContext; 117 118 @Autowired 119 protected DaoRegistry myDaoRegistry; 120 121 @Autowired 122 protected IResourceVersionSvc myResourceVersionSvc; 123 124 @Autowired 125 protected JpaStorageSettings myStorageSettings; 126 127 @Autowired 128 protected IMetaTagSorter myMetaTagSorter; 129 130 @VisibleForTesting 131 public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) { 132 mySearchParamRegistry = theSearchParamRegistry; 133 } 134 135 @VisibleForTesting 136 public void setMyMetaTagSorter(IMetaTagSorter theMetaTagSorter) { 137 myMetaTagSorter = theMetaTagSorter; 138 } 139 140 /** 141 * May be overridden by subclasses to validate resources prior to storage 142 * 143 * @param theResource The resource that is about to be stored 144 * @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead 145 */ 146 protected void preProcessResourceForStorage(IBaseResource theResource) { 147 // nothing 148 } 149 150 /** 151 * May be overridden by subclasses to validate resources prior to storage 152 * 153 * @param theResource The resource that is about to be stored 154 * @since 5.3.0 155 */ 156 protected void preProcessResourceForStorage( 157 IBaseResource theResource, 158 RequestDetails theRequestDetails, 159 TransactionDetails theTransactionDetails, 160 boolean thePerformIndexing) { 161 162 verifyResourceTypeIsAppropriateForDao(theResource); 163 164 verifyResourceIdIsValid(theResource); 165 166 verifyBundleTypeIsAppropriateForStorage(theResource); 167 168 if (!getStorageSettings().getTreatBaseUrlsAsLocal().isEmpty()) { 169 replaceAbsoluteReferencesWithRelative(theResource, myFhirContext.newTerser()); 170 } 171 172 performAutoVersioning(theResource, thePerformIndexing); 173 174 myMetaTagSorter.sort(theResource.getMeta()); 175 } 176 177 /** 178 * Sanity check - Is this resource the right type for this DAO? 179 */ 180 private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) { 181 String type = getContext().getResourceType(theResource); 182 if (getResourceName() != null && !getResourceName().equals(type)) { 183 throw new InvalidRequestException(Msg.code(520) 184 + getContext() 185 .getLocalizer() 186 .getMessageSanitized( 187 BaseStorageDao.class, "incorrectResourceType", type, getResourceName())); 188 } 189 } 190 191 /** 192 * Verify that the resource ID is actually valid according to FHIR's rules 193 */ 194 private void verifyResourceIdIsValid(IBaseResource theResource) { 195 if (theResource.getIdElement().hasResourceType()) { 196 String expectedType = getContext().getResourceType(theResource); 197 if (!expectedType.equals(theResource.getIdElement().getResourceType())) { 198 throw new InvalidRequestException(Msg.code(2616) 199 + getContext() 200 .getLocalizer() 201 .getMessageSanitized( 202 BaseStorageDao.class, 203 "failedToCreateWithInvalidIdWrongResourceType", 204 theResource.getIdElement().toUnqualifiedVersionless())); 205 } 206 } 207 208 if (theResource.getIdElement().hasIdPart()) { 209 if (!theResource.getIdElement().isIdPartValid()) { 210 throw new InvalidRequestException(Msg.code(521) 211 + getContext() 212 .getLocalizer() 213 .getMessageSanitized( 214 BaseStorageDao.class, 215 "failedToCreateWithInvalidId", 216 theResource.getIdElement().getIdPart())); 217 } 218 } 219 } 220 221 /** 222 * Verify that we're not storing a Bundle with a disallowed bundle type 223 */ 224 private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) { 225 if (theResource instanceof IBaseBundle) { 226 Set<String> allowedBundleTypes = getStorageSettings().getBundleTypesAllowedForStorage(); 227 String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource); 228 bundleType = defaultString(bundleType); 229 if (!allowedBundleTypes.contains(bundleType)) { 230 String message = myFhirContext 231 .getLocalizer() 232 .getMessage( 233 BaseStorageDao.class, 234 "invalidBundleTypeForStorage", 235 (isNotBlank(bundleType) ? bundleType : "(missing)")); 236 throw new UnprocessableEntityException(Msg.code(522) + message); 237 } 238 } 239 } 240 241 /** 242 * Replace absolute references with relative ones if configured to do so 243 */ 244 private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource, FhirTerser theTerser) { 245 List<ResourceReferenceInfo> refs = theTerser.getAllResourceReferences(theResource); 246 for (ResourceReferenceInfo nextRef : refs) { 247 IIdType refId = nextRef.getResourceReference().getReferenceElement(); 248 if (refId != null && refId.hasBaseUrl()) { 249 if (getStorageSettings().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) { 250 IIdType newRefId = refId.toUnqualified(); 251 nextRef.getResourceReference().setReference(newRefId.getValue()); 252 } 253 } 254 } 255 } 256 257 /** 258 * Handle {@link JpaStorageSettings#getAutoVersionReferenceAtPaths() auto-populate-versions} 259 * <p> 260 * We only do this if thePerformIndexing is true because if it's false, that means 261 * we're in a FHIR transaction during the first phase of write operation processing, 262 * meaning that the versions of other resources may not have need updatd yet. For example 263 * we're about to store an Observation with a reference to a Patient, and that Patient 264 * is also being updated in the same transaction, during the first "no index" phase, 265 * the Patient will not yet have its version number incremented, so it would be wrong 266 * to use that value. During the second phase it is correct. 267 * <p> 268 * Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning 269 * and it is the one that takes care of the placeholder IDs. Look for the other caller of 270 * {@link #extractReferencesToAutoVersion(FhirContext, StorageSettings, IBaseResource)} 271 * to find this. 272 */ 273 private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) { 274 if (thePerformIndexing) { 275 Set<IBaseReference> referencesToVersion = 276 extractReferencesToAutoVersion(myFhirContext, myStorageSettings, theResource); 277 for (IBaseReference nextReference : referencesToVersion) { 278 IIdType referenceElement = nextReference.getReferenceElement(); 279 if (!referenceElement.hasBaseUrl()) { 280 281 ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds( 282 RequestPartitionId.allPartitions(), Collections.singletonList(referenceElement)); 283 284 // 3 cases: 285 // 1) there exists a resource in the db with some version (use this version) 286 // 2) no resource exists, but we will create one (eventually). The version is 1 287 // 3) no resource exists, and none will be made -> throw 288 Long version; 289 if (resourceVersionMap.containsKey(referenceElement)) { 290 // the resource exists... latest id 291 // will be the value in the IResourcePersistentId 292 version = resourceVersionMap 293 .getResourcePersistentId(referenceElement) 294 .getVersion(); 295 } else if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) { 296 // if idToPID doesn't contain object 297 // but autcreateplaceholders is on 298 // then the version will be 1 (the first version) 299 version = 1L; 300 } else { 301 // resource not found 302 // and no autocreateplaceholders set... 303 // we throw 304 throw new ResourceNotFoundException(Msg.code(523) + referenceElement); 305 } 306 String newTargetReference = 307 referenceElement.withVersion(version.toString()).getValue(); 308 nextReference.setReference(newTargetReference); 309 } 310 } 311 } 312 } 313 314 protected DaoMethodOutcome toMethodOutcome( 315 RequestDetails theRequest, 316 @Nonnull final IBasePersistedResource theEntity, 317 @Nonnull IBaseResource theResource, 318 @Nullable String theMatchUrl, 319 @Nonnull RestOperationTypeEnum theOperationType) { 320 DaoMethodOutcome outcome = new DaoMethodOutcome(); 321 322 IResourcePersistentId persistentId = theEntity.getPersistentId(); 323 persistentId.setAssociatedResourceId(theResource.getIdElement()); 324 325 outcome.setPersistentId(persistentId); 326 outcome.setMatchUrl(theMatchUrl); 327 outcome.setOperationType(theOperationType); 328 329 if (theEntity instanceof ResourceTable) { 330 if (((ResourceTable) theEntity).isUnchangedInCurrentOperation()) { 331 outcome.setNop(true); 332 } 333 } 334 335 IIdType id = null; 336 if (theResource.getIdElement().getValue() != null) { 337 id = theResource.getIdElement(); 338 } 339 if (id == null) { 340 id = theEntity.getIdDt(); 341 if (getContext().getVersion().getVersion().isRi()) { 342 id = getContext().getVersion().newIdType().setValue(id.getValue()); 343 } 344 } 345 346 outcome.setId(id); 347 if (theEntity.getDeleted() == null) { 348 outcome.setResource(theResource); 349 } 350 outcome.setEntity(theEntity); 351 352 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 353 if (outcome.getResource() != null) { 354 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource()); 355 IInterceptorBroadcaster compositeBroadcaster = 356 CompositeInterceptorBroadcaster.newCompositeBroadcaster(getInterceptorBroadcaster(), theRequest); 357 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 358 HookParams params = new HookParams() 359 .add(IPreResourceAccessDetails.class, accessDetails) 360 .add(RequestDetails.class, theRequest) 361 .addIfMatchesType(ServletRequestDetails.class, theRequest); 362 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 363 if (accessDetails.isDontReturnResourceAtIndex(0)) { 364 outcome.setResource(null); 365 } 366 } 367 } 368 369 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 370 // Note that this will only fire if someone actually goes to use the 371 // resource in a response (it's their responsibility to call 372 // outcome.fireResourceViewCallback()) 373 outcome.registerResourceViewCallback(() -> { 374 if (outcome.getResource() != null) { 375 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 376 getInterceptorBroadcaster(), theRequest); 377 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 378 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 379 HookParams params = new HookParams() 380 .add(IPreResourceShowDetails.class, showDetails) 381 .add(RequestDetails.class, theRequest) 382 .addIfMatchesType(ServletRequestDetails.class, theRequest); 383 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 384 outcome.setResource(showDetails.getResource(0)); 385 } 386 } 387 }); 388 389 return outcome; 390 } 391 392 protected DaoMethodOutcome toMethodOutcomeLazy( 393 RequestDetails theRequest, 394 IResourcePersistentId theResourcePersistentId, 395 @Nonnull final Supplier<LazyDaoMethodOutcome.EntityAndResource> theEntity, 396 Supplier<IIdType> theIdSupplier) { 397 LazyDaoMethodOutcome outcome = new LazyDaoMethodOutcome(theResourcePersistentId); 398 399 outcome.setEntitySupplier(theEntity); 400 outcome.setIdSupplier(theIdSupplier); 401 outcome.setEntitySupplierUseCallback(() -> { 402 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 403 if (outcome.getResource() != null) { 404 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 405 getInterceptorBroadcaster(), theRequest); 406 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 407 SimplePreResourceAccessDetails accessDetails = 408 new SimplePreResourceAccessDetails(outcome.getResource()); 409 HookParams params = new HookParams() 410 .add(IPreResourceAccessDetails.class, accessDetails) 411 .add(RequestDetails.class, theRequest) 412 .addIfMatchesType(ServletRequestDetails.class, theRequest); 413 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 414 if (accessDetails.isDontReturnResourceAtIndex(0)) { 415 outcome.setResource(null); 416 } 417 } 418 } 419 420 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 421 // Note that this will only fire if someone actually goes to use the 422 // resource in a response (it's their responsibility to call 423 // outcome.fireResourceViewCallback()) 424 outcome.registerResourceViewCallback(() -> { 425 if (outcome.getResource() != null) { 426 IInterceptorBroadcaster compositeBroadcaster = 427 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 428 getInterceptorBroadcaster(), theRequest); 429 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 430 SimplePreResourceShowDetails showDetails = 431 new SimplePreResourceShowDetails(outcome.getResource()); 432 HookParams params = new HookParams() 433 .add(IPreResourceShowDetails.class, showDetails) 434 .add(RequestDetails.class, theRequest) 435 .addIfMatchesType(ServletRequestDetails.class, theRequest); 436 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 437 outcome.setResource(showDetails.getResource(0)); 438 } 439 } 440 }); 441 }); 442 443 return outcome; 444 } 445 446 protected void doCallHooks( 447 TransactionDetails theTransactionDetails, 448 RequestDetails theRequestDetails, 449 Pointcut thePointcut, 450 HookParams theParams) { 451 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) { 452 theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams); 453 } else { 454 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 455 getInterceptorBroadcaster(), theRequestDetails); 456 compositeBroadcaster.callHooks(thePointcut, theParams); 457 } 458 } 459 460 protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); 461 462 public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) { 463 return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode); 464 } 465 466 public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) { 467 return createInfoOperationOutcome(theMessage, null); 468 } 469 470 public IBaseOperationOutcome createInfoOperationOutcome( 471 String theMessage, @Nullable StorageResponseCodeEnum theStorageResponseCode) { 472 return createOperationOutcome( 473 OO_SEVERITY_INFO, theMessage, OO_ISSUE_CODE_INFORMATIONAL, theStorageResponseCode); 474 } 475 476 private IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) { 477 return createOperationOutcome(theSeverity, theMessage, theCode, null); 478 } 479 480 protected IBaseOperationOutcome createOperationOutcome( 481 String theSeverity, 482 String theMessage, 483 String theCode, 484 @Nullable StorageResponseCodeEnum theStorageResponseCode) { 485 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); 486 String detailSystem = null; 487 String detailCode = null; 488 String detailDescription = null; 489 if (theStorageResponseCode != null) { 490 detailSystem = theStorageResponseCode.getSystem(); 491 detailCode = theStorageResponseCode.getCode(); 492 detailDescription = theStorageResponseCode.getDisplay(); 493 } 494 OperationOutcomeUtil.addIssue( 495 getContext(), oo, theSeverity, theMessage, null, theCode, detailSystem, detailCode, detailDescription); 496 return oo; 497 } 498 499 /** 500 * Creates a base method outcome for a delete request for the provided ID. 501 * <p> 502 * Additional information may be set on the outcome. 503 * 504 * @param theResourceId - the id of the object being deleted. Eg: Patient/123 505 */ 506 protected DaoMethodOutcome createMethodOutcomeForResourceId( 507 String theResourceId, String theMessageKey, StorageResponseCodeEnum theStorageResponseCode) { 508 DaoMethodOutcome outcome = new DaoMethodOutcome(); 509 510 IIdType id = getContext().getVersion().newIdType(); 511 id.setValue(theResourceId); 512 outcome.setId(id); 513 514 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, theMessageKey, id); 515 String severity = "information"; 516 String code = "informational"; 517 IBaseOperationOutcome oo = createOperationOutcome(severity, message, code, 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 OO_SEVERITY_INFO, 772 msg, 773 null, 774 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}