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