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(getResourceName()); 523 524 Set<String> paramNames = theSource.keySet(); 525 for (String nextParamName : paramNames) { 526 QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName); 527 RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName()); 528 if (param == null) { 529 Collection<String> validNames = 530 mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(getResourceName()); 531 String msg = getContext() 532 .getLocalizer() 533 .getMessageSanitized( 534 BaseStorageDao.class, 535 "invalidSearchParameter", 536 qualifiedParamName.getParamName(), 537 getResourceName(), 538 validNames); 539 throw new InvalidRequestException(Msg.code(524) + msg); 540 } 541 542 // Should not be null since the check above would have caught it 543 RuntimeSearchParam paramDef = 544 mySearchParamRegistry.getActiveSearchParam(getResourceName(), qualifiedParamName.getParamName()); 545 546 for (String nextValue : theSource.get(nextParamName)) { 547 QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape( 548 qualifiedParamName.getWholeQualifier(), nextValue); 549 List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam); 550 IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams( 551 mySearchParamRegistry, getContext(), paramDef, nextParamName, paramList); 552 theTarget.add(qualifiedParamName.getParamName(), parsedParam); 553 } 554 } 555 } 556 557 protected void populateOperationOutcomeForUpdate( 558 @Nullable StopWatch theItemStopwatch, 559 DaoMethodOutcome theMethodOutcome, 560 String theMatchUrl, 561 RestOperationTypeEnum theOperationType) { 562 String msg; 563 StorageResponseCodeEnum outcome; 564 565 if (theOperationType == RestOperationTypeEnum.PATCH) { 566 567 if (theMatchUrl != null) { 568 if (theMethodOutcome.isNop()) { 569 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH_NO_CHANGE; 570 msg = getContext() 571 .getLocalizer() 572 .getMessageSanitized( 573 BaseStorageDao.class, 574 "successfulPatchConditionalNoChange", 575 theMethodOutcome.getId(), 576 UrlUtil.sanitizeUrlPart(theMatchUrl), 577 theMethodOutcome.getId()); 578 } else { 579 outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH; 580 msg = getContext() 581 .getLocalizer() 582 .getMessageSanitized( 583 BaseStorageDao.class, 584 "successfulPatchConditional", 585 theMethodOutcome.getId(), 586 UrlUtil.sanitizeUrlPart(theMatchUrl), 587 theMethodOutcome.getId()); 588 } 589 } else { 590 if (theMethodOutcome.isNop()) { 591 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH_NO_CHANGE; 592 msg = getContext() 593 .getLocalizer() 594 .getMessageSanitized( 595 BaseStorageDao.class, "successfulPatchNoChange", theMethodOutcome.getId()); 596 } else { 597 outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH; 598 msg = getContext() 599 .getLocalizer() 600 .getMessageSanitized(BaseStorageDao.class, "successfulPatch", theMethodOutcome.getId()); 601 } 602 } 603 604 } else if (theOperationType == RestOperationTypeEnum.CREATE) { 605 606 if (theMatchUrl == null) { 607 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE; 608 msg = getContext() 609 .getLocalizer() 610 .getMessageSanitized(BaseStorageDao.class, "successfulCreate", theMethodOutcome.getId()); 611 } else if (theMethodOutcome.isNop()) { 612 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH; 613 msg = getContext() 614 .getLocalizer() 615 .getMessageSanitized( 616 BaseStorageDao.class, 617 "successfulCreateConditionalWithMatch", 618 theMethodOutcome.getId(), 619 UrlUtil.sanitizeUrlPart(theMatchUrl)); 620 } else { 621 outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_NO_CONDITIONAL_MATCH; 622 msg = getContext() 623 .getLocalizer() 624 .getMessageSanitized( 625 BaseStorageDao.class, 626 "successfulCreateConditionalNoMatch", 627 theMethodOutcome.getId(), 628 UrlUtil.sanitizeUrlPart(theMatchUrl)); 629 } 630 631 } else if (theMethodOutcome.isNop()) { 632 633 if (theMatchUrl != null) { 634 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH_NO_CHANGE; 635 msg = getContext() 636 .getLocalizer() 637 .getMessageSanitized( 638 BaseStorageDao.class, 639 "successfulUpdateConditionalNoChangeWithMatch", 640 theMethodOutcome.getId(), 641 theMatchUrl); 642 } else { 643 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CHANGE; 644 msg = getContext() 645 .getLocalizer() 646 .getMessageSanitized( 647 BaseStorageDao.class, "successfulUpdateNoChange", theMethodOutcome.getId()); 648 } 649 650 } else { 651 652 if (theMatchUrl != null) { 653 if (theMethodOutcome.getCreated() == Boolean.TRUE) { 654 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CONDITIONAL_MATCH; 655 msg = getContext() 656 .getLocalizer() 657 .getMessageSanitized( 658 BaseStorageDao.class, 659 "successfulUpdateConditionalNoMatch", 660 theMethodOutcome.getId()); 661 } else { 662 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH; 663 msg = getContext() 664 .getLocalizer() 665 .getMessageSanitized( 666 BaseStorageDao.class, 667 "successfulUpdateConditionalWithMatch", 668 theMethodOutcome.getId(), 669 theMatchUrl); 670 } 671 } else if (theMethodOutcome.getCreated() == Boolean.TRUE) { 672 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_AS_CREATE; 673 msg = getContext() 674 .getLocalizer() 675 .getMessageSanitized( 676 BaseStorageDao.class, "successfulUpdateAsCreate", theMethodOutcome.getId()); 677 } else { 678 outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE; 679 msg = getContext() 680 .getLocalizer() 681 .getMessageSanitized(BaseStorageDao.class, "successfulUpdate", theMethodOutcome.getId()); 682 } 683 } 684 685 if (theItemStopwatch != null) { 686 String msgSuffix = getContext() 687 .getLocalizer() 688 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", theItemStopwatch.getMillis()); 689 msg = msg + " " + msgSuffix; 690 } 691 692 theMethodOutcome.setOperationOutcome(createInfoOperationOutcome(msg, outcome)); 693 ourLog.debug(msg); 694 } 695 696 /** 697 * Extracts a list of references that should be auto-versioned. 698 * 699 * @return A set of references that should be versioned according to both storage settings 700 * and auto-version reference extensions, or it may also be empty. 701 */ 702 @Nonnull 703 public static Set<IBaseReference> extractReferencesToAutoVersion( 704 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 705 Set<IBaseReference> referencesToAutoVersionFromConfig = 706 getReferencesToAutoVersionFromConfig(theFhirContext, theStorageSettings, theResource); 707 708 Set<IBaseReference> referencesToAutoVersionFromExtensions = 709 getReferencesToAutoVersionFromExtension(theFhirContext, theResource); 710 711 return Stream.concat(referencesToAutoVersionFromConfig.stream(), referencesToAutoVersionFromExtensions.stream()) 712 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 713 .keySet(); 714 } 715 716 /** 717 * Extracts a list of references that should be auto-versioned according to 718 * <code>auto-version-references-at-path</code> extensions. 719 * @see HapiExtensions#EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH 720 */ 721 @Nonnull 722 private static Set<IBaseReference> getReferencesToAutoVersionFromExtension( 723 FhirContext theFhirContext, IBaseResource theResource) { 724 String resourceType = theFhirContext.getResourceType(theResource); 725 Set<String> autoVersionReferencesAtPaths = 726 MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType); 727 728 if (!autoVersionReferencesAtPaths.isEmpty()) { 729 return getReferencesWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource); 730 } 731 return Collections.emptySet(); 732 } 733 734 /** 735 * Extracts a list of references that should be auto-versioned according to storage configuration. 736 * @see StorageSettings#getAutoVersionReferenceAtPaths() 737 */ 738 @Nonnull 739 private static Set<IBaseReference> getReferencesToAutoVersionFromConfig( 740 FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) { 741 if (!theStorageSettings.getAutoVersionReferenceAtPaths().isEmpty()) { 742 String resourceName = theFhirContext.getResourceType(theResource); 743 Set<String> autoVersionReferencesPaths = 744 theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName); 745 return getReferencesWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource); 746 } 747 return Collections.emptySet(); 748 } 749 750 private static Set<IBaseReference> getReferencesWithoutVersionId( 751 Set<String> autoVersionReferencesPaths, FhirContext theFhirContext, IBaseResource theResource) { 752 return autoVersionReferencesPaths.stream() 753 .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class)) 754 .flatMap(Collection::stream) 755 .filter(reference -> !reference.getReferenceElement().hasVersionIdPart()) 756 .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new)) 757 .keySet(); 758 } 759 760 public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 761 if (theRequestDetails != null) { 762 theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST); 763 } 764 } 765 766 public static void markRequestAsProcessingSubRequest(RequestDetails theRequestDetails) { 767 if (theRequestDetails != null) { 768 theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE); 769 } 770 } 771}