
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; 030import ca.uhn.fhir.interceptor.model.RequestPartitionId; 031import ca.uhn.fhir.interceptor.model.TransactionWriteOperationsDetails; 032import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 033import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 034import ca.uhn.fhir.jpa.api.dao.IJpaDao; 035import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 036import ca.uhn.fhir.jpa.api.model.DeleteConflict; 037import ca.uhn.fhir.jpa.api.model.DeleteConflictList; 038import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; 039import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; 040import ca.uhn.fhir.jpa.cache.IResourceVersionSvc; 041import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap; 042import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 043import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 044import ca.uhn.fhir.jpa.delete.DeleteConflictUtil; 045import ca.uhn.fhir.jpa.model.config.PartitionSettings; 046import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 047import ca.uhn.fhir.jpa.model.entity.ResourceTable; 048import ca.uhn.fhir.jpa.model.entity.StorageSettings; 049import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; 050import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 051import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 052import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; 053import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; 054import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; 055import ca.uhn.fhir.jpa.util.TransactionSemanticsHeader; 056import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 057import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; 058import ca.uhn.fhir.parser.DataFormatException; 059import ca.uhn.fhir.parser.IParser; 060import ca.uhn.fhir.rest.api.Constants; 061import ca.uhn.fhir.rest.api.PatchTypeEnum; 062import ca.uhn.fhir.rest.api.PreferReturnEnum; 063import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 064import ca.uhn.fhir.rest.api.server.RequestDetails; 065import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 066import ca.uhn.fhir.rest.api.server.storage.DeferredInterceptorBroadcasts; 067import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 068import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 069import ca.uhn.fhir.rest.param.ParameterUtil; 070import ca.uhn.fhir.rest.server.RestfulServerUtils; 071import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 072import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 073import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 074import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 075import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; 076import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; 077import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 078import ca.uhn.fhir.rest.server.method.BaseMethodBinding; 079import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; 080import ca.uhn.fhir.rest.server.method.UpdateMethodBinding; 081import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 082import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails; 083import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 084import ca.uhn.fhir.rest.server.util.ServletRequestUtil; 085import ca.uhn.fhir.util.AsyncUtil; 086import ca.uhn.fhir.util.BundleUtil; 087import ca.uhn.fhir.util.ElementUtil; 088import ca.uhn.fhir.util.FhirTerser; 089import ca.uhn.fhir.util.ResourceReferenceInfo; 090import ca.uhn.fhir.util.StopWatch; 091import ca.uhn.fhir.util.UrlUtil; 092import com.google.common.annotations.VisibleForTesting; 093import com.google.common.collect.ArrayListMultimap; 094import com.google.common.collect.ListMultimap; 095import jakarta.annotation.Nonnull; 096import jakarta.annotation.Nullable; 097import org.apache.commons.lang3.RandomUtils; 098import org.apache.commons.lang3.StringUtils; 099import org.apache.commons.lang3.ThreadUtils; 100import org.apache.commons.lang3.Validate; 101import org.hl7.fhir.dstu3.model.Bundle; 102import org.hl7.fhir.exceptions.FHIRException; 103import org.hl7.fhir.instance.model.api.IBase; 104import org.hl7.fhir.instance.model.api.IBaseBinary; 105import org.hl7.fhir.instance.model.api.IBaseBundle; 106import org.hl7.fhir.instance.model.api.IBaseExtension; 107import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 108import org.hl7.fhir.instance.model.api.IBaseParameters; 109import org.hl7.fhir.instance.model.api.IBaseReference; 110import org.hl7.fhir.instance.model.api.IBaseResource; 111import org.hl7.fhir.instance.model.api.IIdType; 112import org.hl7.fhir.instance.model.api.IPrimitiveType; 113import org.hl7.fhir.r4.model.IdType; 114import org.slf4j.Logger; 115import org.slf4j.LoggerFactory; 116import org.springframework.beans.factory.annotation.Autowired; 117import org.springframework.core.task.TaskExecutor; 118import org.springframework.transaction.PlatformTransactionManager; 119import org.springframework.transaction.TransactionDefinition; 120import org.springframework.transaction.support.TransactionCallback; 121import org.springframework.transaction.support.TransactionTemplate; 122 123import java.time.Duration; 124import java.util.ArrayList; 125import java.util.Collection; 126import java.util.Collections; 127import java.util.Comparator; 128import java.util.Date; 129import java.util.HashMap; 130import java.util.HashSet; 131import java.util.IdentityHashMap; 132import java.util.Iterator; 133import java.util.LinkedHashMap; 134import java.util.LinkedHashSet; 135import java.util.List; 136import java.util.Map; 137import java.util.Optional; 138import java.util.Set; 139import java.util.TreeSet; 140import java.util.concurrent.ConcurrentHashMap; 141import java.util.concurrent.CountDownLatch; 142import java.util.concurrent.TimeUnit; 143import java.util.regex.Pattern; 144import java.util.stream.Collectors; 145 146import static ca.uhn.fhir.util.HapiExtensions.EXTENSION_TRANSACTION_ENTRY_PARTITION_IDS; 147import static ca.uhn.fhir.util.StringUtil.toUtf8String; 148import static java.util.Objects.isNull; 149import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 150import static org.apache.commons.lang3.StringUtils.defaultString; 151import static org.apache.commons.lang3.StringUtils.isBlank; 152import static org.apache.commons.lang3.StringUtils.isNotBlank; 153 154public abstract class BaseTransactionProcessor { 155 156 public static final String URN_PREFIX = "urn:"; 157 public static final String URN_PREFIX_ESCAPED = UrlUtil.escapeUrlParam(URN_PREFIX); 158 public static final Pattern UNQUALIFIED_MATCH_URL_START = Pattern.compile("^[a-zA-Z0-9_-]+="); 159 public static final Pattern INVALID_PLACEHOLDER_PATTERN = Pattern.compile("[a-zA-Z]+:.*"); 160 161 private static final Logger ourLog = LoggerFactory.getLogger(BaseTransactionProcessor.class); 162 private static final String POST = "POST"; 163 private static final String PUT = "PUT"; 164 private static final String DELETE = "DELETE"; 165 private static final String PATCH = "PATCH"; 166 167 @Autowired 168 private IRequestPartitionHelperSvc myRequestPartitionHelperService; 169 170 @Autowired 171 private PlatformTransactionManager myTxManager; 172 173 @Autowired 174 private FhirContext myContext; 175 176 @Autowired 177 @SuppressWarnings("rawtypes") 178 private ITransactionProcessorVersionAdapter myVersionAdapter; 179 180 @Autowired 181 private DaoRegistry myDaoRegistry; 182 183 @Autowired 184 private IInterceptorBroadcaster myInterceptorBroadcaster; 185 186 @Autowired 187 private IHapiTransactionService myHapiTransactionService; 188 189 @Autowired 190 private StorageSettings myStorageSettings; 191 192 @Autowired 193 PartitionSettings myPartitionSettings; 194 195 @Autowired 196 private InMemoryResourceMatcher myInMemoryResourceMatcher; 197 198 @Autowired 199 private SearchParamMatcher mySearchParamMatcher; 200 201 @Autowired 202 private ThreadPoolFactory myThreadPoolFactory; 203 204 private TaskExecutor myExecutor; 205 206 @Autowired 207 private IResourceVersionSvc myResourceVersionSvc; 208 209 @VisibleForTesting 210 public void setStorageSettings(StorageSettings theStorageSettings) { 211 myStorageSettings = theStorageSettings; 212 } 213 214 public ITransactionProcessorVersionAdapter getVersionAdapter() { 215 return myVersionAdapter; 216 } 217 218 @VisibleForTesting 219 public void setVersionAdapter(ITransactionProcessorVersionAdapter theVersionAdapter) { 220 myVersionAdapter = theVersionAdapter; 221 } 222 223 private TaskExecutor getTaskExecutor() { 224 if (myExecutor == null) { 225 myExecutor = myThreadPoolFactory.newThreadPool( 226 myStorageSettings.getBundleBatchPoolSize(), 227 myStorageSettings.getBundleBatchMaxPoolSize(), 228 "bundle-batch-"); 229 } 230 return myExecutor; 231 } 232 233 public <BUNDLE extends IBaseBundle> BUNDLE transaction( 234 RequestDetails theRequestDetails, BUNDLE theRequest, boolean theNestedMode) { 235 String actionName = "Transaction"; 236 IBaseBundle response = processTransactionAsSubRequest(theRequestDetails, theRequest, actionName, theNestedMode); 237 238 List<IBase> entries = myVersionAdapter.getEntries(response); 239 for (int i = 0; i < entries.size(); i++) { 240 if (ElementUtil.isEmpty(entries.get(i))) { 241 entries.remove(i); 242 i--; 243 } 244 } 245 246 return (BUNDLE) response; 247 } 248 249 public IBaseBundle collection(final RequestDetails theRequestDetails, IBaseBundle theRequest) { 250 String transactionType = myVersionAdapter.getBundleType(theRequest); 251 252 if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) { 253 throw new InvalidRequestException( 254 Msg.code(526) + "Can not process collection Bundle of type: " + transactionType); 255 } 256 257 ourLog.info( 258 "Beginning storing collection with {} resources", 259 myVersionAdapter.getEntries(theRequest).size()); 260 261 TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); 262 txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 263 264 IBaseBundle resp = 265 myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); 266 267 List<IBaseResource> resources = new ArrayList<>(); 268 for (final Object nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { 269 IBaseResource resource = myVersionAdapter.getResource((IBase) nextRequestEntry); 270 resources.add(resource); 271 } 272 273 IBaseBundle transactionBundle = myVersionAdapter.createBundle("transaction"); 274 for (IBaseResource next : resources) { 275 IBase entry = myVersionAdapter.addEntry(transactionBundle); 276 myVersionAdapter.setResource(entry, next); 277 myVersionAdapter.setRequestVerb(entry, "PUT"); 278 myVersionAdapter.setRequestUrl( 279 entry, next.getIdElement().toUnqualifiedVersionless().getValue()); 280 } 281 282 transaction(theRequestDetails, transactionBundle, false); 283 284 return resp; 285 } 286 287 @SuppressWarnings("unchecked") 288 private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, IBase nextEntry) { 289 myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); 290 } 291 292 @SuppressWarnings("unchecked") 293 private void handleTransactionCreateOrUpdateOutcome( 294 IdSubstitutionMap theIdSubstitutions, 295 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 296 IIdType theNextResourceId, 297 DaoMethodOutcome theOutcome, 298 IBase theNewEntry, 299 String theResourceType, 300 IBaseResource theRes, 301 RequestDetails theRequestDetails) { 302 IIdType newId = theOutcome.getId().toUnqualified(); 303 IIdType resourceId = 304 isPlaceholder(theNextResourceId) ? theNextResourceId : theNextResourceId.toUnqualifiedVersionless(); 305 if (!newId.equals(resourceId)) { 306 if (!theNextResourceId.isEmpty()) { 307 theIdSubstitutions.put(resourceId, newId); 308 } 309 if (isPlaceholder(resourceId)) { 310 /* 311 * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. 312 */ 313 IIdType id = myContext.getVersion().newIdType(); 314 id.setValue(theResourceType + '/' + resourceId.getValue()); 315 theIdSubstitutions.put(id, newId); 316 } 317 } 318 319 populateIdToPersistedOutcomeMap(theIdToPersistedOutcome, newId, theOutcome); 320 321 if (shouldSwapBinaryToActualResource(theRes, theResourceType, theNextResourceId)) { 322 theRes = theIdToPersistedOutcome.get(newId).getResource(); 323 } 324 325 if (theOutcome.getCreated()) { 326 myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_201_CREATED)); 327 } else { 328 myVersionAdapter.setResponseStatus(theNewEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); 329 } 330 331 Date lastModified = getLastModified(theRes); 332 myVersionAdapter.setResponseLastModified(theNewEntry, lastModified); 333 334 if (theOutcome.getOperationOutcome() != null) { 335 myVersionAdapter.setResponseOutcome(theNewEntry, theOutcome.getOperationOutcome()); 336 } 337 338 if (theRequestDetails != null) { 339 String prefer = theRequestDetails.getHeader(Constants.HEADER_PREFER); 340 PreferReturnEnum preferReturn = 341 RestfulServerUtils.parsePreferHeader(null, prefer).getReturn(); 342 if (preferReturn != null) { 343 if (preferReturn == PreferReturnEnum.REPRESENTATION) { 344 if (theOutcome.getResource() != null) { 345 theOutcome.fireResourceViewCallbacks(); 346 myVersionAdapter.setResource(theNewEntry, theOutcome.getResource()); 347 } 348 } 349 } 350 } 351 } 352 353 /** 354 * Method which populates entry in idToPersistedOutcome. 355 * Will store whatever outcome is sent, unless the key already exists, then we only replace an instance if we find that the instance 356 * we are replacing with is non-lazy. This allows us to evaluate later more easily, as we _know_ we need access to these. 357 */ 358 private void populateIdToPersistedOutcomeMap( 359 Map<IIdType, DaoMethodOutcome> idToPersistedOutcome, IIdType newId, DaoMethodOutcome outcome) { 360 // Prefer real method outcomes over lazy ones. 361 if (idToPersistedOutcome.containsKey(newId)) { 362 if (!(outcome instanceof LazyDaoMethodOutcome)) { 363 idToPersistedOutcome.put(newId, outcome); 364 } 365 } else { 366 idToPersistedOutcome.put(newId, outcome); 367 } 368 } 369 370 private Date getLastModified(IBaseResource theRes) { 371 return theRes.getMeta().getLastUpdated(); 372 } 373 374 private IBaseBundle processTransactionAsSubRequest( 375 RequestDetails theRequestDetails, IBaseBundle theRequest, String theActionName, boolean theNestedMode) { 376 BaseStorageDao.markRequestAsProcessingSubRequest(theRequestDetails); 377 try { 378 379 // Interceptor call: STORAGE_TRANSACTION_PROCESSING 380 IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster( 381 myInterceptorBroadcaster, theRequestDetails); 382 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_PROCESSING)) { 383 HookParams params = new HookParams() 384 .add(RequestDetails.class, theRequestDetails) 385 .addIfMatchesType(ServletRequestDetails.class, theRequest) 386 .add(IBaseBundle.class, theRequest); 387 compositeBroadcaster.callHooks(Pointcut.STORAGE_TRANSACTION_PROCESSING, params); 388 } 389 390 return processTransaction(theRequestDetails, theRequest, theActionName, theNestedMode); 391 } finally { 392 BaseStorageDao.clearRequestAsProcessingSubRequest(theRequestDetails); 393 } 394 } 395 396 @VisibleForTesting 397 public void setTxManager(PlatformTransactionManager theTxManager) { 398 myTxManager = theTxManager; 399 } 400 401 private IBaseBundle batch(final RequestDetails theRequestDetails, IBaseBundle theRequest, boolean theNestedMode) { 402 TransactionSemanticsHeader transactionSemantics = TransactionSemanticsHeader.DEFAULT; 403 if (theRequestDetails != null) { 404 String transactionSemanticsString = theRequestDetails.getHeader("X-Transaction-Semantics"); 405 if (transactionSemanticsString != null) { 406 transactionSemantics = TransactionSemanticsHeader.parse(transactionSemanticsString); 407 } 408 } 409 410 int totalAttempts = defaultIfNull(transactionSemantics.getRetryCount(), 0) + 1; 411 boolean switchedToBatch = false; 412 413 int minRetryDelay = defaultIfNull(transactionSemantics.getMinRetryDelay(), 0); 414 int maxRetryDelay = defaultIfNull(transactionSemantics.getMaxRetryDelay(), 0); 415 416 // Don't let the user request a crazy delay, this could be used 417 // as a DOS attack 418 maxRetryDelay = Math.max(maxRetryDelay, minRetryDelay); 419 maxRetryDelay = Math.min(maxRetryDelay, 10000); 420 totalAttempts = Math.min(totalAttempts, 5); 421 422 IBaseBundle response; 423 for (int i = 1; ; i++) { 424 try { 425 if (i < totalAttempts && transactionSemantics.isTryBatchAsTransactionFirst()) { 426 BundleUtil.setBundleType(myContext, theRequest, "transaction"); 427 response = processTransaction(theRequestDetails, theRequest, "Transaction", theNestedMode); 428 } else { 429 BundleUtil.setBundleType(myContext, theRequest, "batch"); 430 response = processBatch(theRequestDetails, theRequest, theNestedMode); 431 } 432 break; 433 } catch (BaseServerResponseException e) { 434 if (i >= totalAttempts || theNestedMode) { 435 throw e; 436 } 437 438 long delay = RandomUtils.insecure().randomLong(minRetryDelay, maxRetryDelay); 439 String sleepMessage = ""; 440 if (delay > 0) { 441 sleepMessage = "Sleeping for " + delay + " ms. "; 442 } 443 444 String switchedToBatchMessage = ""; 445 if (switchedToBatch) { 446 switchedToBatchMessage = " (as batch)"; 447 } 448 449 String switchingToBatchMessage = ""; 450 if (i + 1 == totalAttempts && transactionSemantics.isTryBatchAsTransactionFirst()) { 451 switchingToBatchMessage = "Performing final retry using batch semantics. "; 452 switchedToBatch = true; 453 BundleUtil.setBundleType(myContext, theRequest, "batch"); 454 } 455 456 ourLog.warn( 457 "Transaction processing attempt{} {}/{} failed. {}{}", 458 switchedToBatchMessage, 459 i, 460 totalAttempts, 461 sleepMessage, 462 switchingToBatchMessage); 463 464 if (delay > 0) { 465 ThreadUtils.sleepQuietly(Duration.ofMillis(delay)); 466 } 467 } 468 } 469 470 return response; 471 } 472 473 private IBaseBundle processBatch(RequestDetails theRequestDetails, IBaseBundle theRequest, boolean theNestedMode) { 474 ourLog.info( 475 "Beginning batch with {} resources", 476 myVersionAdapter.getEntries(theRequest).size()); 477 478 long start = System.currentTimeMillis(); 479 480 IBaseBundle response = 481 myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); 482 Map<Integer, Object> responseMap = new ConcurrentHashMap<>(); 483 484 List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest); 485 int requestEntriesSize = requestEntries.size(); 486 487 // Now, run all non-gets sequentially, and all gets are submitted to the executor to run (potentially) in 488 // parallel 489 // The result is kept in the map to save the original position 490 List<RetriableBundleTask> getCalls = new ArrayList<>(); 491 List<RetriableBundleTask> nonGetCalls = new ArrayList<>(); 492 493 CountDownLatch completionLatch = new CountDownLatch(requestEntriesSize); 494 for (int i = 0; i < requestEntriesSize; i++) { 495 IBase nextRequestEntry = requestEntries.get(i); 496 RetriableBundleTask retriableBundleTask = new RetriableBundleTask( 497 completionLatch, theRequestDetails, responseMap, i, nextRequestEntry, theNestedMode); 498 if (myVersionAdapter 499 .getEntryRequestVerb(myContext, nextRequestEntry) 500 .equalsIgnoreCase("GET")) { 501 getCalls.add(retriableBundleTask); 502 } else { 503 nonGetCalls.add(retriableBundleTask); 504 } 505 } 506 // Execute all non-gets on calling thread. 507 nonGetCalls.forEach(RetriableBundleTask::run); 508 // Execute all gets (potentially in a pool) 509 if (myStorageSettings.getBundleBatchPoolSize() == 1) { 510 getCalls.forEach(RetriableBundleTask::run); 511 } else { 512 getCalls.forEach(getCall -> getTaskExecutor().execute(getCall)); 513 } 514 515 // waiting for all async tasks to be completed 516 AsyncUtil.awaitLatchAndIgnoreInterrupt(completionLatch, 300L, TimeUnit.SECONDS); 517 518 // Now, create the bundle response in original order 519 Object nextResponseEntry; 520 for (int i = 0; i < requestEntriesSize; i++) { 521 522 nextResponseEntry = responseMap.get(i); 523 if (nextResponseEntry instanceof ServerResponseExceptionHolder) { 524 ServerResponseExceptionHolder caughtEx = (ServerResponseExceptionHolder) nextResponseEntry; 525 if (caughtEx.getException() != null) { 526 IBase nextEntry = myVersionAdapter.addEntry(response); 527 populateEntryWithOperationOutcome(caughtEx.getException(), nextEntry); 528 myVersionAdapter.setResponseStatus( 529 nextEntry, toStatusString(caughtEx.getException().getStatusCode())); 530 } 531 } else { 532 myVersionAdapter.addEntry(response, (IBase) nextResponseEntry); 533 } 534 } 535 536 long delay = System.currentTimeMillis() - start; 537 ourLog.info("Batch completed in {}ms", delay); 538 539 return response; 540 } 541 542 @VisibleForTesting 543 public void setHapiTransactionService(HapiTransactionService theHapiTransactionService) { 544 myHapiTransactionService = theHapiTransactionService; 545 } 546 547 private IBaseBundle processTransaction( 548 final RequestDetails theRequestDetails, 549 final IBaseBundle theRequest, 550 final String theActionName, 551 boolean theNestedMode) { 552 validateDependencies(); 553 554 String transactionType = myVersionAdapter.getBundleType(theRequest); 555 556 if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) { 557 return batch(theRequestDetails, theRequest, theNestedMode); 558 } 559 560 if (transactionType == null) { 561 String message = "Transaction Bundle did not specify valid Bundle.type, assuming " 562 + Bundle.BundleType.TRANSACTION.toCode(); 563 ourLog.warn(message); 564 transactionType = org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode(); 565 } 566 if (!org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode().equals(transactionType)) { 567 throw new InvalidRequestException( 568 Msg.code(527) + "Unable to process transaction where incoming Bundle.type = " + transactionType); 569 } 570 571 List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest); 572 int numberOfEntries = requestEntries.size(); 573 574 if (myStorageSettings.getMaximumTransactionBundleSize() != null 575 && numberOfEntries > myStorageSettings.getMaximumTransactionBundleSize()) { 576 throw new PayloadTooLargeException( 577 Msg.code(528) + "Transaction Bundle Too large. Transaction bundle contains " + numberOfEntries 578 + " which exceedes the maximum permitted transaction bundle size of " 579 + myStorageSettings.getMaximumTransactionBundleSize()); 580 } 581 582 ourLog.debug("Beginning {} with {} resources", theActionName, numberOfEntries); 583 584 final TransactionDetails transactionDetails = new TransactionDetails(); 585 transactionDetails.setFhirTransaction(true); 586 final StopWatch transactionStopWatch = new StopWatch(); 587 588 // Do all entries have a verb? 589 for (int i = 0; i < numberOfEntries; i++) { 590 IBase nextReqEntry = requestEntries.get(i); 591 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry); 592 if (verb == null || !isValidVerb(verb)) { 593 throw new InvalidRequestException(Msg.code(529) 594 + myContext 595 .getLocalizer() 596 .getMessage(BaseStorageDao.class, "transactionEntryHasInvalidVerb", verb, i)); 597 } 598 } 599 600 /* 601 * We want to execute the transaction request bundle elements in the order 602 * specified by the FHIR specification (see TransactionSorter) so we save the 603 * original order in the request, then sort it. 604 * 605 * Entries with a type of GET are removed from the bundle so that they 606 * can be processed at the very end. We do this because the incoming resources 607 * are saved in a two-phase way in order to deal with interdependencies, and 608 * we want the GET processing to use the final indexing state 609 */ 610 final IBaseBundle response = 611 myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTIONRESPONSE.toCode()); 612 List<IBase> getEntries = new ArrayList<>(); 613 final IdentityHashMap<IBase, Integer> originalRequestOrder = new IdentityHashMap<>(); 614 for (int i = 0; i < requestEntries.size(); i++) { 615 IBase requestEntry = requestEntries.get(i); 616 originalRequestOrder.put(requestEntry, i); 617 myVersionAdapter.addEntry(response); 618 if (myVersionAdapter.getEntryRequestVerb(myContext, requestEntry).equals("GET")) { 619 getEntries.add(requestEntry); 620 } 621 } 622 623 /* 624 * See FhirSystemDaoDstu3Test#testTransactionWithPlaceholderIdInMatchUrl 625 * Basically if the resource has a match URL that references a placeholder, 626 * we try to handle the resource with the placeholder first. 627 */ 628 Set<String> placeholderIds = new HashSet<>(); 629 for (IBase nextEntry : requestEntries) { 630 String fullUrl = myVersionAdapter.getFullUrl(nextEntry); 631 if (isNotBlank(fullUrl) && fullUrl.startsWith(URN_PREFIX)) { 632 placeholderIds.add(fullUrl); 633 } 634 } 635 requestEntries.sort(new TransactionSorter(placeholderIds)); 636 637 // perform all writes 638 prepareThenExecuteTransactionWriteOperations( 639 theRequestDetails, 640 theActionName, 641 transactionDetails, 642 transactionStopWatch, 643 response, 644 originalRequestOrder, 645 requestEntries); 646 647 // perform all gets 648 // (we do these last so that the gets happen on the final state of the DB; 649 // see above note) 650 doTransactionReadOperations( 651 theRequestDetails, response, getEntries, originalRequestOrder, transactionStopWatch, theNestedMode); 652 653 // Interceptor broadcast: JPA_PERFTRACE_INFO 654 IInterceptorBroadcaster compositeBroadcaster = 655 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 656 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) { 657 String taskDurations = transactionStopWatch.formatTaskDurations(); 658 StorageProcessingMessage message = new StorageProcessingMessage(); 659 message.setMessage("Transaction timing:\n" + taskDurations); 660 HookParams params = new HookParams() 661 .add(RequestDetails.class, theRequestDetails) 662 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 663 .add(StorageProcessingMessage.class, message); 664 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 665 } 666 667 return response; 668 } 669 670 @SuppressWarnings("unchecked") 671 private void doTransactionReadOperations( 672 final RequestDetails theRequestDetails, 673 IBaseBundle theResponse, 674 List<IBase> theGetEntries, 675 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 676 StopWatch theTransactionStopWatch, 677 boolean theNestedMode) { 678 if (theGetEntries.size() > 0) { 679 theTransactionStopWatch.startTask("Process " + theGetEntries.size() + " GET entries"); 680 681 /* 682 * Loop through the request and process any entries of type GET 683 */ 684 for (IBase nextReqEntry : theGetEntries) { 685 if (theNestedMode) { 686 throw new InvalidRequestException( 687 Msg.code(530) + "Can not invoke read operation on nested transaction"); 688 } 689 690 if (!(theRequestDetails instanceof ServletRequestDetails)) { 691 throw new MethodNotAllowedException( 692 Msg.code(531) + "Can not call transaction GET methods from this context"); 693 } 694 695 ServletRequestDetails srd = (ServletRequestDetails) theRequestDetails; 696 Integer originalOrder = theOriginalRequestOrder.get(nextReqEntry); 697 IBase nextRespEntry = 698 (IBase) myVersionAdapter.getEntries(theResponse).get(originalOrder); 699 700 ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create(); 701 ServletSubRequestDetails requestDetailsForEntry = 702 createRequestDetailsForReadEntry(srd, nextReqEntry, paramValues); 703 704 String url = requestDetailsForEntry.getRequestPath(); 705 706 BaseMethodBinding method = srd.getServer().determineResourceMethod(requestDetailsForEntry, url); 707 if (method == null) { 708 throw new IllegalArgumentException(Msg.code(532) + "Unable to handle GET " + url); 709 } 710 711 Validate.isTrue(method instanceof BaseResourceReturningMethodBinding, "Unable to handle GET {}", url); 712 try { 713 BaseResourceReturningMethodBinding methodBinding = (BaseResourceReturningMethodBinding) method; 714 requestDetailsForEntry.setRestOperationType(methodBinding.getRestOperationType()); 715 716 IBaseResource resource = methodBinding.doInvokeServer(srd.getServer(), requestDetailsForEntry); 717 if (paramValues.containsKey(Constants.PARAM_SUMMARY) 718 || paramValues.containsKey(Constants.PARAM_CONTENT)) { 719 resource = filterNestedBundle(requestDetailsForEntry, resource); 720 } 721 myVersionAdapter.setResource(nextRespEntry, resource); 722 myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(Constants.STATUS_HTTP_200_OK)); 723 } catch (NotModifiedException e) { 724 myVersionAdapter.setResponseStatus( 725 nextRespEntry, toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); 726 } catch (BaseServerResponseException e) { 727 ourLog.info("Failure processing transaction GET {}: {}", url, e.toString()); 728 myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(e.getStatusCode())); 729 populateEntryWithOperationOutcome(e, nextRespEntry); 730 } 731 } 732 theTransactionStopWatch.endCurrentTask(); 733 } 734 } 735 736 /** 737 * Checks if the given request entry has the extension specified to override the partition ids to use when processing that entry. 738 * If the extension is present, this method will set the partition ids header in the given request details. 739 */ 740 private void setRequestPartitionHeaderIfEntryHasTheExtension(IBase theReqEntry, RequestDetails theRequestDetails) { 741 Optional<IBaseExtension<?, ?>> partitionIdsExtensionOptional = 742 myVersionAdapter.getEntryRequestExtensionByUrl(theReqEntry, EXTENSION_TRANSACTION_ENTRY_PARTITION_IDS); 743 if (partitionIdsExtensionOptional.isPresent() 744 && partitionIdsExtensionOptional.get().getValue() instanceof IPrimitiveType<?>) { 745 IPrimitiveType<?> valueAsPrimitiveType = 746 (IPrimitiveType<?>) partitionIdsExtensionOptional.get().getValue(); 747 String value = valueAsPrimitiveType.getValueAsString(); 748 theRequestDetails.setHeaders(Constants.HEADER_X_REQUEST_PARTITION_IDS, List.of(value)); 749 } 750 } 751 752 /** 753 * Creates a new RequestDetails object based on the given RequestDetails. The new RequestDetails is to be used 754 * when processing a write entry. 755 * If the entry.request has the extension to override the partition ids, this method 756 * sets the partition ids header in the newly created request details with the values from extension. 757 * This allows using different partitions for different entries in the same transaction. 758 * 759 * @return the newly created request details 760 */ 761 private RequestDetails createRequestDetailsForWriteEntry( 762 RequestDetails theRequestDetails, IBase theEntry, String theUrl, String theVerb) { 763 764 if (theRequestDetails == null) { 765 ourLog.warn( 766 "The RequestDetails passed in to the transaction is null. Cannot create a new RequestDetails for transaction entry."); 767 return null; 768 } 769 770 RequestDetails newRequestDetails; 771 if (theRequestDetails instanceof ServletRequestDetails) { 772 newRequestDetails = ServletRequestUtil.getServletSubRequestDetails( 773 (ServletRequestDetails) theRequestDetails, theUrl, theVerb, ArrayListMultimap.create()); 774 } else if (theRequestDetails instanceof SystemRequestDetails) { 775 newRequestDetails = new SystemRequestDetails((SystemRequestDetails) theRequestDetails); 776 } else { 777 // RequestDetails is not a ServletRequestDetails or SystemRequestDetails, and I don't know how to properly 778 // clone such a RequestDetails. Use the original RequestDetails without making any entry specific 779 // modifications 780 ourLog.warn( 781 "Cannot create a new RequestDetails for transaction entry out of the existing RequestDetails which is of type '{}'" 782 + "Using the original RequestDetails for transaction entries without any entry specific modifications.", 783 theRequestDetails.getClass().getSimpleName()); 784 return theRequestDetails; 785 } 786 setRequestPartitionHeaderIfEntryHasTheExtension(theEntry, newRequestDetails); 787 return newRequestDetails; 788 } 789 790 /** 791 * Creates a new RequestDetails based on the given one. The returned RequestDetails is to be used when processing 792 * a GET entry of transaction. 793 * It sets the headers in the newly request details according to the information from entry.request if needed. 794 * Currently, GET entries only support ServletRequestDetails so it handles only that type. 795 */ 796 private ServletSubRequestDetails createRequestDetailsForReadEntry( 797 ServletRequestDetails theRequestDetails, IBase theEntry, ArrayListMultimap<String, String> theParamValues) { 798 799 final String verb = "GET"; 800 String transactionUrl = extractTransactionUrlOrThrowException(theEntry, verb); 801 802 ServletSubRequestDetails subRequestDetails = 803 ServletRequestUtil.getServletSubRequestDetails(theRequestDetails, transactionUrl, verb, theParamValues); 804 805 if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(theEntry))) { 806 subRequestDetails.addHeader(Constants.HEADER_IF_MATCH, myVersionAdapter.getEntryRequestIfMatch(theEntry)); 807 } 808 if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneExist(theEntry))) { 809 subRequestDetails.addHeader( 810 Constants.HEADER_IF_NONE_EXIST, myVersionAdapter.getEntryRequestIfNoneExist(theEntry)); 811 } 812 if (isNotBlank(myVersionAdapter.getEntryRequestIfNoneMatch(theEntry))) { 813 subRequestDetails.addHeader( 814 Constants.HEADER_IF_NONE_MATCH, myVersionAdapter.getEntryRequestIfNoneMatch(theEntry)); 815 } 816 817 setRequestPartitionHeaderIfEntryHasTheExtension(theEntry, subRequestDetails); 818 819 return subRequestDetails; 820 } 821 822 /** 823 * All of the write operations in the transaction (PUT, POST, etc.. basically anything 824 * except GET) are performed in their own database transaction before we do the reads. 825 * We do this because the reads (specifically the searches) often spawn their own 826 * secondary database transaction and if we allow that within the primary 827 * database transaction we can end up with deadlocks if the server is under 828 * heavy load with lots of concurrent transactions using all available 829 * database connections. 830 */ 831 @SuppressWarnings("unchecked") 832 private void prepareThenExecuteTransactionWriteOperations( 833 RequestDetails theRequestDetails, 834 String theActionName, 835 TransactionDetails theTransactionDetails, 836 StopWatch theTransactionStopWatch, 837 IBaseBundle theResponse, 838 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 839 List<IBase> theEntries) { 840 841 TransactionWriteOperationsDetails writeOperationsDetails = null; 842 if (haveWriteOperationsHooks(theRequestDetails)) { 843 writeOperationsDetails = buildWriteOperationsDetails(theEntries); 844 callWriteOperationsHook( 845 Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, 846 theRequestDetails, 847 theTransactionDetails, 848 writeOperationsDetails); 849 } 850 851 TransactionCallback<EntriesToProcessMap> txCallback = status -> { 852 final Set<IIdType> allIds = new LinkedHashSet<>(); 853 final IdSubstitutionMap idSubstitutions = new IdSubstitutionMap(); 854 final Map<IIdType, DaoMethodOutcome> idToPersistedOutcome = new LinkedHashMap<>(); 855 856 EntriesToProcessMap retVal = doTransactionWriteOperations( 857 theRequestDetails, 858 theActionName, 859 theTransactionDetails, 860 allIds, 861 idSubstitutions, 862 idToPersistedOutcome, 863 theResponse, 864 theOriginalRequestOrder, 865 theEntries, 866 theTransactionStopWatch); 867 868 theTransactionStopWatch.startTask("Commit writes to database"); 869 return retVal; 870 }; 871 EntriesToProcessMap entriesToProcess; 872 873 RequestPartitionId requestPartitionId = 874 determineRequestPartitionIdForWriteEntries(theRequestDetails, theEntries); 875 876 try { 877 entriesToProcess = myHapiTransactionService 878 .withRequest(theRequestDetails) 879 .withRequestPartitionId(requestPartitionId) 880 .withTransactionDetails(theTransactionDetails) 881 .execute(txCallback); 882 } finally { 883 if (haveWriteOperationsHooks(theRequestDetails)) { 884 callWriteOperationsHook( 885 Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, 886 theRequestDetails, 887 theTransactionDetails, 888 writeOperationsDetails); 889 } 890 } 891 892 theTransactionStopWatch.endCurrentTask(); 893 894 for (Map.Entry<IBase, IIdType> nextEntry : entriesToProcess.entrySet()) { 895 String responseLocation = nextEntry.getValue().toUnqualified().getValue(); 896 String responseEtag = nextEntry.getValue().getVersionIdPart(); 897 myVersionAdapter.setResponseLocation(nextEntry.getKey(), responseLocation); 898 myVersionAdapter.setResponseETag(nextEntry.getKey(), responseEtag); 899 } 900 } 901 902 /** 903 * This method looks at the FHIR actions being performed in a List of bundle entries, 904 * and determines the associated request partitions. 905 */ 906 @Nullable 907 protected RequestPartitionId determineRequestPartitionIdForWriteEntries( 908 RequestDetails theRequestDetails, List<IBase> theEntries) { 909 if (!myPartitionSettings.isPartitioningEnabled()) { 910 return RequestPartitionId.allPartitions(); 911 } 912 913 return theEntries.stream() 914 .map(e -> getEntryRequestPartitionId(theRequestDetails, e)) 915 .reduce(null, (accumulator, nextPartition) -> { 916 if (accumulator == null) { 917 return nextPartition; 918 } else if (nextPartition == null) { 919 return accumulator; 920 } else if (myHapiTransactionService.isCompatiblePartition(accumulator, nextPartition)) { 921 return accumulator.mergeIds(nextPartition); 922 } else { 923 String msg = myContext 924 .getLocalizer() 925 .getMessage( 926 BaseTransactionProcessor.class, "multiplePartitionAccesses", theEntries.size()); 927 throw new InvalidRequestException(Msg.code(2541) + msg); 928 } 929 }); 930 } 931 932 @Nullable 933 private RequestPartitionId getEntryRequestPartitionId(RequestDetails theRequestDetails, IBase nextEntry) { 934 935 RequestPartitionId nextWriteEntryRequestPartitionId = null; 936 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextEntry); 937 String url = extractTransactionUrlOrThrowException(nextEntry, verb); 938 RequestDetails requestDetailsForEntry = 939 createRequestDetailsForWriteEntry(theRequestDetails, nextEntry, url, verb); 940 if (isNotBlank(verb)) { 941 BundleEntryTransactionMethodEnum verbEnum = BundleEntryTransactionMethodEnum.valueOf(verb); 942 switch (verbEnum) { 943 case GET: 944 nextWriteEntryRequestPartitionId = null; 945 break; 946 case DELETE: { 947 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry); 948 if (isNotBlank(requestUrl)) { 949 IdType id = new IdType(requestUrl); 950 String resourceType = id.getResourceType(); 951 ReadPartitionIdRequestDetails details = 952 ReadPartitionIdRequestDetails.forDelete(resourceType, id); 953 nextWriteEntryRequestPartitionId = 954 myRequestPartitionHelperService.determineReadPartitionForRequest( 955 requestDetailsForEntry, details); 956 } 957 break; 958 } 959 case PATCH: { 960 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry); 961 if (isNotBlank(requestUrl)) { 962 IdType id = new IdType(requestUrl); 963 String resourceType = id.getResourceType(); 964 ReadPartitionIdRequestDetails details = 965 ReadPartitionIdRequestDetails.forPatch(resourceType, id); 966 nextWriteEntryRequestPartitionId = 967 myRequestPartitionHelperService.determineReadPartitionForRequest( 968 requestDetailsForEntry, details); 969 } 970 break; 971 } 972 case POST: { 973 IBaseResource resource = myVersionAdapter.getResource(nextEntry); 974 String resourceType = myContext.getResourceType(resource); 975 nextWriteEntryRequestPartitionId = 976 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 977 requestDetailsForEntry, resourceType); 978 break; 979 } 980 case PUT: { 981 IBaseResource resource = myVersionAdapter.getResource(nextEntry); 982 if (resource != null) { 983 String resourceType = myContext.getResourceType(resource); 984 nextWriteEntryRequestPartitionId = 985 myRequestPartitionHelperService.determineCreatePartitionForRequest( 986 requestDetailsForEntry, resource, resourceType); 987 } 988 break; 989 } 990 } 991 } 992 return nextWriteEntryRequestPartitionId; 993 } 994 995 private boolean haveWriteOperationsHooks(RequestDetails theRequestDetails) { 996 IInterceptorBroadcaster compositeBroadcaster = 997 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 998 return compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE) 999 || compositeBroadcaster.hasHooks(Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST); 1000 } 1001 1002 private void callWriteOperationsHook( 1003 Pointcut thePointcut, 1004 RequestDetails theRequestDetails, 1005 TransactionDetails theTransactionDetails, 1006 TransactionWriteOperationsDetails theWriteOperationsDetails) { 1007 IInterceptorBroadcaster compositeBroadcaster = 1008 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 1009 if (compositeBroadcaster.hasHooks(thePointcut)) { 1010 HookParams params = new HookParams() 1011 .add(TransactionDetails.class, theTransactionDetails) 1012 .add(TransactionWriteOperationsDetails.class, theWriteOperationsDetails); 1013 compositeBroadcaster.callHooks(thePointcut, params); 1014 } 1015 } 1016 1017 @SuppressWarnings("unchecked") 1018 private TransactionWriteOperationsDetails buildWriteOperationsDetails(List<IBase> theEntries) { 1019 TransactionWriteOperationsDetails writeOperationsDetails; 1020 List<String> updateRequestUrls = new ArrayList<>(); 1021 List<String> conditionalCreateRequestUrls = new ArrayList<>(); 1022 // Extract 1023 for (IBase nextEntry : theEntries) { 1024 String method = myVersionAdapter.getEntryRequestVerb(myContext, nextEntry); 1025 if ("PUT".equals(method)) { 1026 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry); 1027 if (isNotBlank(requestUrl)) { 1028 updateRequestUrls.add(requestUrl); 1029 } 1030 } else if ("POST".equals(method)) { 1031 String requestUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextEntry); 1032 if (isNotBlank(requestUrl) && requestUrl.contains("?")) { 1033 conditionalCreateRequestUrls.add(requestUrl); 1034 } 1035 } 1036 } 1037 1038 writeOperationsDetails = new TransactionWriteOperationsDetails(); 1039 writeOperationsDetails.setUpdateRequestUrls(updateRequestUrls); 1040 writeOperationsDetails.setConditionalCreateRequestUrls(conditionalCreateRequestUrls); 1041 return writeOperationsDetails; 1042 } 1043 1044 private boolean isValidVerb(String theVerb) { 1045 try { 1046 return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null; 1047 } catch (FHIRException theE) { 1048 return false; 1049 } 1050 } 1051 1052 /** 1053 * This method is called for nested bundles (e.g. if we received a transaction with an entry that 1054 * was a GET search, this method is called on the bundle for the search result, that will be placed in the 1055 * outer bundle). This method applies the _summary and _content parameters to the output of 1056 * that bundle. 1057 * <p> 1058 * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. 1059 */ 1060 private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { 1061 IParser p = myContext.newJsonParser(); 1062 RestfulServerUtils.configureResponseParser(theRequestDetails, p); 1063 return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); 1064 } 1065 1066 protected void validateDependencies() { 1067 Validate.notNull(myContext); 1068 Validate.notNull(myTxManager); 1069 } 1070 1071 private IIdType newIdType(String theValue) { 1072 return myContext.getVersion().newIdType().setValue(theValue); 1073 } 1074 1075 /** 1076 * Searches for duplicate conditional creates and consolidates them. 1077 */ 1078 @SuppressWarnings("unchecked") 1079 private void consolidateDuplicateConditionals( 1080 RequestDetails theRequestDetails, String theActionName, List<IBase> theEntries) { 1081 final Set<String> keysWithNoFullUrl = new HashSet<>(); 1082 final HashMap<String, String> keyToUuid = new HashMap<>(); 1083 1084 for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) { 1085 IBase nextReqEntry = theEntries.get(index); 1086 IBaseResource resource = myVersionAdapter.getResource(nextReqEntry); 1087 if (resource != null) { 1088 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry); 1089 String entryFullUrl = myVersionAdapter.getFullUrl(nextReqEntry); 1090 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry); 1091 String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); 1092 1093 // Conditional UPDATE 1094 boolean consolidateEntryCandidate = false; 1095 String conditionalUrl; 1096 switch (verb) { 1097 case "PUT": 1098 conditionalUrl = requestUrl; 1099 if (isNotBlank(requestUrl)) { 1100 int questionMarkIndex = requestUrl.indexOf('?'); 1101 if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) { 1102 consolidateEntryCandidate = true; 1103 } 1104 } 1105 break; 1106 1107 // Conditional CREATE 1108 case "POST": 1109 conditionalUrl = ifNoneExist; 1110 if (isNotBlank(ifNoneExist)) { 1111 if (isBlank(entryFullUrl) || !entryFullUrl.equals(requestUrl)) { 1112 consolidateEntryCandidate = true; 1113 } 1114 } 1115 break; 1116 1117 default: 1118 continue; 1119 } 1120 1121 if (isNotBlank(conditionalUrl) && !conditionalUrl.contains("?")) { 1122 conditionalUrl = myContext.getResourceType(resource) + "?" + conditionalUrl; 1123 } 1124 1125 String key = verb + "|" + conditionalUrl; 1126 if (consolidateEntryCandidate) { 1127 if (isBlank(entryFullUrl)) { 1128 if (isNotBlank(conditionalUrl)) { 1129 if (!keysWithNoFullUrl.add(key)) { 1130 throw new InvalidRequestException(Msg.code(2008) + "Unable to process " + theActionName 1131 + " - Request contains multiple anonymous entries (Bundle.entry.fullUrl not populated) with conditional URL: \"" 1132 + UrlUtil.sanitizeUrlPart(conditionalUrl) 1133 + "\". Does transaction request contain duplicates?"); 1134 } 1135 } 1136 } else { 1137 if (!keyToUuid.containsKey(key)) { 1138 keyToUuid.put(key, entryFullUrl); 1139 } else { 1140 String msg = "Discarding transaction bundle entry " + originalIndex 1141 + " as it contained a duplicate conditional " + verb; 1142 ourLog.info(msg); 1143 // Interceptor broadcast: JPA_PERFTRACE_INFO 1144 IInterceptorBroadcaster compositeBroadcaster = 1145 CompositeInterceptorBroadcaster.newCompositeBroadcaster( 1146 myInterceptorBroadcaster, theRequestDetails); 1147 if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_WARNING)) { 1148 StorageProcessingMessage message = new StorageProcessingMessage().setMessage(msg); 1149 HookParams params = new HookParams() 1150 .add(RequestDetails.class, theRequestDetails) 1151 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1152 .add(StorageProcessingMessage.class, message); 1153 compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params); 1154 } 1155 1156 theEntries.remove(index); 1157 index--; 1158 String existingUuid = keyToUuid.get(key); 1159 replaceReferencesInEntriesWithConsolidatedUUID(theEntries, entryFullUrl, existingUuid); 1160 } 1161 } 1162 } 1163 } 1164 } 1165 } 1166 1167 /** 1168 * Iterates over all entries, and if it finds any which have references which match the fullUrl of the entry that was consolidated out 1169 * replace them with our new consolidated UUID 1170 */ 1171 private void replaceReferencesInEntriesWithConsolidatedUUID( 1172 List<IBase> theEntries, String theEntryFullUrl, String existingUuid) { 1173 for (IBase nextEntry : theEntries) { 1174 @SuppressWarnings("unchecked") 1175 IBaseResource nextResource = myVersionAdapter.getResource(nextEntry); 1176 if (nextResource != null) { 1177 for (IBaseReference nextReference : 1178 myContext.newTerser().getAllPopulatedChildElementsOfType(nextResource, IBaseReference.class)) { 1179 // We're interested in any references directly to the placeholder ID, but also 1180 // references that have a resource target that has the placeholder ID. 1181 String nextReferenceId = nextReference.getReferenceElement().getValue(); 1182 if (isBlank(nextReferenceId) && nextReference.getResource() != null) { 1183 nextReferenceId = 1184 nextReference.getResource().getIdElement().getValue(); 1185 } 1186 if (theEntryFullUrl.equals(nextReferenceId)) { 1187 nextReference.setReference(existingUuid); 1188 nextReference.setResource(null); 1189 } 1190 } 1191 } 1192 } 1193 } 1194 1195 /** 1196 * Retrieves the next resource id (IIdType) from the base resource and next request entry. 1197 * 1198 * @param theBaseResource - base resource 1199 * @param theNextReqEntry - next request entry 1200 * @param theAllIds - set of all IIdType values 1201 * @param theVerb 1202 * @return 1203 */ 1204 private IIdType getNextResourceIdFromBaseResource( 1205 IBaseResource theBaseResource, IBase theNextReqEntry, Set<IIdType> theAllIds, String theVerb) { 1206 IIdType nextResourceId = null; 1207 if (theBaseResource != null) { 1208 nextResourceId = theBaseResource.getIdElement(); 1209 1210 @SuppressWarnings("unchecked") 1211 String fullUrl = myVersionAdapter.getFullUrl(theNextReqEntry); 1212 if (isNotBlank(fullUrl)) { 1213 IIdType fullUrlIdType = newIdType(fullUrl); 1214 if (isPlaceholder(fullUrlIdType)) { 1215 nextResourceId = fullUrlIdType; 1216 } else if (!nextResourceId.hasIdPart()) { 1217 nextResourceId = fullUrlIdType; 1218 } 1219 } 1220 1221 if (nextResourceId.hasIdPart() && !isPlaceholder(nextResourceId)) { 1222 int colonIndex = nextResourceId.getIdPart().indexOf(':'); 1223 if (colonIndex != -1) { 1224 if (INVALID_PLACEHOLDER_PATTERN 1225 .matcher(nextResourceId.getIdPart()) 1226 .matches()) { 1227 throw new InvalidRequestException( 1228 Msg.code(533) + "Invalid placeholder ID found: " + nextResourceId.getIdPart() 1229 + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); 1230 } 1231 } 1232 } 1233 1234 if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { 1235 nextResourceId = newIdType(toResourceName(theBaseResource.getClass()), nextResourceId.getIdPart()); 1236 theBaseResource.setId(nextResourceId); 1237 } 1238 1239 /* 1240 * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness 1241 */ 1242 if (isPlaceholder(nextResourceId)) { 1243 if (!theAllIds.add(nextResourceId)) { 1244 throw new InvalidRequestException(Msg.code(534) 1245 + myContext 1246 .getLocalizer() 1247 .getMessage( 1248 BaseStorageDao.class, 1249 "transactionContainsMultipleWithDuplicateId", 1250 nextResourceId)); 1251 } 1252 } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart() && !"POST".equals(theVerb)) { 1253 IIdType nextId = nextResourceId.toUnqualifiedVersionless(); 1254 if (!theAllIds.add(nextId)) { 1255 throw new InvalidRequestException(Msg.code(535) 1256 + myContext 1257 .getLocalizer() 1258 .getMessage( 1259 BaseStorageDao.class, 1260 "transactionContainsMultipleWithDuplicateId", 1261 nextId)); 1262 } 1263 } 1264 } 1265 1266 return nextResourceId; 1267 } 1268 1269 /** 1270 * After pre-hooks have been called 1271 */ 1272 @SuppressWarnings({"unchecked", "rawtypes"}) 1273 protected EntriesToProcessMap doTransactionWriteOperations( 1274 final RequestDetails theRequest, 1275 String theActionName, 1276 TransactionDetails theTransactionDetails, 1277 Set<IIdType> theAllIds, 1278 IdSubstitutionMap theIdSubstitutions, 1279 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1280 IBaseBundle theResponse, 1281 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 1282 List<IBase> theEntries, 1283 StopWatch theTransactionStopWatch) { 1284 1285 // During a transaction, we don't execute hooks, instead, we execute them all post-transaction. 1286 theTransactionDetails.beginAcceptingDeferredInterceptorBroadcasts( 1287 Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, 1288 Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, 1289 Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED); 1290 try { 1291 Set<String> deletedResources = new HashSet<>(); 1292 DeleteConflictList deleteConflicts = new DeleteConflictList(); 1293 EntriesToProcessMap entriesToProcess = new EntriesToProcessMap(); 1294 Set<IIdType> nonUpdatedEntities = new HashSet<>(); 1295 Set<IBasePersistedResource> updatedEntities = new HashSet<>(); 1296 Map<String, IIdType> conditionalUrlToIdMap = new HashMap<>(); 1297 List<IBaseResource> updatedResources = new ArrayList<>(); 1298 Map<String, Class<? extends IBaseResource>> conditionalRequestUrls = new HashMap<>(); 1299 1300 /* 1301 * Look for duplicate conditional creates and consolidate them 1302 */ 1303 consolidateDuplicateConditionals(theRequest, theActionName, theEntries); 1304 1305 /* 1306 * Loop through the request and process any entries of type 1307 * PUT, POST or DELETE 1308 */ 1309 String previousVerb = null; 1310 for (int i = 0; i < theEntries.size(); i++) { 1311 if (i % 250 == 0) { 1312 ourLog.debug("Processed {} non-GET entries out of {} in transaction", i, theEntries.size()); 1313 } 1314 1315 IBase nextReqEntry = theEntries.get(i); 1316 1317 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry); 1318 1319 if (previousVerb != null && !previousVerb.equals(verb)) { 1320 handleVerbChangeInTransactionWriteOperations(); 1321 } 1322 previousVerb = verb; 1323 if ("GET".equals(verb)) { 1324 continue; 1325 } 1326 1327 String url = extractAndVerifyTransactionUrlForEntry(nextReqEntry, verb); 1328 RequestDetails requestDetailsForEntry = 1329 createRequestDetailsForWriteEntry(theRequest, nextReqEntry, url, verb); 1330 1331 IBaseResource res = myVersionAdapter.getResource(nextReqEntry); 1332 IIdType nextResourceId = getNextResourceIdFromBaseResource(res, nextReqEntry, theAllIds, verb); 1333 1334 String resourceType = res != null ? myContext.getResourceType(res) : null; 1335 Integer order = theOriginalRequestOrder.get(nextReqEntry); 1336 IBase nextRespEntry = 1337 (IBase) myVersionAdapter.getEntries(theResponse).get(order); 1338 1339 theTransactionStopWatch.startTask( 1340 "Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType)); 1341 1342 if (res != null) { 1343 String previousResourceId = res.getIdElement().getValue(); 1344 theTransactionDetails.addRollbackUndoAction(() -> res.setId(previousResourceId)); 1345 } 1346 1347 switch (verb) { 1348 case "POST": { 1349 // CREATE 1350 validateResourcePresent(res, order, verb); 1351 1352 IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); 1353 res.setId((String) null); 1354 1355 DaoMethodOutcome outcome; 1356 String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); 1357 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1358 // create individual resource 1359 outcome = 1360 resourceDao.create(res, matchUrl, false, requestDetailsForEntry, theTransactionDetails); 1361 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1362 res.setId(outcome.getId()); 1363 1364 if (nextResourceId != null) { 1365 handleTransactionCreateOrUpdateOutcome( 1366 theIdSubstitutions, 1367 theIdToPersistedOutcome, 1368 nextResourceId, 1369 outcome, 1370 nextRespEntry, 1371 resourceType, 1372 res, 1373 requestDetailsForEntry); 1374 } 1375 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1376 theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); 1377 if (outcome.getCreated() == false) { 1378 nonUpdatedEntities.add(outcome.getId()); 1379 } else { 1380 if (isNotBlank(matchUrl)) { 1381 conditionalRequestUrls.put(matchUrl, res.getClass()); 1382 } 1383 } 1384 1385 break; 1386 } 1387 case "DELETE": { 1388 // DELETE 1389 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1390 IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url); 1391 int status = Constants.STATUS_HTTP_204_NO_CONTENT; 1392 if (parts.getResourceId() != null) { 1393 IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId()); 1394 if (!deletedResources.contains(deleteId.getValueAsString())) { 1395 DaoMethodOutcome outcome = dao.delete( 1396 deleteId, deleteConflicts, requestDetailsForEntry, theTransactionDetails); 1397 if (outcome.getEntity() != null) { 1398 deletedResources.add(deleteId.getValueAsString()); 1399 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1400 } 1401 myVersionAdapter.setResponseOutcome(nextRespEntry, outcome.getOperationOutcome()); 1402 } 1403 } else { 1404 String matchUrl = parts.getResourceType() + '?' + parts.getParams(); 1405 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1406 DeleteMethodOutcome deleteOutcome = dao.deleteByUrl( 1407 matchUrl, deleteConflicts, requestDetailsForEntry, theTransactionDetails); 1408 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, deleteOutcome.getId()); 1409 List<? extends IBasePersistedResource> allDeleted = deleteOutcome.getDeletedEntities(); 1410 for (IBasePersistedResource deleted : allDeleted) { 1411 deletedResources.add(deleted.getIdDt() 1412 .toUnqualifiedVersionless() 1413 .getValueAsString()); 1414 } 1415 if (allDeleted.isEmpty()) { 1416 status = Constants.STATUS_HTTP_204_NO_CONTENT; 1417 } 1418 1419 myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome()); 1420 } 1421 1422 myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status)); 1423 1424 break; 1425 } 1426 case "PUT": { 1427 // UPDATE 1428 validateResourcePresent(res, order, verb); 1429 @SuppressWarnings("rawtypes") 1430 IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); 1431 1432 DaoMethodOutcome outcome; 1433 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1434 if (isNotBlank(parts.getResourceId())) { 1435 String version = null; 1436 if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { 1437 version = ParameterUtil.parseETagValue( 1438 myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); 1439 } 1440 res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); 1441 outcome = resourceDao.update( 1442 res, null, false, false, requestDetailsForEntry, theTransactionDetails); 1443 } else { 1444 if (!shouldConditionalUpdateMatchId(theTransactionDetails, res.getIdElement())) { 1445 res.setId((String) null); 1446 } 1447 String matchUrl; 1448 if (isNotBlank(parts.getParams())) { 1449 matchUrl = parts.getResourceType() + '?' + parts.getParams(); 1450 } else { 1451 matchUrl = parts.getResourceType(); 1452 } 1453 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1454 outcome = resourceDao.update( 1455 res, matchUrl, false, false, requestDetailsForEntry, theTransactionDetails); 1456 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1457 if (Boolean.TRUE.equals(outcome.getCreated())) { 1458 conditionalRequestUrls.put(matchUrl, res.getClass()); 1459 } 1460 } 1461 1462 if (outcome.getCreated() == Boolean.FALSE 1463 || (outcome.getCreated() == Boolean.TRUE 1464 && outcome.getId().getVersionIdPartAsLong() > 1)) { 1465 updatedEntities.add(outcome.getEntity()); 1466 if (outcome.getResource() != null) { 1467 updatedResources.add(outcome.getResource()); 1468 } 1469 } 1470 1471 theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); 1472 handleTransactionCreateOrUpdateOutcome( 1473 theIdSubstitutions, 1474 theIdToPersistedOutcome, 1475 nextResourceId, 1476 outcome, 1477 nextRespEntry, 1478 resourceType, 1479 res, 1480 requestDetailsForEntry); 1481 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1482 break; 1483 } 1484 case "PATCH": { 1485 // PATCH 1486 validateResourcePresent(res, order, verb); 1487 1488 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1489 1490 String matchUrl = toMatchUrl(nextReqEntry); 1491 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1492 String patchBody = null; 1493 String contentType; 1494 IBaseParameters patchBodyParameters = null; 1495 PatchTypeEnum patchType = null; 1496 1497 if (res instanceof IBaseBinary) { 1498 IBaseBinary binary = (IBaseBinary) res; 1499 if (binary.getContent() != null && binary.getContent().length > 0) { 1500 patchBody = toUtf8String(binary.getContent()); 1501 } 1502 contentType = binary.getContentType(); 1503 patchType = 1504 PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(myContext, contentType); 1505 if (patchType == PatchTypeEnum.FHIR_PATCH_JSON 1506 || patchType == PatchTypeEnum.FHIR_PATCH_XML) { 1507 String msg = myContext 1508 .getLocalizer() 1509 .getMessage( 1510 BaseTransactionProcessor.class, "fhirPatchShouldNotUseBinaryResource"); 1511 throw new InvalidRequestException(Msg.code(536) + msg); 1512 } 1513 } else if (res instanceof IBaseParameters) { 1514 patchBodyParameters = (IBaseParameters) res; 1515 patchType = PatchTypeEnum.FHIR_PATCH_JSON; 1516 } 1517 1518 if (patchBodyParameters == null) { 1519 if (isBlank(patchBody)) { 1520 String msg = myContext 1521 .getLocalizer() 1522 .getMessage(BaseTransactionProcessor.class, "missingPatchBody"); 1523 throw new InvalidRequestException(Msg.code(537) + msg); 1524 } 1525 } 1526 1527 IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url); 1528 IIdType patchId = 1529 myContext.getVersion().newIdType(parts.getResourceType(), parts.getResourceId()); 1530 1531 String conditionalUrl; 1532 if (isNull(patchId.getIdPart())) { 1533 conditionalUrl = url; 1534 } else { 1535 conditionalUrl = matchUrl; 1536 String ifMatch = myVersionAdapter.getEntryRequestIfMatch(nextReqEntry); 1537 if (isNotBlank(ifMatch)) { 1538 patchId = UpdateMethodBinding.applyETagAsVersion(ifMatch, patchId); 1539 } 1540 } 1541 1542 DaoMethodOutcome outcome = dao.patchInTransaction( 1543 patchId, 1544 conditionalUrl, 1545 false, 1546 patchType, 1547 patchBody, 1548 patchBodyParameters, 1549 requestDetailsForEntry, 1550 theTransactionDetails); 1551 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1552 updatedEntities.add(outcome.getEntity()); 1553 if (outcome.getResource() != null) { 1554 updatedResources.add(outcome.getResource()); 1555 } 1556 if (nextResourceId != null) { 1557 handleTransactionCreateOrUpdateOutcome( 1558 theIdSubstitutions, 1559 theIdToPersistedOutcome, 1560 nextResourceId, 1561 outcome, 1562 nextRespEntry, 1563 resourceType, 1564 res, 1565 requestDetailsForEntry); 1566 } 1567 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1568 1569 break; 1570 } 1571 case "GET": 1572 break; 1573 default: 1574 throw new InvalidRequestException( 1575 Msg.code(538) + "Unable to handle verb in transaction: " + verb); 1576 } 1577 1578 theTransactionStopWatch.endCurrentTask(); 1579 } 1580 1581 postTransactionProcess(theTransactionDetails); 1582 1583 /* 1584 * Make sure that there are no conflicts from deletions. E.g. we can't delete something 1585 * if something else has a reference to it.. Unless the thing that has a reference to it 1586 * was also deleted as a part of this transaction, which is why we check this now at the 1587 * end. 1588 */ 1589 checkForDeleteConflicts(deleteConflicts, deletedResources, updatedResources); 1590 1591 theIdToPersistedOutcome.entrySet().forEach(idAndOutcome -> { 1592 theTransactionDetails.addResolvedResourceId( 1593 idAndOutcome.getKey(), idAndOutcome.getValue().getPersistentId()); 1594 }); 1595 1596 /* 1597 * Perform ID substitutions and then index each resource we have saved 1598 */ 1599 1600 resolveReferencesThenSaveAndIndexResources( 1601 theRequest, 1602 theTransactionDetails, 1603 theIdSubstitutions, 1604 theIdToPersistedOutcome, 1605 theTransactionStopWatch, 1606 entriesToProcess, 1607 nonUpdatedEntities, 1608 updatedEntities); 1609 1610 theTransactionStopWatch.endCurrentTask(); 1611 1612 // flush writes to db 1613 theTransactionStopWatch.startTask("Flush writes to database"); 1614 1615 // flush the changes 1616 flushSession(theIdToPersistedOutcome); 1617 1618 theTransactionStopWatch.endCurrentTask(); 1619 1620 /* 1621 * Double check we didn't allow any duplicates we shouldn't have 1622 */ 1623 if (conditionalRequestUrls.size() > 0) { 1624 theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); 1625 } 1626 if (!myStorageSettings.isMassIngestionMode()) { 1627 validateNoDuplicates( 1628 theRequest, 1629 theTransactionDetails, 1630 theActionName, 1631 conditionalRequestUrls, 1632 theIdToPersistedOutcome.values()); 1633 } 1634 1635 theTransactionStopWatch.endCurrentTask(); 1636 if (conditionalUrlToIdMap.size() > 0) { 1637 theTransactionStopWatch.startTask( 1638 "Check that all conditionally created/updated entities actually match their conditionals."); 1639 } 1640 1641 if (!myStorageSettings.isMassIngestionMode()) { 1642 validateAllInsertsMatchTheirConditionalUrls(theIdToPersistedOutcome, conditionalUrlToIdMap, theRequest); 1643 } 1644 theTransactionStopWatch.endCurrentTask(); 1645 1646 for (IIdType next : theAllIds) { 1647 IIdType replacement = theIdSubstitutions.getForSource(next); 1648 if (replacement != null && !replacement.equals(next)) { 1649 ourLog.debug( 1650 "Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); 1651 } 1652 } 1653 1654 IInterceptorBroadcaster compositeBroadcaster = 1655 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 1656 ListMultimap<Pointcut, HookParams> deferredBroadcastEvents = 1657 theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts(); 1658 for (Map.Entry<Pointcut, HookParams> nextEntry : deferredBroadcastEvents.entries()) { 1659 Pointcut nextPointcut = nextEntry.getKey(); 1660 HookParams nextParams = nextEntry.getValue(); 1661 compositeBroadcaster.callHooks(nextPointcut, nextParams); 1662 } 1663 1664 DeferredInterceptorBroadcasts deferredInterceptorBroadcasts = 1665 new DeferredInterceptorBroadcasts(deferredBroadcastEvents); 1666 HookParams params = new HookParams() 1667 .add(RequestDetails.class, theRequest) 1668 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1669 .add(DeferredInterceptorBroadcasts.class, deferredInterceptorBroadcasts) 1670 .add(TransactionDetails.class, theTransactionDetails) 1671 .add(IBaseBundle.class, theResponse); 1672 compositeBroadcaster.callHooks(Pointcut.STORAGE_TRANSACTION_PROCESSED, params); 1673 1674 theTransactionDetails.deferredBroadcastProcessingFinished(); 1675 1676 // finishedCallingDeferredInterceptorBroadcasts 1677 1678 return entriesToProcess; 1679 1680 } finally { 1681 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts()) { 1682 theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts(); 1683 } 1684 } 1685 } 1686 1687 /** 1688 * Subclasses may override this in order to invoke specific operations when 1689 * we're finished handling all the write entries in the transaction bundle 1690 * with a given verb. 1691 */ 1692 protected void handleVerbChangeInTransactionWriteOperations() { 1693 // nothing 1694 } 1695 1696 /** 1697 * Implement to handle post transaction processing 1698 */ 1699 protected void postTransactionProcess(TransactionDetails theTransactionDetails) { 1700 // nothing 1701 } 1702 1703 /** 1704 * Check for if a resource id should be matched in a conditional update 1705 * If the FHIR version is older than R4, it follows the old specifications and does not match 1706 * If the resource id has been resolved, then it is an existing resource and does not need to be matched 1707 * If the resource id is local or a placeholder, the id is temporary and should not be matched 1708 */ 1709 private boolean shouldConditionalUpdateMatchId(TransactionDetails theTransactionDetails, IIdType theId) { 1710 if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.R4)) { 1711 return false; 1712 } 1713 if (theTransactionDetails.hasResolvedResourceId(theId) 1714 && !theTransactionDetails.isResolvedResourceIdEmpty(theId)) { 1715 return false; 1716 } 1717 if (theId != null && theId.getValue() != null) { 1718 return !(theId.getValue().startsWith("urn:") || theId.getValue().startsWith("#")); 1719 } 1720 return true; 1721 } 1722 1723 private boolean shouldSwapBinaryToActualResource( 1724 IBaseResource theResource, String theResourceType, IIdType theNextResourceId) { 1725 if ("Binary".equalsIgnoreCase(theResourceType) 1726 && theNextResourceId.getResourceType() != null 1727 && !theNextResourceId.getResourceType().equalsIgnoreCase("Binary")) { 1728 return true; 1729 } else { 1730 return false; 1731 } 1732 } 1733 1734 private void setConditionalUrlToBeValidatedLater( 1735 Map<String, IIdType> theConditionalUrlToIdMap, String theMatchUrl, IIdType theId) { 1736 if (!isBlank(theMatchUrl)) { 1737 theConditionalUrlToIdMap.put(theMatchUrl, theId); 1738 } 1739 } 1740 1741 /** 1742 * After transaction processing and resolution of indexes and references, we want to validate that the resources that were stored _actually_ 1743 * match the conditional URLs that they were brought in on. 1744 */ 1745 private void validateAllInsertsMatchTheirConditionalUrls( 1746 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1747 Map<String, IIdType> conditionalUrlToIdMap, 1748 RequestDetails theRequest) { 1749 conditionalUrlToIdMap.entrySet().stream() 1750 .filter(entry -> entry.getKey() != null) 1751 .forEach(entry -> { 1752 String matchUrl = entry.getKey(); 1753 IIdType value = entry.getValue(); 1754 DaoMethodOutcome daoMethodOutcome = theIdToPersistedOutcome.get(value); 1755 if (daoMethodOutcome != null 1756 && !daoMethodOutcome.isNop() 1757 && daoMethodOutcome.getResource() != null) { 1758 InMemoryMatchResult match = 1759 mySearchParamMatcher.match(matchUrl, daoMethodOutcome.getResource(), theRequest); 1760 if (ourLog.isDebugEnabled()) { 1761 ourLog.debug( 1762 "Checking conditional URL [{}] against resource with ID [{}]: Supported?:[{}], Matched?:[{}]", 1763 matchUrl, 1764 value, 1765 match.supported(), 1766 match.matched()); 1767 } 1768 if (match.supported()) { 1769 if (!match.matched()) { 1770 throw new PreconditionFailedException(Msg.code(539) + "Invalid conditional URL \"" 1771 + matchUrl + "\". The given resource is not matched by this URL."); 1772 } 1773 } 1774 } 1775 }); 1776 } 1777 1778 /** 1779 * Checks for any delete conflicts. 1780 * 1781 * @param theDeleteConflicts - set of delete conflicts 1782 * @param theDeletedResources - set of deleted resources 1783 * @param theUpdatedResources - list of updated resources 1784 */ 1785 private void checkForDeleteConflicts( 1786 DeleteConflictList theDeleteConflicts, 1787 Set<String> theDeletedResources, 1788 List<IBaseResource> theUpdatedResources) { 1789 for (Iterator<DeleteConflict> iter = theDeleteConflicts.iterator(); iter.hasNext(); ) { 1790 DeleteConflict nextDeleteConflict = iter.next(); 1791 1792 /* 1793 * If we have a conflict, it means we can't delete Resource/A because 1794 * Resource/B has a reference to it. We'll ignore that conflict though 1795 * if it turns out we're also deleting Resource/B in this transaction. 1796 */ 1797 if (theDeletedResources.contains( 1798 nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) { 1799 iter.remove(); 1800 continue; 1801 } 1802 1803 /* 1804 * And then, this is kind of a last ditch check. It's also ok to delete 1805 * Resource/A if Resource/B isn't being deleted, but it is being UPDATED 1806 * in this transaction, and the updated version of it has no references 1807 * to Resource/A any more. 1808 */ 1809 String sourceId = 1810 nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue(); 1811 String targetId = 1812 nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue(); 1813 Optional<IBaseResource> updatedSource = theUpdatedResources.stream() 1814 .filter(t -> sourceId.equals( 1815 t.getIdElement().toUnqualifiedVersionless().getValue())) 1816 .findFirst(); 1817 if (updatedSource.isPresent()) { 1818 List<ResourceReferenceInfo> referencesInSource = 1819 myContext.newTerser().getAllResourceReferences(updatedSource.get()); 1820 boolean sourceStillReferencesTarget = referencesInSource.stream() 1821 .anyMatch(t -> targetId.equals(t.getResourceReference() 1822 .getReferenceElement() 1823 .toUnqualifiedVersionless() 1824 .getValue())); 1825 if (!sourceStillReferencesTarget) { 1826 iter.remove(); 1827 } 1828 } 1829 } 1830 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(myContext, theDeleteConflicts); 1831 } 1832 1833 /** 1834 * This method replaces any placeholder references in the 1835 * source transaction Bundle with their actual targets, then stores the resource contents and indexes 1836 * in the database. This is trickier than you'd think because of a couple of possibilities during the 1837 * save: 1838 * * There may be resources that have not changed (e.g. an update/PUT with a resource body identical 1839 * to what is already in the database) 1840 * * There may be resources with auto-versioned references, meaning we're replacing certain references 1841 * in the resource with a versioned references, referencing the current version at the time of the 1842 * transaction processing 1843 * * There may by auto-versioned references pointing to these unchanged targets 1844 * <p> 1845 * If we're not doing any auto-versioned references, we'll just iterate through all resources in the 1846 * transaction and save them one at a time. 1847 * <p> 1848 * However, if we have any auto-versioned references we do this in 2 passes: First the resources from the 1849 * transaction that don't have any auto-versioned references are stored. We do them first since there's 1850 * a chance they may be a NOP and we'll need to account for their version number not actually changing. 1851 * Then we do a second pass for any resources that have auto-versioned references. These happen in a separate 1852 * pass because it's too complex to try and insert the auto-versioned references and still 1853 * account for NOPs, so we block NOPs in that pass. 1854 */ 1855 private void resolveReferencesThenSaveAndIndexResources( 1856 RequestDetails theRequest, 1857 TransactionDetails theTransactionDetails, 1858 IdSubstitutionMap theIdSubstitutions, 1859 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1860 StopWatch theTransactionStopWatch, 1861 EntriesToProcessMap theEntriesToProcess, 1862 Set<IIdType> theNonUpdatedEntities, 1863 Set<IBasePersistedResource> theUpdatedEntities) { 1864 FhirTerser terser = myContext.newTerser(); 1865 theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); 1866 IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null; 1867 int i = 0; 1868 for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { 1869 1870 if (i++ % 250 == 0) { 1871 ourLog.debug( 1872 "Have indexed {} entities out of {} in transaction", 1873 i, 1874 theIdToPersistedOutcome.values().size()); 1875 } 1876 1877 if (nextOutcome.isNop()) { 1878 continue; 1879 } 1880 1881 IBaseResource nextResource = nextOutcome.getResource(); 1882 if (nextResource == null) { 1883 continue; 1884 } 1885 1886 Set<IBaseReference> referencesToAutoVersion = 1887 BaseStorageDao.extractReferencesToAutoVersion(myContext, myStorageSettings, nextResource); 1888 Set<IBaseReference> referencesToKeepClientSuppliedVersion = 1889 BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource); 1890 1891 if (referencesToAutoVersion.isEmpty()) { 1892 // no references to autoversion - we can do the resolve and save now 1893 resolveReferencesThenSaveAndIndexResource( 1894 theRequest, 1895 theTransactionDetails, 1896 theIdSubstitutions, 1897 theIdToPersistedOutcome, 1898 theEntriesToProcess, 1899 theNonUpdatedEntities, 1900 theUpdatedEntities, 1901 terser, 1902 nextOutcome, 1903 nextResource, 1904 referencesToAutoVersion, // this is empty 1905 referencesToKeepClientSuppliedVersion); 1906 } else { 1907 // we have autoversioned things to defer until later 1908 if (deferredIndexesForAutoVersioning == null) { 1909 deferredIndexesForAutoVersioning = new IdentityHashMap<>(); 1910 } 1911 deferredIndexesForAutoVersioning.put(nextOutcome, referencesToAutoVersion); 1912 } 1913 } 1914 1915 // If we have any resources we'll be auto-versioning, index these next 1916 if (deferredIndexesForAutoVersioning != null) { 1917 for (Map.Entry<DaoMethodOutcome, Set<IBaseReference>> nextEntry : 1918 deferredIndexesForAutoVersioning.entrySet()) { 1919 DaoMethodOutcome nextOutcome = nextEntry.getKey(); 1920 Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue(); 1921 IBaseResource nextResource = nextOutcome.getResource(); 1922 Set<IBaseReference> referencesToKeepClientSuppliedVersion = 1923 BaseStorageDao.extractReferencesToAvoidReplacement(myContext, nextResource); 1924 1925 resolveReferencesThenSaveAndIndexResource( 1926 theRequest, 1927 theTransactionDetails, 1928 theIdSubstitutions, 1929 theIdToPersistedOutcome, 1930 theEntriesToProcess, 1931 theNonUpdatedEntities, 1932 theUpdatedEntities, 1933 terser, 1934 nextOutcome, 1935 nextResource, 1936 referencesToAutoVersion, 1937 referencesToKeepClientSuppliedVersion); 1938 } 1939 } 1940 } 1941 1942 private void resolveReferencesThenSaveAndIndexResource( 1943 RequestDetails theRequest, 1944 TransactionDetails theTransactionDetails, 1945 IdSubstitutionMap theIdSubstitutions, 1946 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1947 EntriesToProcessMap theEntriesToProcess, 1948 Set<IIdType> theNonUpdatedEntities, 1949 Set<IBasePersistedResource> theUpdatedEntities, 1950 FhirTerser theTerser, 1951 DaoMethodOutcome theDaoMethodOutcome, 1952 IBaseResource theResource, 1953 Set<IBaseReference> theReferencesToAutoVersion, 1954 Set<IBaseReference> theReferencesToKeepClientSuppliedVersion) { 1955 // References 1956 List<ResourceReferenceInfo> allRefs = theTerser.getAllResourceReferences(theResource); 1957 for (ResourceReferenceInfo nextRef : allRefs) { 1958 IBaseReference resourceReference = nextRef.getResourceReference(); 1959 IIdType nextId = resourceReference.getReferenceElement(); 1960 IIdType newId = null; 1961 if (!nextId.hasIdPart()) { 1962 if (resourceReference.getResource() != null) { 1963 IIdType targetId = resourceReference.getResource().getIdElement(); 1964 if (targetId.getValue() == null || targetId.getValue().startsWith("#")) { 1965 // This means it's a contained resource 1966 continue; 1967 } else if (theIdSubstitutions.containsTarget(targetId)) { 1968 newId = targetId; 1969 } else { 1970 throw new InternalErrorException(Msg.code(540) 1971 + "References by resource with no reference ID are not supported in DAO layer"); 1972 } 1973 } else { 1974 continue; 1975 } 1976 } 1977 if (newId != null || theIdSubstitutions.containsSource(nextId)) { 1978 if (shouldReplaceResourceReference( 1979 theReferencesToAutoVersion, theReferencesToKeepClientSuppliedVersion, resourceReference)) { 1980 if (newId == null) { 1981 newId = theIdSubstitutions.getForSource(nextId); 1982 } 1983 if (newId != null) { 1984 ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); 1985 if (theReferencesToAutoVersion.contains(resourceReference)) { 1986 replaceResourceReference(newId, resourceReference, theTransactionDetails); 1987 } else { 1988 replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails); 1989 } 1990 } 1991 } 1992 } else if (nextId.getValue().startsWith("urn:")) { 1993 throw new InvalidRequestException( 1994 Msg.code(541) + "Unable to satisfy placeholder ID " + nextId.getValue() 1995 + " found in element named '" + nextRef.getName() + "' within resource of type: " 1996 + theResource.getIdElement().getResourceType()); 1997 } else { 1998 // get a map of 1999 // existing ids -> PID (for resources that exist in the DB) 2000 // should this be allPartitions? 2001 ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds( 2002 RequestPartitionId.allPartitions(), 2003 theReferencesToAutoVersion.stream() 2004 .map(IBaseReference::getReferenceElement) 2005 .collect(Collectors.toList())); 2006 2007 for (IBaseReference baseRef : theReferencesToAutoVersion) { 2008 IIdType id = baseRef.getReferenceElement(); 2009 if (!resourceVersionMap.containsKey(id) 2010 && myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) { 2011 // not in the db, but autocreateplaceholders is true 2012 // so the version we'll set is "1" (since it will be 2013 // created later) 2014 String newRef = id.withVersion("1").getValue(); 2015 id.setValue(newRef); 2016 } else { 2017 // we will add the looked up info to the transaction 2018 // for later 2019 if (resourceVersionMap.containsKey(id)) { 2020 theTransactionDetails.addResolvedResourceId( 2021 id, resourceVersionMap.getResourcePersistentId(id)); 2022 } 2023 } 2024 } 2025 2026 if (theReferencesToAutoVersion.contains(resourceReference)) { 2027 DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId); 2028 2029 if (outcome != null && !outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) { 2030 replaceResourceReference(nextId, resourceReference, theTransactionDetails); 2031 } 2032 2033 // if referenced resource is not in transaction but exists in the DB, resolving its version 2034 IResourcePersistentId persistedReferenceId = resourceVersionMap.getResourcePersistentId(nextId); 2035 if (outcome == null && persistedReferenceId != null && persistedReferenceId.getVersion() != null) { 2036 IIdType newReferenceId = nextId.withVersion( 2037 persistedReferenceId.getVersion().toString()); 2038 replaceResourceReference(newReferenceId, resourceReference, theTransactionDetails); 2039 } 2040 } 2041 } 2042 } 2043 2044 // URIs 2045 Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) 2046 myContext.getElementDefinition("uri").getImplementingClass(); 2047 List<? extends IPrimitiveType<?>> allUris = theTerser.getAllPopulatedChildElementsOfType(theResource, uriType); 2048 for (IPrimitiveType<?> nextRef : allUris) { 2049 if (nextRef instanceof IIdType) { 2050 continue; // No substitution on the resource ID itself! 2051 } 2052 String nextUriString = nextRef.getValueAsString(); 2053 if (isNotBlank(nextUriString)) { 2054 if (theIdSubstitutions.containsSource(nextUriString)) { 2055 IIdType newId = theIdSubstitutions.getForSource(nextUriString); 2056 ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); 2057 2058 String existingValue = nextRef.getValueAsString(); 2059 theTransactionDetails.addRollbackUndoAction(() -> nextRef.setValueAsString(existingValue)); 2060 2061 nextRef.setValueAsString(newId.toVersionless().getValue()); 2062 } else { 2063 ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); 2064 } 2065 } 2066 } 2067 2068 IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(theResource); 2069 Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; 2070 2071 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(theResource.getClass()); 2072 IJpaDao jpaDao = (IJpaDao) dao; 2073 2074 IBasePersistedResource updateOutcome = null; 2075 if (theUpdatedEntities.contains(theDaoMethodOutcome.getEntity())) { 2076 boolean forceUpdateVersion = !theReferencesToAutoVersion.isEmpty(); 2077 String matchUrl = theDaoMethodOutcome.getMatchUrl(); 2078 RestOperationTypeEnum operationType = theDaoMethodOutcome.getOperationType(); 2079 DaoMethodOutcome daoMethodOutcome = jpaDao.updateInternal( 2080 theRequest, 2081 theResource, 2082 matchUrl, 2083 true, 2084 forceUpdateVersion, 2085 theDaoMethodOutcome.getEntity(), 2086 theResource.getIdElement(), 2087 theDaoMethodOutcome.getPreviousResource(), 2088 operationType, 2089 theTransactionDetails); 2090 updateOutcome = daoMethodOutcome.getEntity(); 2091 theDaoMethodOutcome = daoMethodOutcome; 2092 } else if (!theNonUpdatedEntities.contains(theDaoMethodOutcome.getId())) { 2093 updateOutcome = jpaDao.updateEntity( 2094 theRequest, 2095 theResource, 2096 theDaoMethodOutcome.getEntity(), 2097 deletedTimestampOrNull, 2098 true, 2099 false, 2100 theTransactionDetails, 2101 false, 2102 true); 2103 } 2104 2105 // Make sure we reflect the actual final version for the resource. 2106 if (updateOutcome != null) { 2107 IIdType newId = updateOutcome.getIdDt(); 2108 2109 IIdType entryId = theEntriesToProcess.getIdWithVersionlessComparison(newId); 2110 if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) { 2111 entryId.setValue(newId.getValue()); 2112 } 2113 2114 theDaoMethodOutcome.setId(newId); 2115 2116 theIdSubstitutions.updateTargets(newId); 2117 2118 // This will only be null if we're not intending to return an OO 2119 IBaseOperationOutcome operationOutcome = theDaoMethodOutcome.getOperationOutcome(); 2120 if (operationOutcome != null) { 2121 2122 List<IIdType> autoCreatedPlaceholders = 2123 theTransactionDetails.getAutoCreatedPlaceholderResourcesAndClear(); 2124 for (IIdType autoCreatedPlaceholder : autoCreatedPlaceholders) { 2125 BaseStorageDao.addIssueToOperationOutcomeForAutoCreatedPlaceholder( 2126 myContext, autoCreatedPlaceholder, operationOutcome); 2127 } 2128 2129 IBase responseEntry = theEntriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); 2130 myVersionAdapter.setResponseOutcome(responseEntry, operationOutcome); 2131 } 2132 } 2133 } 2134 2135 /** 2136 * We should replace the references when 2137 * 1. It is not a reference we should keep the client-supplied version for as configured by `DontStripVersionsFromReferences` or 2138 * 2. It is a reference that has been identified for auto versioning or 2139 * 3. Is a placeholder reference 2140 * 2141 * @param theReferencesToAutoVersion list of references identified for auto versioning 2142 * @param theReferencesToKeepClientSuppliedVersion list of references that we should not strip the version for 2143 * @param theResourceReference the resource reference 2144 * @return true if we should replace the resource reference, false if we should keep the client provided reference 2145 */ 2146 private boolean shouldReplaceResourceReference( 2147 Set<IBaseReference> theReferencesToAutoVersion, 2148 Set<IBaseReference> theReferencesToKeepClientSuppliedVersion, 2149 IBaseReference theResourceReference) { 2150 return (!theReferencesToKeepClientSuppliedVersion.contains(theResourceReference) 2151 && myContext.getParserOptions().isStripVersionsFromReferences()) 2152 || theReferencesToAutoVersion.contains(theResourceReference) 2153 || isPlaceholder(theResourceReference.getReferenceElement()); 2154 } 2155 2156 private void replaceResourceReference( 2157 IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) { 2158 addRollbackReferenceRestore(theTransactionDetails, theResourceReference); 2159 theResourceReference.setReference(theReferenceId.getValue()); 2160 theResourceReference.setResource(null); 2161 } 2162 2163 private void addRollbackReferenceRestore( 2164 TransactionDetails theTransactionDetails, IBaseReference resourceReference) { 2165 String existingValue = resourceReference.getReferenceElement().getValue(); 2166 theTransactionDetails.addRollbackUndoAction(() -> resourceReference.setReference(existingValue)); 2167 } 2168 2169 private void validateNoDuplicates( 2170 RequestDetails theRequest, 2171 TransactionDetails theTransactionDetails, 2172 String theActionName, 2173 Map<String, Class<? extends IBaseResource>> conditionalRequestUrls, 2174 Collection<DaoMethodOutcome> thePersistedOutcomes) { 2175 2176 Map<ResourceTable, ResourceIndexedSearchParams> existingSearchParams = 2177 theTransactionDetails.getOrCreateUserData( 2178 HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, Collections::emptyMap); 2179 2180 IdentityHashMap<IBaseResource, ResourceIndexedSearchParams> resourceToIndexedParams = 2181 new IdentityHashMap<>(thePersistedOutcomes.size()); 2182 thePersistedOutcomes.stream() 2183 .filter(t -> !t.isNop()) 2184 // N.B. GGG: This validation never occurs for mongo, as nothing is a ResourceTable. 2185 .filter(t -> t.getEntity() instanceof ResourceTable) 2186 .filter(t -> t.getEntity().getDeleted() == null) 2187 .filter(t -> t.getResource() != null) 2188 .forEach(t -> { 2189 ResourceTable entity = (ResourceTable) t.getEntity(); 2190 ResourceIndexedSearchParams params = existingSearchParams.get(entity); 2191 if (params == null) { 2192 params = ResourceIndexedSearchParams.withLists(entity); 2193 } 2194 resourceToIndexedParams.put(t.getResource(), params); 2195 }); 2196 2197 for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) { 2198 String matchUrl = nextEntry.getKey(); 2199 if (isNotBlank(matchUrl)) { 2200 if (matchUrl.startsWith("?") 2201 || (!matchUrl.contains("?") 2202 && UNQUALIFIED_MATCH_URL_START.matcher(matchUrl).find())) { 2203 StringBuilder b = new StringBuilder(); 2204 b.append(myContext.getResourceType(nextEntry.getValue())); 2205 if (!matchUrl.startsWith("?")) { 2206 b.append("?"); 2207 } 2208 b.append(matchUrl); 2209 matchUrl = b.toString(); 2210 } 2211 2212 if (!myInMemoryResourceMatcher.canBeEvaluatedInMemory(matchUrl).supported()) { 2213 continue; 2214 } 2215 2216 int counter = 0; 2217 for (Map.Entry<IBaseResource, ResourceIndexedSearchParams> entries : 2218 resourceToIndexedParams.entrySet()) { 2219 ResourceIndexedSearchParams indexedParams = entries.getValue(); 2220 IBaseResource resource = entries.getKey(); 2221 2222 String resourceType = myContext.getResourceType(resource); 2223 if (!matchUrl.startsWith(resourceType + "?")) { 2224 continue; 2225 } 2226 2227 if (myInMemoryResourceMatcher 2228 .match(matchUrl, resource, indexedParams, theRequest) 2229 .matched()) { 2230 counter++; 2231 if (counter > 1) { 2232 throw new InvalidRequestException(Msg.code(542) + "Unable to process " + theActionName 2233 + " - Request would cause multiple resources to match URL: \"" + matchUrl 2234 + "\". Does transaction request contain duplicates?"); 2235 } 2236 } 2237 } 2238 } 2239 } 2240 } 2241 2242 protected abstract void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome); 2243 2244 private void validateResourcePresent(IBaseResource theResource, Integer theOrder, String theVerb) { 2245 if (theResource == null) { 2246 String msg = myContext 2247 .getLocalizer() 2248 .getMessage(BaseTransactionProcessor.class, "missingMandatoryResource", theVerb, theOrder); 2249 throw new InvalidRequestException(Msg.code(543) + msg); 2250 } 2251 } 2252 2253 private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) { 2254 IdType id = new IdType(theResourceType, theResourceId, theVersion); 2255 return myContext.getVersion().newIdType().setValue(id.getValue()); 2256 } 2257 2258 private IIdType newIdType(String theToResourceName, String theIdPart) { 2259 return newIdType(theToResourceName, theIdPart, null); 2260 } 2261 2262 @VisibleForTesting 2263 public void setDaoRegistry(DaoRegistry theDaoRegistry) { 2264 myDaoRegistry = theDaoRegistry; 2265 } 2266 2267 private IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) { 2268 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(theClass); 2269 if (dao == null) { 2270 Set<String> types = new TreeSet<>(myDaoRegistry.getRegisteredDaoTypes()); 2271 String type = myContext.getResourceType(theClass); 2272 String msg = myContext 2273 .getLocalizer() 2274 .getMessage(BaseTransactionProcessor.class, "unsupportedResourceType", type, types.toString()); 2275 throw new InvalidRequestException(Msg.code(544) + msg); 2276 } 2277 return dao; 2278 } 2279 2280 private String toResourceName(Class<? extends IBaseResource> theResourceType) { 2281 return myContext.getResourceType(theResourceType); 2282 } 2283 2284 public void setContext(FhirContext theContext) { 2285 myContext = theContext; 2286 } 2287 2288 /** 2289 * Extracts the transaction url from the entry and verifies it's: 2290 * <li>not null or blank (unless it is a POST), and</li> 2291 * <li>is a relative url matching the resourceType it is about</li> 2292 * <p> 2293 * For POST requests, the url is allowed to be blank to preserve the existing behavior. 2294 * <p> 2295 * Returns the transaction url (or throws an InvalidRequestException if url is missing or not valid) 2296 */ 2297 private String extractAndVerifyTransactionUrlForEntry(IBase theEntry, String theVerb) { 2298 String url = extractTransactionUrlOrThrowException(theEntry, theVerb); 2299 if (url.isEmpty() || isValidResourceTypeUrl(url)) { 2300 // for POST requests, the url is allowed to be blank, which is checked in 2301 // extractTransactionUrlOrThrowException and an empty string is returned in that case. 2302 // That is why empty url is allowed here. 2303 return url; 2304 } 2305 2306 ourLog.debug("Invalid url. Should begin with a resource type: {}", url); 2307 String msg = myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, url); 2308 throw new InvalidRequestException(Msg.code(2006) + msg); 2309 } 2310 2311 /** 2312 * Returns true if the provided url is a valid entry request.url. 2313 * <p> 2314 * This means: 2315 * a) not an absolute url (does not start with http/https) 2316 * b) starts with either a ResourceType or /ResourceType 2317 */ 2318 private boolean isValidResourceTypeUrl(@Nonnull String theUrl) { 2319 if (UrlUtil.isAbsolute(theUrl)) { 2320 return false; 2321 } else { 2322 int queryStringIndex = theUrl.indexOf("?"); 2323 String url; 2324 if (queryStringIndex > 0) { 2325 url = theUrl.substring(0, theUrl.indexOf("?")); 2326 } else { 2327 url = theUrl; 2328 } 2329 String[] parts; 2330 if (url.startsWith("/")) { 2331 parts = url.substring(1).split("/"); 2332 } else { 2333 parts = url.split("/"); 2334 } 2335 Set<String> allResourceTypes = myContext.getResourceTypes(); 2336 2337 return allResourceTypes.contains(parts[0]); 2338 } 2339 } 2340 2341 /** 2342 * Extracts the transaction url from the entry and verifies that it is not null/blank, unless it is a POST, 2343 * and returns it. For POST requests allows null or blank values to keep the existing behaviour and returns 2344 * an empty string in that case. 2345 */ 2346 private String extractTransactionUrlOrThrowException(IBase nextEntry, String verb) { 2347 String url = myVersionAdapter.getEntryRequestUrl(nextEntry); 2348 if ("POST".equals(verb) && isBlank(url)) { 2349 // allow blanks for POST to keep the existing behaviour 2350 // returning empty string instead of null to not get NPE later 2351 return ""; 2352 } 2353 2354 if (isBlank(url)) { 2355 throw new InvalidRequestException(Msg.code(545) 2356 + myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionMissingUrl", verb)); 2357 } 2358 return url; 2359 } 2360 2361 private IFhirResourceDao<? extends IBaseResource> toDao(UrlUtil.UrlParts theParts, String theVerb, String theUrl) { 2362 RuntimeResourceDefinition resType; 2363 try { 2364 resType = myContext.getResourceDefinition(theParts.getResourceType()); 2365 } catch (DataFormatException e) { 2366 String msg = 2367 myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, theUrl); 2368 throw new InvalidRequestException(Msg.code(546) + msg); 2369 } 2370 IFhirResourceDao<? extends IBaseResource> dao = null; 2371 if (resType != null) { 2372 dao = myDaoRegistry.getResourceDao(resType.getImplementingClass()); 2373 } 2374 if (dao == null) { 2375 String msg = 2376 myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, theUrl); 2377 throw new InvalidRequestException(Msg.code(547) + msg); 2378 } 2379 2380 return dao; 2381 } 2382 2383 private String toMatchUrl(IBase theEntry) { 2384 String verb = myVersionAdapter.getEntryRequestVerb(myContext, theEntry); 2385 switch (defaultString(verb)) { 2386 case POST: 2387 return myVersionAdapter.getEntryIfNoneExist(theEntry); 2388 case PUT: 2389 case DELETE: 2390 case PATCH: 2391 String url = extractTransactionUrlOrThrowException(theEntry, verb); 2392 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 2393 if (isBlank(parts.getResourceId())) { 2394 return parts.getResourceType() + '?' + parts.getParams(); 2395 } 2396 return null; 2397 default: 2398 return null; 2399 } 2400 } 2401 2402 @VisibleForTesting 2403 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 2404 myPartitionSettings = thePartitionSettings; 2405 } 2406 2407 /** 2408 * Transaction Order, per the spec: 2409 * <p> 2410 * Process any DELETE interactions 2411 * Process any POST interactions 2412 * Process any PUT interactions 2413 * Process any PATCH interactions 2414 * Process any GET interactions 2415 */ 2416 // @formatter:off 2417 public class TransactionSorter implements Comparator<IBase> { 2418 2419 private final Set<String> myPlaceholderIds; 2420 2421 public TransactionSorter(Set<String> thePlaceholderIds) { 2422 myPlaceholderIds = thePlaceholderIds; 2423 } 2424 2425 @Override 2426 public int compare(IBase theO1, IBase theO2) { 2427 int o1 = toOrder(theO1); 2428 int o2 = toOrder(theO2); 2429 2430 if (o1 == o2) { 2431 String matchUrl1 = toMatchUrl(theO1); 2432 String matchUrl2 = toMatchUrl(theO2); 2433 if (isBlank(matchUrl1) && isBlank(matchUrl2)) { 2434 return 0; 2435 } 2436 if (isBlank(matchUrl1)) { 2437 return -1; 2438 } 2439 if (isBlank(matchUrl2)) { 2440 return 1; 2441 } 2442 2443 boolean match1containsSubstitutions = false; 2444 boolean match2containsSubstitutions = false; 2445 for (String nextPlaceholder : myPlaceholderIds) { 2446 if (matchUrl1.contains(nextPlaceholder)) { 2447 match1containsSubstitutions = true; 2448 } 2449 if (matchUrl2.contains(nextPlaceholder)) { 2450 match2containsSubstitutions = true; 2451 } 2452 } 2453 2454 if (match1containsSubstitutions && match2containsSubstitutions) { 2455 return 0; 2456 } 2457 if (!match1containsSubstitutions && !match2containsSubstitutions) { 2458 return 0; 2459 } 2460 if (match1containsSubstitutions) { 2461 return 1; 2462 } else { 2463 return -1; 2464 } 2465 } 2466 2467 return o1 - o2; 2468 } 2469 2470 private int toOrder(IBase theO1) { 2471 int o1 = 0; 2472 if (myVersionAdapter.getEntryRequestVerb(myContext, theO1) != null) { 2473 switch (myVersionAdapter.getEntryRequestVerb(myContext, theO1)) { 2474 case "DELETE": 2475 o1 = 1; 2476 break; 2477 case "POST": 2478 o1 = 2; 2479 break; 2480 case "PUT": 2481 o1 = 3; 2482 break; 2483 case "PATCH": 2484 o1 = 4; 2485 break; 2486 case "GET": 2487 o1 = 5; 2488 break; 2489 default: 2490 o1 = 0; 2491 break; 2492 } 2493 } 2494 return o1; 2495 } 2496 } 2497 2498 public class RetriableBundleTask implements Runnable { 2499 2500 private final CountDownLatch myCompletedLatch; 2501 private final RequestDetails myRequestDetails; 2502 private final IBase myNextReqEntry; 2503 private final Map<Integer, Object> myResponseMap; 2504 private final int myResponseOrder; 2505 private final boolean myNestedMode; 2506 private BaseServerResponseException myLastSeenException; 2507 2508 protected RetriableBundleTask( 2509 CountDownLatch theCompletedLatch, 2510 RequestDetails theRequestDetails, 2511 Map<Integer, Object> theResponseMap, 2512 int theResponseOrder, 2513 IBase theNextReqEntry, 2514 boolean theNestedMode) { 2515 this.myCompletedLatch = theCompletedLatch; 2516 this.myRequestDetails = theRequestDetails; 2517 this.myNextReqEntry = theNextReqEntry; 2518 this.myResponseMap = theResponseMap; 2519 this.myResponseOrder = theResponseOrder; 2520 this.myNestedMode = theNestedMode; 2521 this.myLastSeenException = null; 2522 } 2523 2524 private void processBatchEntry() { 2525 IBaseBundle subRequestBundle = 2526 myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); 2527 myVersionAdapter.addEntry(subRequestBundle, myNextReqEntry); 2528 2529 IBaseBundle nextResponseBundle = processTransactionAsSubRequest( 2530 myRequestDetails, subRequestBundle, "Batch sub-request", myNestedMode); 2531 2532 IBase subResponseEntry = 2533 (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0); 2534 myResponseMap.put(myResponseOrder, subResponseEntry); 2535 2536 /* 2537 * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it 2538 */ 2539 if (myVersionAdapter.getResource(subResponseEntry) == null) { 2540 IBase nextResponseBundleFirstEntry = 2541 (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0); 2542 myResponseMap.put(myResponseOrder, nextResponseBundleFirstEntry); 2543 } 2544 } 2545 2546 private boolean processBatchEntryWithRetry() { 2547 int maxAttempts = 3; 2548 for (int attempt = 1; ; attempt++) { 2549 try { 2550 processBatchEntry(); 2551 return true; 2552 } catch (BaseServerResponseException e) { 2553 // If we catch a known and structured exception from HAPI, just fail. 2554 myLastSeenException = e; 2555 return false; 2556 } catch (Throwable t) { 2557 myLastSeenException = new InternalErrorException(t); 2558 // If we have caught a non-tag-storage failure we are unfamiliar with, or we have exceeded max 2559 // attempts, exit. 2560 if (!DaoFailureUtil.isTagStorageFailure(t) || attempt >= maxAttempts) { 2561 ourLog.error("Failure during BATCH sub transaction processing", t); 2562 return false; 2563 } 2564 } 2565 } 2566 } 2567 2568 @Override 2569 public void run() { 2570 boolean success = processBatchEntryWithRetry(); 2571 if (!success) { 2572 populateResponseMapWithLastSeenException(); 2573 } 2574 2575 // checking for the parallelism 2576 ourLog.debug("processing batch for {} is completed", myVersionAdapter.getEntryRequestUrl(myNextReqEntry)); 2577 myCompletedLatch.countDown(); 2578 } 2579 2580 private void populateResponseMapWithLastSeenException() { 2581 ServerResponseExceptionHolder caughtEx = new ServerResponseExceptionHolder(); 2582 caughtEx.setException(myLastSeenException); 2583 myResponseMap.put(myResponseOrder, caughtEx); 2584 } 2585 } 2586 2587 private static class ServerResponseExceptionHolder { 2588 private BaseServerResponseException myException; 2589 2590 public BaseServerResponseException getException() { 2591 return myException; 2592 } 2593 2594 public void setException(BaseServerResponseException myException) { 2595 this.myException = myException; 2596 } 2597 } 2598 2599 public static boolean isPlaceholder(IIdType theId) { 2600 if (theId != null && theId.getValue() != null) { 2601 return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); 2602 } 2603 return false; 2604 } 2605 2606 private static String toStatusString(int theStatusCode) { 2607 return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); 2608 } 2609 2610 /** 2611 * Given a match URL containing 2612 * 2613 * @param theIdSubstitutions 2614 * @param theMatchUrl 2615 * @return 2616 */ 2617 public static String performIdSubstitutionsInMatchUrl(IdSubstitutionMap theIdSubstitutions, String theMatchUrl) { 2618 String matchUrl = theMatchUrl; 2619 if (isNotBlank(matchUrl) && !theIdSubstitutions.isEmpty()) { 2620 int startIdx = 0; 2621 while (startIdx != -1) { 2622 2623 int endIdx = matchUrl.indexOf('&', startIdx + 1); 2624 if (endIdx == -1) { 2625 endIdx = matchUrl.length(); 2626 } 2627 2628 int equalsIdx = matchUrl.indexOf('=', startIdx + 1); 2629 2630 int searchFrom; 2631 if (equalsIdx == -1) { 2632 searchFrom = matchUrl.length(); 2633 } else if (equalsIdx >= endIdx) { 2634 // First equals we found is from a subsequent parameter 2635 searchFrom = matchUrl.length(); 2636 } else { 2637 String paramValue = matchUrl.substring(equalsIdx + 1, endIdx); 2638 boolean isUrn = isUrn(paramValue); 2639 boolean isUrnEscaped = !isUrn && isUrnEscaped(paramValue); 2640 if (isUrn || isUrnEscaped) { 2641 if (isUrnEscaped) { 2642 paramValue = UrlUtil.unescape(paramValue); 2643 } 2644 IIdType replacement = theIdSubstitutions.getForSource(paramValue); 2645 if (replacement != null) { 2646 String replacementValue; 2647 if (replacement.hasVersionIdPart()) { 2648 replacementValue = replacement.toVersionless().getValue(); 2649 } else { 2650 replacementValue = replacement.getValue(); 2651 } 2652 matchUrl = matchUrl.substring(0, equalsIdx + 1) 2653 + replacementValue 2654 + matchUrl.substring(endIdx); 2655 searchFrom = equalsIdx + 1 + replacementValue.length(); 2656 } else { 2657 searchFrom = endIdx; 2658 } 2659 } else { 2660 searchFrom = endIdx; 2661 } 2662 } 2663 2664 if (searchFrom >= matchUrl.length()) { 2665 break; 2666 } 2667 2668 startIdx = matchUrl.indexOf('&', searchFrom); 2669 } 2670 } 2671 return matchUrl; 2672 } 2673 2674 private static boolean isUrn(@Nonnull String theId) { 2675 return theId.startsWith(URN_PREFIX); 2676 } 2677 2678 private static boolean isUrnEscaped(@Nonnull String theId) { 2679 return theId.startsWith(URN_PREFIX_ESCAPED); 2680 } 2681}