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