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