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