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 HookParams params = new HookParams() 339 .add(IPreResourceAccessDetails.class, accessDetails) 340 .add(RequestDetails.class, theRequest) 341 .addIfMatchesType(ServletRequestDetails.class, theRequest); 342 CompositeInterceptorBroadcaster.doCallHooks( 343 getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 344 if (accessDetails.isDontReturnResourceAtIndex(0)) { 345 outcome.setResource(null); 346 } 347 } 348 349 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 350 // Note that this will only fire if someone actually goes to use the 351 // resource in a response (it's their responsibility to call 352 // outcome.fireResourceViewCallback()) 353 outcome.registerResourceViewCallback(() -> { 354 if (outcome.getResource() != null) { 355 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 356 HookParams params = new HookParams() 357 .add(IPreResourceShowDetails.class, showDetails) 358 .add(RequestDetails.class, theRequest) 359 .addIfMatchesType(ServletRequestDetails.class, theRequest); 360 CompositeInterceptorBroadcaster.doCallHooks( 361 getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 362 outcome.setResource(showDetails.getResource(0)); 363 } 364 }); 365 366 return outcome; 367 } 368 369 protected DaoMethodOutcome toMethodOutcomeLazy( 370 RequestDetails theRequest, 371 IResourcePersistentId theResourcePersistentId, 372 @Nonnull final Supplier<LazyDaoMethodOutcome.EntityAndResource> theEntity, 373 Supplier<IIdType> theIdSupplier) { 374 LazyDaoMethodOutcome outcome = new LazyDaoMethodOutcome(theResourcePersistentId); 375 376 outcome.setEntitySupplier(theEntity); 377 outcome.setIdSupplier(theIdSupplier); 378 outcome.setEntitySupplierUseCallback(() -> { 379 // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES 380 if (outcome.getResource() != null) { 381 SimplePreResourceAccessDetails accessDetails = 382 new SimplePreResourceAccessDetails(outcome.getResource()); 383 HookParams params = new HookParams() 384 .add(IPreResourceAccessDetails.class, accessDetails) 385 .add(RequestDetails.class, theRequest) 386 .addIfMatchesType(ServletRequestDetails.class, theRequest); 387 CompositeInterceptorBroadcaster.doCallHooks( 388 getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params); 389 if (accessDetails.isDontReturnResourceAtIndex(0)) { 390 outcome.setResource(null); 391 } 392 } 393 394 // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES 395 // Note that this will only fire if someone actually goes to use the 396 // resource in a response (it's their responsibility to call 397 // outcome.fireResourceViewCallback()) 398 outcome.registerResourceViewCallback(() -> { 399 if (outcome.getResource() != null) { 400 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource()); 401 HookParams params = new HookParams() 402 .add(IPreResourceShowDetails.class, showDetails) 403 .add(RequestDetails.class, theRequest) 404 .addIfMatchesType(ServletRequestDetails.class, theRequest); 405 CompositeInterceptorBroadcaster.doCallHooks( 406 getInterceptorBroadcaster(), theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params); 407 outcome.setResource(showDetails.getResource(0)); 408 } 409 }); 410 }); 411 412 return outcome; 413 } 414 415 protected void doCallHooks( 416 TransactionDetails theTransactionDetails, 417 RequestDetails theRequestDetails, 418 Pointcut thePointcut, 419 HookParams theParams) { 420 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) { 421 theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams); 422 } else { 423 CompositeInterceptorBroadcaster.doCallHooks( 424 getInterceptorBroadcaster(), theRequestDetails, thePointcut, theParams); 425 } 426 } 427 428 protected abstract IInterceptorBroadcaster getInterceptorBroadcaster(); 429 430 public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) { 431 return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode); 432 } 433 434 public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) { 435 return createInfoOperationOutcome(theMessage, null); 436 } 437 438 public IBaseOperationOutcome createInfoOperationOutcome( 439 String theMessage, @Nullable StorageResponseCodeEnum theStorageResponseCode) { 440 return createOperationOutcome(OO_SEVERITY_INFO, theMessage, "informational", theStorageResponseCode); 441 } 442 443 private IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) { 444 return createOperationOutcome(theSeverity, theMessage, theCode, null); 445 } 446 447 protected IBaseOperationOutcome createOperationOutcome( 448 String theSeverity, 449 String theMessage, 450 String theCode, 451 @Nullable StorageResponseCodeEnum theStorageResponseCode) { 452 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext()); 453 String detailSystem = null; 454 String detailCode = null; 455 String detailDescription = null; 456 if (theStorageResponseCode != null) { 457 detailSystem = theStorageResponseCode.getSystem(); 458 detailCode = theStorageResponseCode.getCode(); 459 detailDescription = theStorageResponseCode.getDisplay(); 460 } 461 OperationOutcomeUtil.addIssue( 462 getContext(), oo, theSeverity, theMessage, null, theCode, detailSystem, detailCode, detailDescription); 463 return oo; 464 } 465 466 /** 467 * Creates a base method outcome for a delete request for the provided ID. 468 * <p> 469 * Additional information may be set on the outcome. 470 * 471 * @param theResourceId - the id of the object being deleted. Eg: Patient/123 472 */ 473 protected DaoMethodOutcome createMethodOutcomeForResourceId( 474 String theResourceId, String theMessageKey, StorageResponseCodeEnum theStorageResponseCode) { 475 DaoMethodOutcome outcome = new DaoMethodOutcome(); 476 477 IIdType id = getContext().getVersion().newIdType(); 478 id.setValue(theResourceId); 479 outcome.setId(id); 480 481 String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, theMessageKey, id); 482 String severity = "information"; 483 String code = "informational"; 484 IBaseOperationOutcome oo = createOperationOutcome(severity, message, code, theStorageResponseCode); 485 outcome.setOperationOutcome(oo); 486 487 return outcome; 488 } 489 490 @Nonnull 491 protected ResourceGoneException createResourceGoneException(IBasePersistedResource theResourceEntity) { 492 StringBuilder b = new StringBuilder(); 493 b.append("Resource was deleted at "); 494 b.append(new InstantType(theResourceEntity.getDeleted()).getValueAsString()); 495 ResourceGoneException retVal = new ResourceGoneException(b.toString()); 496 retVal.setResourceId(theResourceEntity.getIdDt()); 497 return retVal; 498 } 499 500 /** 501 * Provide the JpaStorageSettings 502 */ 503 protected abstract JpaStorageSettings getStorageSettings(); 504 505 /** 506 * Returns the resource type for this DAO, or null if this is a system-level DAO 507 */ 508 @Nullable 509 protected abstract String getResourceName(); 510 511 /** 512 * Provides the FHIR context 513 */ 514 protected abstract FhirContext getContext(); 515 516 @Transactional(propagation = Propagation.SUPPORTS) 517 public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) { 518 if (theSource == null || theSource.isEmpty()) { 519 return; 520 } 521 522 ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams( 523 getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 524 525 Set<String> paramNames = theSource.keySet(); 526 for (String nextParamName : paramNames) { 527 QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName); 528 RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName()); 529 if (param == null) { 530 Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta( 531 getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 532 RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam( 533 getResourceName(), 534 qualifiedParamName.getParamName(), 535 ISearchParamRegistry.SearchParamLookupContextEnum.ALL); 536 if (notEnabledForSearchParam != null) { 537 String msg = getContext() 538 .getLocalizer() 539 .getMessageSanitized( 540 BaseStorageDao.class, 541 "invalidSearchParameterNotEnabledForSearch", 542 qualifiedParamName.getParamName(), 543 getResourceName(), 544 validNames); 545 throw new InvalidRequestException(Msg.code(2539) + msg); 546 } else { 547 String msg = getContext() 548 .getLocalizer() 549 .getMessageSanitized( 550 BaseStorageDao.class, 551 "invalidSearchParameter", 552 qualifiedParamName.getParamName(), 553 getResourceName(), 554 validNames); 555 throw new InvalidRequestException(Msg.code(524) + msg); 556 } 557 } 558 559 // Should not be null since the check above would have caught it 560 RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam( 561 getResourceName(), 562 qualifiedParamName.getParamName(), 563 ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 564 565 for (String nextValue : theSource.get(nextParamName)) { 566 QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape( 567 qualifiedParamName.getWholeQualifier(), nextValue); 568 List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam); 569 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 570 mySearchParamRegistry, getContext(), paramDef, nextParamName, paramList); 571 theTarget.add(qualifiedParamName.getParamName(), parsedParam); 572 } 573 } 574 } 575 576 protected void populateOperationOutcomeForUpdate( 577 @Nullable StopWatch theItemStopwatch, 578 DaoMethodOutcome theMethodOutcome, 579 String theMatchUrl, 580 RestOperationTypeEnum theOperationType) { 581 String msg; 582 StorageResponseCodeEnum outcome; 583 584 if (theOperationType == RestOperationTypeEnum.PATCH) { 585 586 if (theMatchUrl != null) { 587 if (theMethodOutcome.isNop()) { 588 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH_NO_CHANGE; 589 msg = getContext() 590 .getLocalizer() 591 .getMessageSanitized( 592 BaseStorageDao.class, 593 "successfulPatchConditionalNoChange", 594 theMethodOutcome.getId(), 595 UrlUtil.sanitizeUrlPart(theMatchUrl), 596 theMethodOutcome.getId()); 597 } else { 598 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH; 599 msg = getContext() 600 .getLocalizer() 601 .getMessageSanitized( 602 BaseStorageDao.class, 603 "successfulPatchConditional", 604 theMethodOutcome.getId(), 605 UrlUtil.sanitizeUrlPart(theMatchUrl), 606 theMethodOutcome.getId()); 607 } 608 } else { 609 if (theMethodOutcome.isNop()) { 610 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH_NO_CHANGE; 611 msg = getContext() 612 .getLocalizer() 613 .getMessageSanitized( 614 BaseStorageDao.class, "successfulPatchNoChange", theMethodOutcome.getId()); 615 } else { 616 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH; 617 msg = getContext() 618 .getLocalizer() 619 .getMessageSanitized(BaseStorageDao.class, "successfulPatch", theMethodOutcome.getId()); 620 } 621 } 622 623 } else if (theOperationType == RestOperationTypeEnum.CREATE) { 624 625 if (theMatchUrl == null) { 626 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE; 627 msg = getContext() 628 .getLocalizer() 629 .getMessageSanitized(BaseStorageDao.class, "successfulCreate", theMethodOutcome.getId()); 630 } else if (theMethodOutcome.isNop()) { 631 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH; 632 msg = getContext() 633 .getLocalizer() 634 .getMessageSanitized( 635 BaseStorageDao.class, 636 "successfulCreateConditionalWithMatch", 637 theMethodOutcome.getId(), 638 UrlUtil.sanitizeUrlPart(theMatchUrl)); 639 } else { 640 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_NO_CONDITIONAL_MATCH; 641 msg = getContext() 642 .getLocalizer() 643 .getMessageSanitized( 644 BaseStorageDao.class, 645 "successfulCreateConditionalNoMatch", 646 theMethodOutcome.getId(), 647 UrlUtil.sanitizeUrlPart(theMatchUrl)); 648 } 649 650 } else if (theMethodOutcome.isNop()) { 651 652 if (theMatchUrl != null) { 653 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH_NO_CHANGE; 654 msg = getContext() 655 .getLocalizer() 656 .getMessageSanitized( 657 BaseStorageDao.class, 658 "successfulUpdateConditionalNoChangeWithMatch", 659 theMethodOutcome.getId(), 660 theMatchUrl); 661 } else { 662 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CHANGE; 663 msg = getContext() 664 .getLocalizer() 665 .getMessageSanitized( 666 BaseStorageDao.class, "successfulUpdateNoChange", theMethodOutcome.getId()); 667 } 668 669 } else { 670 671 if (theMatchUrl != null) { 672 if (theMethodOutcome.getCreated() == Boolean.TRUE) { 673 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CONDITIONAL_MATCH; 674 msg = getContext() 675 .getLocalizer() 676 .getMessageSanitized( 677 BaseStorageDao.class, 678 "successfulUpdateConditionalNoMatch", 679 theMethodOutcome.getId()); 680 } else { 681 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH; 682 msg = getContext() 683 .getLocalizer() 684 .getMessageSanitized( 685 BaseStorageDao.class, 686 "successfulUpdateConditionalWithMatch", 687 theMethodOutcome.getId(), 688 theMatchUrl); 689 } 690 } else if (theMethodOutcome.getCreated() == Boolean.TRUE) { 691 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_AS_CREATE; 692 msg = getContext() 693 .getLocalizer() 694 .getMessageSanitized( 695 BaseStorageDao.class, "successfulUpdateAsCreate", theMethodOutcome.getId()); 696 } else { 697 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE; 698 msg = getContext() 699 .getLocalizer() 700 .getMessageSanitized(BaseStorageDao.class, "successfulUpdate", theMethodOutcome.getId()); 701 } 702 } 703 704 if (theItemStopwatch != null) { 705 String msgSuffix = getContext() 706 .getLocalizer() 707 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", theItemStopwatch.getMillis()); 708 msg = msg + " " + msgSuffix; 709 } 710 711 theMethodOutcome.setOperationOutcome(createInfoOperationOutcome(msg, outcome)); 712 ourLog.debug(msg); 713 } 714 715 /** 716 * Extracts a list of references that should be auto-versioned. 717 * 718 * @return A set of references that should be versioned according to both storage settings 719 * and auto-version reference extensions, or it may also be empty. 720 */ 721 @Nonnull 722 public static Set<IBaseReference> extractReferencesToAutoVersion( 723 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 724 Set<IBaseReference> referencesToAutoVersionFromConfig = 725 getReferencesToAutoVersionFromConfig(theFhirContext, theStorageSettings, theResource); 726 727 Set<IBaseReference> referencesToAutoVersionFromExtensions = 728 getReferencesToAutoVersionFromExtension(theFhirContext, theResource); 729 730 return Stream.concat(referencesToAutoVersionFromConfig.stream(), referencesToAutoVersionFromExtensions.stream()) 731 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 732 .keySet(); 733 } 734 735 /** 736 * Extracts a list of references that should be auto-versioned according to 737 * <code>auto-version-references-at-path</code> extensions. 738 * @see HapiExtensions#EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH 739 */ 740 @Nonnull 741 private static Set<IBaseReference> getReferencesToAutoVersionFromExtension( 742 FhirContext theFhirContext, IBaseResource theResource) { 743 String resourceType = theFhirContext.getResourceType(theResource); 744 Set<String> autoVersionReferencesAtPaths = 745 MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType); 746 747 if (!autoVersionReferencesAtPaths.isEmpty()) { 748 return getReferencesWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource); 749 } 750 return Collections.emptySet(); 751 } 752 753 /** 754 * Extracts a list of references that should be auto-versioned according to storage configuration. 755 * @see StorageSettings#getAutoVersionReferenceAtPaths() 756 */ 757 @Nonnull 758 private static Set<IBaseReference> getReferencesToAutoVersionFromConfig( 759 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 760 if (!theStorageSettings.getAutoVersionReferenceAtPaths().isEmpty()) { 761 String resourceName = theFhirContext.getResourceType(theResource); 762 Set<String> autoVersionReferencesPaths = 763 theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName); 764 return getReferencesWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource); 765 } 766 return Collections.emptySet(); 767 } 768 769 private static Set<IBaseReference> getReferencesWithoutVersionId( 770 Set<String> autoVersionReferencesPaths, FhirContext theFhirContext, IBaseResource theResource) { 771 return autoVersionReferencesPaths.stream() 772 .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class)) 773 .flatMap(Collection::stream) 774 .filter(reference -> !reference.getReferenceElement().hasVersionIdPart()) 775 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 776 .keySet(); 777 } 778 779 public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 780 if (theRequestDetails != null) { 781 theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); 782 } 783 } 784 785 public static void markRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 786 if (theRequestDetails != null) { 787 theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); 788 } 789 } 790}