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 if (CompositeInterceptorBroadcaster.hasHooks( 363 Pointcut.STORAGE_TRANSACTION_PROCESSING, myInterceptorBroadcaster, theRequestDetails)) { 364 HookParams params = new HookParams() 365 .add(RequestDetails.class, theRequestDetails) 366 .addIfMatchesType(ServletRequestDetails.class, theRequest) 367 .add(IBaseBundle.class, theRequest); 368 CompositeInterceptorBroadcaster.doCallHooks( 369 myInterceptorBroadcaster, theRequestDetails, 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 if (CompositeInterceptorBroadcaster.hasHooks( 565 Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequestDetails)) { 566 String taskDurations = transactionStopWatch.formatTaskDurations(); 567 StorageProcessingMessage message = new StorageProcessingMessage(); 568 message.setMessage("Transaction timing:\n" + taskDurations); 569 HookParams params = new HookParams() 570 .add(RequestDetails.class, theRequestDetails) 571 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 572 .add(StorageProcessingMessage.class, message); 573 CompositeInterceptorBroadcaster.doCallHooks( 574 myInterceptorBroadcaster, theRequestDetails, 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 return CompositeInterceptorBroadcaster.hasHooks( 825 Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_PRE, myInterceptorBroadcaster, theRequestDetails) 826 || CompositeInterceptorBroadcaster.hasHooks( 827 Pointcut.STORAGE_TRANSACTION_WRITE_OPERATIONS_POST, 828 myInterceptorBroadcaster, 829 theRequestDetails); 830 } 831 832 private void callWriteOperationsHook( 833 Pointcut thePointcut, 834 RequestDetails theRequestDetails, 835 TransactionDetails theTransactionDetails, 836 TransactionWriteOperationsDetails theWriteOperationsDetails) { 837 HookParams params = new HookParams() 838 .add(TransactionDetails.class, theTransactionDetails) 839 .add(TransactionWriteOperationsDetails.class, theWriteOperationsDetails); 840 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, thePointcut, params); 841 } 842 843 @SuppressWarnings("unchecked") 844 private TransactionWriteOperationsDetails buildWriteOperationsDetails(List<IBase> theEntries) { 845 TransactionWriteOperationsDetails writeOperationsDetails; 846 List<String> updateRequestUrls = new ArrayList<>(); 847 List<String> conditionalCreateRequestUrls = new ArrayList<>(); 848 // Extract 849 for (IBase nextEntry : theEntries) { 850 String method = myVersionAdapter.getEntryRequestVerb(myContext, nextEntry); 851 if ("PUT".equals(method)) { 852 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextEntry); 853 if (isNotBlank(requestUrl)) { 854 updateRequestUrls.add(requestUrl); 855 } 856 } else if ("POST".equals(method)) { 857 String requestUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextEntry); 858 if (isNotBlank(requestUrl) && requestUrl.contains("?")) { 859 conditionalCreateRequestUrls.add(requestUrl); 860 } 861 } 862 } 863 864 writeOperationsDetails = new TransactionWriteOperationsDetails(); 865 writeOperationsDetails.setUpdateRequestUrls(updateRequestUrls); 866 writeOperationsDetails.setConditionalCreateRequestUrls(conditionalCreateRequestUrls); 867 return writeOperationsDetails; 868 } 869 870 private boolean isValidVerb(String theVerb) { 871 try { 872 return org.hl7.fhir.r4.model.Bundle.HTTPVerb.fromCode(theVerb) != null; 873 } catch (FHIRException theE) { 874 return false; 875 } 876 } 877 878 /** 879 * This method is called for nested bundles (e.g. if we received a transaction with an entry that 880 * was a GET search, this method is called on the bundle for the search result, that will be placed in the 881 * outer bundle). This method applies the _summary and _content parameters to the output of 882 * that bundle. 883 * <p> 884 * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. 885 */ 886 private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { 887 IParser p = myContext.newJsonParser(); 888 RestfulServerUtils.configureResponseParser(theRequestDetails, p); 889 return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); 890 } 891 892 protected void validateDependencies() { 893 Validate.notNull(myContext); 894 Validate.notNull(myTxManager); 895 } 896 897 private IIdType newIdType(String theValue) { 898 return myContext.getVersion().newIdType().setValue(theValue); 899 } 900 901 /** 902 * Searches for duplicate conditional creates and consolidates them. 903 */ 904 @SuppressWarnings("unchecked") 905 private void consolidateDuplicateConditionals( 906 RequestDetails theRequestDetails, String theActionName, List<IBase> theEntries) { 907 final Set<String> keysWithNoFullUrl = new HashSet<>(); 908 final HashMap<String, String> keyToUuid = new HashMap<>(); 909 910 for (int index = 0, originalIndex = 0; index < theEntries.size(); index++, originalIndex++) { 911 IBase nextReqEntry = theEntries.get(index); 912 IBaseResource resource = myVersionAdapter.getResource(nextReqEntry); 913 if (resource != null) { 914 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry); 915 String entryFullUrl = myVersionAdapter.getFullUrl(nextReqEntry); 916 String requestUrl = myVersionAdapter.getEntryRequestUrl(nextReqEntry); 917 String ifNoneExist = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); 918 919 // Conditional UPDATE 920 boolean consolidateEntryCandidate = false; 921 String conditionalUrl; 922 switch (verb) { 923 case "PUT": 924 conditionalUrl = requestUrl; 925 if (isNotBlank(requestUrl)) { 926 int questionMarkIndex = requestUrl.indexOf('?'); 927 if (questionMarkIndex >= 0 && requestUrl.length() > (questionMarkIndex + 1)) { 928 consolidateEntryCandidate = true; 929 } 930 } 931 break; 932 933 // Conditional CREATE 934 case "POST": 935 conditionalUrl = ifNoneExist; 936 if (isNotBlank(ifNoneExist)) { 937 if (isBlank(entryFullUrl) || !entryFullUrl.equals(requestUrl)) { 938 consolidateEntryCandidate = true; 939 } 940 } 941 break; 942 943 default: 944 continue; 945 } 946 947 if (isNotBlank(conditionalUrl) && !conditionalUrl.contains("?")) { 948 conditionalUrl = myContext.getResourceType(resource) + "?" + conditionalUrl; 949 } 950 951 String key = verb + "|" + conditionalUrl; 952 if (consolidateEntryCandidate) { 953 if (isBlank(entryFullUrl)) { 954 if (isNotBlank(conditionalUrl)) { 955 if (!keysWithNoFullUrl.add(key)) { 956 throw new InvalidRequestException(Msg.code(2008) + "Unable to process " + theActionName 957 + " - Request contains multiple anonymous entries (Bundle.entry.fullUrl not populated) with conditional URL: \"" 958 + UrlUtil.sanitizeUrlPart(conditionalUrl) 959 + "\". Does transaction request contain duplicates?"); 960 } 961 } 962 } else { 963 if (!keyToUuid.containsKey(key)) { 964 keyToUuid.put(key, entryFullUrl); 965 } else { 966 String msg = "Discarding transaction bundle entry " + originalIndex 967 + " as it contained a duplicate conditional " + verb; 968 ourLog.info(msg); 969 // Interceptor broadcast: JPA_PERFTRACE_INFO 970 if (CompositeInterceptorBroadcaster.hasHooks( 971 Pointcut.JPA_PERFTRACE_WARNING, myInterceptorBroadcaster, theRequestDetails)) { 972 StorageProcessingMessage message = new StorageProcessingMessage().setMessage(msg); 973 HookParams params = new HookParams() 974 .add(RequestDetails.class, theRequestDetails) 975 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 976 .add(StorageProcessingMessage.class, message); 977 CompositeInterceptorBroadcaster.doCallHooks( 978 myInterceptorBroadcaster, 979 theRequestDetails, 980 Pointcut.JPA_PERFTRACE_INFO, 981 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 for (int i = 0; i < theEntries.size(); i++) { 1137 if (i % 250 == 0) { 1138 ourLog.debug("Processed {} non-GET entries out of {} in transaction", i, theEntries.size()); 1139 } 1140 1141 IBase nextReqEntry = theEntries.get(i); 1142 IBaseResource res = myVersionAdapter.getResource(nextReqEntry); 1143 1144 IIdType nextResourceId = getNextResourceIdFromBaseResource(res, nextReqEntry, theAllIds); 1145 1146 String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry); 1147 String resourceType = res != null ? myContext.getResourceType(res) : null; 1148 Integer order = theOriginalRequestOrder.get(nextReqEntry); 1149 IBase nextRespEntry = 1150 (IBase) myVersionAdapter.getEntries(theResponse).get(order); 1151 1152 theTransactionStopWatch.startTask( 1153 "Bundle.entry[" + i + "]: " + verb + " " + defaultString(resourceType)); 1154 1155 if (res != null) { 1156 String previousResourceId = res.getIdElement().getValue(); 1157 theTransactionDetails.addRollbackUndoAction(() -> res.setId(previousResourceId)); 1158 } 1159 1160 switch (verb) { 1161 case "POST": { 1162 // CREATE 1163 /* 1164 * To preserve existing functionality, 1165 * we will only verify that the request url is 1166 * valid if it's provided at all. 1167 * Otherwise, we'll ignore it 1168 */ 1169 String url = myVersionAdapter.getEntryRequestUrl(nextReqEntry); 1170 if (isNotBlank(url)) { 1171 extractAndVerifyTransactionUrlForEntry(nextReqEntry, verb); 1172 } 1173 validateResourcePresent(res, order, verb); 1174 1175 IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); 1176 res.setId((String) null); 1177 1178 DaoMethodOutcome outcome; 1179 String matchUrl = myVersionAdapter.getEntryRequestIfNoneExist(nextReqEntry); 1180 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1181 // create individual resource 1182 outcome = resourceDao.create(res, matchUrl, false, theRequest, theTransactionDetails); 1183 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1184 res.setId(outcome.getId()); 1185 1186 if (nextResourceId != null) { 1187 handleTransactionCreateOrUpdateOutcome( 1188 theIdSubstitutions, 1189 theIdToPersistedOutcome, 1190 nextResourceId, 1191 outcome, 1192 nextRespEntry, 1193 resourceType, 1194 res, 1195 theRequest); 1196 } 1197 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1198 theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); 1199 if (outcome.getCreated() == false) { 1200 nonUpdatedEntities.add(outcome.getId()); 1201 } else { 1202 if (isNotBlank(matchUrl)) { 1203 conditionalRequestUrls.put(matchUrl, res.getClass()); 1204 } 1205 } 1206 1207 break; 1208 } 1209 case "DELETE": { 1210 // DELETE 1211 String url = extractAndVerifyTransactionUrlForEntry(nextReqEntry, verb); 1212 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1213 IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url); 1214 int status = Constants.STATUS_HTTP_204_NO_CONTENT; 1215 if (parts.getResourceId() != null) { 1216 IIdType deleteId = newIdType(parts.getResourceType(), parts.getResourceId()); 1217 if (!deletedResources.contains(deleteId.getValueAsString())) { 1218 DaoMethodOutcome outcome = 1219 dao.delete(deleteId, deleteConflicts, theRequest, theTransactionDetails); 1220 if (outcome.getEntity() != null) { 1221 deletedResources.add(deleteId.getValueAsString()); 1222 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1223 } 1224 myVersionAdapter.setResponseOutcome(nextRespEntry, outcome.getOperationOutcome()); 1225 } 1226 } else { 1227 String matchUrl = parts.getResourceType() + '?' + parts.getParams(); 1228 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1229 DeleteMethodOutcome deleteOutcome = 1230 dao.deleteByUrl(matchUrl, deleteConflicts, theRequest, theTransactionDetails); 1231 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, deleteOutcome.getId()); 1232 List<? extends IBasePersistedResource> allDeleted = deleteOutcome.getDeletedEntities(); 1233 for (IBasePersistedResource deleted : allDeleted) { 1234 deletedResources.add(deleted.getIdDt() 1235 .toUnqualifiedVersionless() 1236 .getValueAsString()); 1237 } 1238 if (allDeleted.isEmpty()) { 1239 status = Constants.STATUS_HTTP_204_NO_CONTENT; 1240 } 1241 1242 myVersionAdapter.setResponseOutcome(nextRespEntry, deleteOutcome.getOperationOutcome()); 1243 } 1244 1245 myVersionAdapter.setResponseStatus(nextRespEntry, toStatusString(status)); 1246 1247 break; 1248 } 1249 case "PUT": { 1250 // UPDATE 1251 validateResourcePresent(res, order, verb); 1252 @SuppressWarnings("rawtypes") 1253 IFhirResourceDao resourceDao = getDaoOrThrowException(res.getClass()); 1254 1255 String url = extractAndVerifyTransactionUrlForEntry(nextReqEntry, verb); 1256 1257 DaoMethodOutcome outcome; 1258 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1259 if (isNotBlank(parts.getResourceId())) { 1260 String version = null; 1261 if (isNotBlank(myVersionAdapter.getEntryRequestIfMatch(nextReqEntry))) { 1262 version = ParameterUtil.parseETagValue( 1263 myVersionAdapter.getEntryRequestIfMatch(nextReqEntry)); 1264 } 1265 res.setId(newIdType(parts.getResourceType(), parts.getResourceId(), version)); 1266 outcome = resourceDao.update(res, null, false, false, theRequest, theTransactionDetails); 1267 } else { 1268 if (!shouldConditionalUpdateMatchId(theTransactionDetails, res.getIdElement())) { 1269 res.setId((String) null); 1270 } 1271 String matchUrl; 1272 if (isNotBlank(parts.getParams())) { 1273 matchUrl = parts.getResourceType() + '?' + parts.getParams(); 1274 } else { 1275 matchUrl = parts.getResourceType(); 1276 } 1277 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1278 outcome = 1279 resourceDao.update(res, matchUrl, false, false, theRequest, theTransactionDetails); 1280 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1281 if (Boolean.TRUE.equals(outcome.getCreated())) { 1282 conditionalRequestUrls.put(matchUrl, res.getClass()); 1283 } 1284 } 1285 1286 if (outcome.getCreated() == Boolean.FALSE 1287 || (outcome.getCreated() == Boolean.TRUE 1288 && outcome.getId().getVersionIdPartAsLong() > 1)) { 1289 updatedEntities.add(outcome.getEntity()); 1290 if (outcome.getResource() != null) { 1291 updatedResources.add(outcome.getResource()); 1292 } 1293 } 1294 1295 theTransactionDetails.addResolvedResource(outcome.getId(), outcome::getResource); 1296 handleTransactionCreateOrUpdateOutcome( 1297 theIdSubstitutions, 1298 theIdToPersistedOutcome, 1299 nextResourceId, 1300 outcome, 1301 nextRespEntry, 1302 resourceType, 1303 res, 1304 theRequest); 1305 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1306 break; 1307 } 1308 case "PATCH": { 1309 // PATCH 1310 validateResourcePresent(res, order, verb); 1311 1312 String url = extractAndVerifyTransactionUrlForEntry(nextReqEntry, verb); 1313 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 1314 1315 String matchUrl = toMatchUrl(nextReqEntry); 1316 matchUrl = performIdSubstitutionsInMatchUrl(theIdSubstitutions, matchUrl); 1317 String patchBody = null; 1318 String contentType; 1319 IBaseParameters patchBodyParameters = null; 1320 PatchTypeEnum patchType = null; 1321 1322 if (res instanceof IBaseBinary) { 1323 IBaseBinary binary = (IBaseBinary) res; 1324 if (binary.getContent() != null && binary.getContent().length > 0) { 1325 patchBody = toUtf8String(binary.getContent()); 1326 } 1327 contentType = binary.getContentType(); 1328 patchType = 1329 PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(myContext, contentType); 1330 if (patchType == PatchTypeEnum.FHIR_PATCH_JSON 1331 || patchType == PatchTypeEnum.FHIR_PATCH_XML) { 1332 String msg = myContext 1333 .getLocalizer() 1334 .getMessage( 1335 BaseTransactionProcessor.class, "fhirPatchShouldNotUseBinaryResource"); 1336 throw new InvalidRequestException(Msg.code(536) + msg); 1337 } 1338 } else if (res instanceof IBaseParameters) { 1339 patchBodyParameters = (IBaseParameters) res; 1340 patchType = PatchTypeEnum.FHIR_PATCH_JSON; 1341 } 1342 1343 if (patchBodyParameters == null) { 1344 if (isBlank(patchBody)) { 1345 String msg = myContext 1346 .getLocalizer() 1347 .getMessage(BaseTransactionProcessor.class, "missingPatchBody"); 1348 throw new InvalidRequestException(Msg.code(537) + msg); 1349 } 1350 } 1351 1352 IFhirResourceDao<? extends IBaseResource> dao = toDao(parts, verb, url); 1353 IIdType patchId = myContext.getVersion().newIdType().setValue(parts.getResourceId()); 1354 1355 String conditionalUrl; 1356 if (isNull(patchId.getIdPart())) { 1357 conditionalUrl = url; 1358 } else { 1359 conditionalUrl = matchUrl; 1360 String ifMatch = myVersionAdapter.getEntryRequestIfMatch(nextReqEntry); 1361 if (isNotBlank(ifMatch)) { 1362 patchId = UpdateMethodBinding.applyETagAsVersion(ifMatch, patchId); 1363 } 1364 } 1365 1366 DaoMethodOutcome outcome = dao.patchInTransaction( 1367 patchId, 1368 conditionalUrl, 1369 false, 1370 patchType, 1371 patchBody, 1372 patchBodyParameters, 1373 theRequest, 1374 theTransactionDetails); 1375 setConditionalUrlToBeValidatedLater(conditionalUrlToIdMap, matchUrl, outcome.getId()); 1376 updatedEntities.add(outcome.getEntity()); 1377 if (outcome.getResource() != null) { 1378 updatedResources.add(outcome.getResource()); 1379 } 1380 if (nextResourceId != null) { 1381 handleTransactionCreateOrUpdateOutcome( 1382 theIdSubstitutions, 1383 theIdToPersistedOutcome, 1384 nextResourceId, 1385 outcome, 1386 nextRespEntry, 1387 resourceType, 1388 res, 1389 theRequest); 1390 } 1391 entriesToProcess.put(nextRespEntry, outcome.getId(), nextRespEntry); 1392 1393 break; 1394 } 1395 case "GET": 1396 break; 1397 default: 1398 throw new InvalidRequestException( 1399 Msg.code(538) + "Unable to handle verb in transaction: " + verb); 1400 } 1401 1402 theTransactionStopWatch.endCurrentTask(); 1403 } 1404 1405 /* 1406 * Make sure that there are no conflicts from deletions. E.g. we can't delete something 1407 * if something else has a reference to it.. Unless the thing that has a reference to it 1408 * was also deleted as a part of this transaction, which is why we check this now at the 1409 * end. 1410 */ 1411 checkForDeleteConflicts(deleteConflicts, deletedResources, updatedResources); 1412 1413 theIdToPersistedOutcome.entrySet().forEach(idAndOutcome -> { 1414 theTransactionDetails.addResolvedResourceId( 1415 idAndOutcome.getKey(), idAndOutcome.getValue().getPersistentId()); 1416 }); 1417 1418 /* 1419 * Perform ID substitutions and then index each resource we have saved 1420 */ 1421 1422 resolveReferencesThenSaveAndIndexResources( 1423 theRequest, 1424 theTransactionDetails, 1425 theIdSubstitutions, 1426 theIdToPersistedOutcome, 1427 theTransactionStopWatch, 1428 entriesToProcess, 1429 nonUpdatedEntities, 1430 updatedEntities); 1431 1432 theTransactionStopWatch.endCurrentTask(); 1433 1434 // flush writes to db 1435 theTransactionStopWatch.startTask("Flush writes to database"); 1436 1437 // flush the changes 1438 flushSession(theIdToPersistedOutcome); 1439 1440 theTransactionStopWatch.endCurrentTask(); 1441 1442 /* 1443 * Double check we didn't allow any duplicates we shouldn't have 1444 */ 1445 if (conditionalRequestUrls.size() > 0) { 1446 theTransactionStopWatch.startTask("Check for conflicts in conditional resources"); 1447 } 1448 if (!myStorageSettings.isMassIngestionMode()) { 1449 validateNoDuplicates( 1450 theRequest, theActionName, conditionalRequestUrls, theIdToPersistedOutcome.values()); 1451 } 1452 1453 theTransactionStopWatch.endCurrentTask(); 1454 if (conditionalUrlToIdMap.size() > 0) { 1455 theTransactionStopWatch.startTask( 1456 "Check that all conditionally created/updated entities actually match their conditionals."); 1457 } 1458 1459 if (!myStorageSettings.isMassIngestionMode()) { 1460 validateAllInsertsMatchTheirConditionalUrls(theIdToPersistedOutcome, conditionalUrlToIdMap, theRequest); 1461 } 1462 theTransactionStopWatch.endCurrentTask(); 1463 1464 for (IIdType next : theAllIds) { 1465 IIdType replacement = theIdSubstitutions.getForSource(next); 1466 if (replacement != null && !replacement.equals(next)) { 1467 ourLog.debug( 1468 "Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); 1469 } 1470 } 1471 1472 ListMultimap<Pointcut, HookParams> deferredBroadcastEvents = 1473 theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts(); 1474 for (Map.Entry<Pointcut, HookParams> nextEntry : deferredBroadcastEvents.entries()) { 1475 Pointcut nextPointcut = nextEntry.getKey(); 1476 HookParams nextParams = nextEntry.getValue(); 1477 CompositeInterceptorBroadcaster.doCallHooks( 1478 myInterceptorBroadcaster, theRequest, nextPointcut, nextParams); 1479 } 1480 1481 DeferredInterceptorBroadcasts deferredInterceptorBroadcasts = 1482 new DeferredInterceptorBroadcasts(deferredBroadcastEvents); 1483 HookParams params = new HookParams() 1484 .add(RequestDetails.class, theRequest) 1485 .addIfMatchesType(ServletRequestDetails.class, theRequest) 1486 .add(DeferredInterceptorBroadcasts.class, deferredInterceptorBroadcasts) 1487 .add(TransactionDetails.class, theTransactionDetails) 1488 .add(IBaseBundle.class, theResponse); 1489 CompositeInterceptorBroadcaster.doCallHooks( 1490 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_TRANSACTION_PROCESSED, params); 1491 1492 theTransactionDetails.deferredBroadcastProcessingFinished(); 1493 1494 // finishedCallingDeferredInterceptorBroadcasts 1495 1496 return entriesToProcess; 1497 1498 } finally { 1499 if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts()) { 1500 theTransactionDetails.endAcceptingDeferredInterceptorBroadcasts(); 1501 } 1502 } 1503 } 1504 1505 /** 1506 * Check for if a resource id should be matched in a conditional update 1507 * If the FHIR version is older than R4, it follows the old specifications and does not match 1508 * If the resource id has been resolved, then it is an existing resource and does not need to be matched 1509 * If the resource id is local or a placeholder, the id is temporary and should not be matched 1510 */ 1511 private boolean shouldConditionalUpdateMatchId(TransactionDetails theTransactionDetails, IIdType theId) { 1512 if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.R4)) { 1513 return false; 1514 } 1515 if (theTransactionDetails.hasResolvedResourceId(theId) 1516 && !theTransactionDetails.isResolvedResourceIdEmpty(theId)) { 1517 return false; 1518 } 1519 if (theId != null && theId.getValue() != null) { 1520 return !(theId.getValue().startsWith("urn:") || theId.getValue().startsWith("#")); 1521 } 1522 return true; 1523 } 1524 1525 private boolean shouldSwapBinaryToActualResource( 1526 IBaseResource theResource, String theResourceType, IIdType theNextResourceId) { 1527 if ("Binary".equalsIgnoreCase(theResourceType) 1528 && theNextResourceId.getResourceType() != null 1529 && !theNextResourceId.getResourceType().equalsIgnoreCase("Binary")) { 1530 return true; 1531 } else { 1532 return false; 1533 } 1534 } 1535 1536 private void setConditionalUrlToBeValidatedLater( 1537 Map<String, IIdType> theConditionalUrlToIdMap, String theMatchUrl, IIdType theId) { 1538 if (!StringUtils.isBlank(theMatchUrl)) { 1539 theConditionalUrlToIdMap.put(theMatchUrl, theId); 1540 } 1541 } 1542 1543 /** 1544 * After transaction processing and resolution of indexes and references, we want to validate that the resources that were stored _actually_ 1545 * match the conditional URLs that they were brought in on. 1546 */ 1547 private void validateAllInsertsMatchTheirConditionalUrls( 1548 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1549 Map<String, IIdType> conditionalUrlToIdMap, 1550 RequestDetails theRequest) { 1551 conditionalUrlToIdMap.entrySet().stream() 1552 .filter(entry -> entry.getKey() != null) 1553 .forEach(entry -> { 1554 String matchUrl = entry.getKey(); 1555 IIdType value = entry.getValue(); 1556 DaoMethodOutcome daoMethodOutcome = theIdToPersistedOutcome.get(value); 1557 if (daoMethodOutcome != null 1558 && !daoMethodOutcome.isNop() 1559 && daoMethodOutcome.getResource() != null) { 1560 InMemoryMatchResult match = 1561 mySearchParamMatcher.match(matchUrl, daoMethodOutcome.getResource(), theRequest); 1562 if (ourLog.isDebugEnabled()) { 1563 ourLog.debug( 1564 "Checking conditional URL [{}] against resource with ID [{}]: Supported?:[{}], Matched?:[{}]", 1565 matchUrl, 1566 value, 1567 match.supported(), 1568 match.matched()); 1569 } 1570 if (match.supported()) { 1571 if (!match.matched()) { 1572 throw new PreconditionFailedException(Msg.code(539) + "Invalid conditional URL \"" 1573 + matchUrl + "\". The given resource is not matched by this URL."); 1574 } 1575 } 1576 } 1577 }); 1578 } 1579 1580 /** 1581 * Checks for any delete conflicts. 1582 * 1583 * @param theDeleteConflicts - set of delete conflicts 1584 * @param theDeletedResources - set of deleted resources 1585 * @param theUpdatedResources - list of updated resources 1586 */ 1587 private void checkForDeleteConflicts( 1588 DeleteConflictList theDeleteConflicts, 1589 Set<String> theDeletedResources, 1590 List<IBaseResource> theUpdatedResources) { 1591 for (Iterator<DeleteConflict> iter = theDeleteConflicts.iterator(); iter.hasNext(); ) { 1592 DeleteConflict nextDeleteConflict = iter.next(); 1593 1594 /* 1595 * If we have a conflict, it means we can't delete Resource/A because 1596 * Resource/B has a reference to it. We'll ignore that conflict though 1597 * if it turns out we're also deleting Resource/B in this transaction. 1598 */ 1599 if (theDeletedResources.contains( 1600 nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue())) { 1601 iter.remove(); 1602 continue; 1603 } 1604 1605 /* 1606 * And then, this is kind of a last ditch check. It's also ok to delete 1607 * Resource/A if Resource/B isn't being deleted, but it is being UPDATED 1608 * in this transaction, and the updated version of it has no references 1609 * to Resource/A any more. 1610 */ 1611 String sourceId = 1612 nextDeleteConflict.getSourceId().toUnqualifiedVersionless().getValue(); 1613 String targetId = 1614 nextDeleteConflict.getTargetId().toUnqualifiedVersionless().getValue(); 1615 Optional<IBaseResource> updatedSource = theUpdatedResources.stream() 1616 .filter(t -> sourceId.equals( 1617 t.getIdElement().toUnqualifiedVersionless().getValue())) 1618 .findFirst(); 1619 if (updatedSource.isPresent()) { 1620 List<ResourceReferenceInfo> referencesInSource = 1621 myContext.newTerser().getAllResourceReferences(updatedSource.get()); 1622 boolean sourceStillReferencesTarget = referencesInSource.stream() 1623 .anyMatch(t -> targetId.equals(t.getResourceReference() 1624 .getReferenceElement() 1625 .toUnqualifiedVersionless() 1626 .getValue())); 1627 if (!sourceStillReferencesTarget) { 1628 iter.remove(); 1629 } 1630 } 1631 } 1632 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(myContext, theDeleteConflicts); 1633 } 1634 1635 /** 1636 * This method replaces any placeholder references in the 1637 * source transaction Bundle with their actual targets, then stores the resource contents and indexes 1638 * in the database. This is trickier than you'd think because of a couple of possibilities during the 1639 * save: 1640 * * There may be resources that have not changed (e.g. an update/PUT with a resource body identical 1641 * to what is already in the database) 1642 * * There may be resources with auto-versioned references, meaning we're replacing certain references 1643 * in the resource with a versioned references, referencing the current version at the time of the 1644 * transaction processing 1645 * * There may by auto-versioned references pointing to these unchanged targets 1646 * <p> 1647 * If we're not doing any auto-versioned references, we'll just iterate through all resources in the 1648 * transaction and save them one at a time. 1649 * <p> 1650 * However, if we have any auto-versioned references we do this in 2 passes: First the resources from the 1651 * transaction that don't have any auto-versioned references are stored. We do them first since there's 1652 * a chance they may be a NOP and we'll need to account for their version number not actually changing. 1653 * Then we do a second pass for any resources that have auto-versioned references. These happen in a separate 1654 * pass because it's too complex to try and insert the auto-versioned references and still 1655 * account for NOPs, so we block NOPs in that pass. 1656 */ 1657 private void resolveReferencesThenSaveAndIndexResources( 1658 RequestDetails theRequest, 1659 TransactionDetails theTransactionDetails, 1660 IdSubstitutionMap theIdSubstitutions, 1661 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1662 StopWatch theTransactionStopWatch, 1663 EntriesToProcessMap entriesToProcess, 1664 Set<IIdType> nonUpdatedEntities, 1665 Set<IBasePersistedResource> updatedEntities) { 1666 FhirTerser terser = myContext.newTerser(); 1667 theTransactionStopWatch.startTask("Index " + theIdToPersistedOutcome.size() + " resources"); 1668 IdentityHashMap<DaoMethodOutcome, Set<IBaseReference>> deferredIndexesForAutoVersioning = null; 1669 int i = 0; 1670 for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { 1671 1672 if (i++ % 250 == 0) { 1673 ourLog.debug( 1674 "Have indexed {} entities out of {} in transaction", 1675 i, 1676 theIdToPersistedOutcome.values().size()); 1677 } 1678 1679 if (nextOutcome.isNop()) { 1680 continue; 1681 } 1682 1683 IBaseResource nextResource = nextOutcome.getResource(); 1684 if (nextResource == null) { 1685 continue; 1686 } 1687 1688 Set<IBaseReference> referencesToAutoVersion = 1689 BaseStorageDao.extractReferencesToAutoVersion(myContext, myStorageSettings, nextResource); 1690 if (referencesToAutoVersion.isEmpty()) { 1691 // no references to autoversion - we can do the resolve and save now 1692 resolveReferencesThenSaveAndIndexResource( 1693 theRequest, 1694 theTransactionDetails, 1695 theIdSubstitutions, 1696 theIdToPersistedOutcome, 1697 entriesToProcess, 1698 nonUpdatedEntities, 1699 updatedEntities, 1700 terser, 1701 nextOutcome, 1702 nextResource, 1703 referencesToAutoVersion); // this is empty 1704 } else { 1705 // we have autoversioned things to defer until later 1706 if (deferredIndexesForAutoVersioning == null) { 1707 deferredIndexesForAutoVersioning = new IdentityHashMap<>(); 1708 } 1709 deferredIndexesForAutoVersioning.put(nextOutcome, referencesToAutoVersion); 1710 } 1711 } 1712 1713 // If we have any resources we'll be auto-versioning, index these next 1714 if (deferredIndexesForAutoVersioning != null) { 1715 for (Map.Entry<DaoMethodOutcome, Set<IBaseReference>> nextEntry : 1716 deferredIndexesForAutoVersioning.entrySet()) { 1717 DaoMethodOutcome nextOutcome = nextEntry.getKey(); 1718 Set<IBaseReference> referencesToAutoVersion = nextEntry.getValue(); 1719 IBaseResource nextResource = nextOutcome.getResource(); 1720 1721 resolveReferencesThenSaveAndIndexResource( 1722 theRequest, 1723 theTransactionDetails, 1724 theIdSubstitutions, 1725 theIdToPersistedOutcome, 1726 entriesToProcess, 1727 nonUpdatedEntities, 1728 updatedEntities, 1729 terser, 1730 nextOutcome, 1731 nextResource, 1732 referencesToAutoVersion); 1733 } 1734 } 1735 } 1736 1737 private void resolveReferencesThenSaveAndIndexResource( 1738 RequestDetails theRequest, 1739 TransactionDetails theTransactionDetails, 1740 IdSubstitutionMap theIdSubstitutions, 1741 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 1742 EntriesToProcessMap entriesToProcess, 1743 Set<IIdType> nonUpdatedEntities, 1744 Set<IBasePersistedResource> updatedEntities, 1745 FhirTerser terser, 1746 DaoMethodOutcome theDaoMethodOutcome, 1747 IBaseResource theResource, 1748 Set<IBaseReference> theReferencesToAutoVersion) { 1749 // References 1750 List<ResourceReferenceInfo> allRefs = terser.getAllResourceReferences(theResource); 1751 for (ResourceReferenceInfo nextRef : allRefs) { 1752 IBaseReference resourceReference = nextRef.getResourceReference(); 1753 IIdType nextId = resourceReference.getReferenceElement(); 1754 IIdType newId = null; 1755 if (!nextId.hasIdPart()) { 1756 if (resourceReference.getResource() != null) { 1757 IIdType targetId = resourceReference.getResource().getIdElement(); 1758 if (targetId.getValue() == null || targetId.getValue().startsWith("#")) { 1759 // This means it's a contained resource 1760 continue; 1761 } else if (theIdSubstitutions.containsTarget(targetId)) { 1762 newId = targetId; 1763 } else { 1764 throw new InternalErrorException(Msg.code(540) 1765 + "References by resource with no reference ID are not supported in DAO layer"); 1766 } 1767 } else { 1768 continue; 1769 } 1770 } 1771 if (newId != null || theIdSubstitutions.containsSource(nextId)) { 1772 if (newId == null) { 1773 newId = theIdSubstitutions.getForSource(nextId); 1774 } 1775 if (newId != null) { 1776 ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); 1777 1778 if (theReferencesToAutoVersion.contains(resourceReference)) { 1779 replaceResourceReference(newId, resourceReference, theTransactionDetails); 1780 } else { 1781 replaceResourceReference(newId.toVersionless(), resourceReference, theTransactionDetails); 1782 } 1783 } 1784 } else if (nextId.getValue().startsWith("urn:")) { 1785 throw new InvalidRequestException( 1786 Msg.code(541) + "Unable to satisfy placeholder ID " + nextId.getValue() 1787 + " found in element named '" + nextRef.getName() + "' within resource of type: " 1788 + theResource.getIdElement().getResourceType()); 1789 } else { 1790 // get a map of 1791 // existing ids -> PID (for resources that exist in the DB) 1792 // should this be allPartitions? 1793 ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds( 1794 RequestPartitionId.allPartitions(), 1795 theReferencesToAutoVersion.stream() 1796 .map(IBaseReference::getReferenceElement) 1797 .collect(Collectors.toList())); 1798 1799 for (IBaseReference baseRef : theReferencesToAutoVersion) { 1800 IIdType id = baseRef.getReferenceElement(); 1801 if (!resourceVersionMap.containsKey(id) 1802 && myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) { 1803 // not in the db, but autocreateplaceholders is true 1804 // so the version we'll set is "1" (since it will be 1805 // created later) 1806 String newRef = id.withVersion("1").getValue(); 1807 id.setValue(newRef); 1808 } else { 1809 // we will add the looked up info to the transaction 1810 // for later 1811 theTransactionDetails.addResolvedResourceId(id, resourceVersionMap.getResourcePersistentId(id)); 1812 } 1813 } 1814 1815 if (theReferencesToAutoVersion.contains(resourceReference)) { 1816 DaoMethodOutcome outcome = theIdToPersistedOutcome.get(nextId); 1817 1818 if (outcome != null && !outcome.isNop() && !Boolean.TRUE.equals(outcome.getCreated())) { 1819 replaceResourceReference(nextId, resourceReference, theTransactionDetails); 1820 } 1821 1822 // if referenced resource is not in transaction but exists in the DB, resolving its version 1823 IResourcePersistentId persistedReferenceId = resourceVersionMap.getResourcePersistentId(nextId); 1824 if (outcome == null && persistedReferenceId != null && persistedReferenceId.getVersion() != null) { 1825 IIdType newReferenceId = nextId.withVersion( 1826 persistedReferenceId.getVersion().toString()); 1827 replaceResourceReference(newReferenceId, resourceReference, theTransactionDetails); 1828 } 1829 } 1830 } 1831 } 1832 1833 // URIs 1834 Class<? extends IPrimitiveType<?>> uriType = (Class<? extends IPrimitiveType<?>>) 1835 myContext.getElementDefinition("uri").getImplementingClass(); 1836 List<? extends IPrimitiveType<?>> allUris = terser.getAllPopulatedChildElementsOfType(theResource, uriType); 1837 for (IPrimitiveType<?> nextRef : allUris) { 1838 if (nextRef instanceof IIdType) { 1839 continue; // No substitution on the resource ID itself! 1840 } 1841 String nextUriString = nextRef.getValueAsString(); 1842 if (isNotBlank(nextUriString)) { 1843 if (theIdSubstitutions.containsSource(nextUriString)) { 1844 IIdType newId = theIdSubstitutions.getForSource(nextUriString); 1845 ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); 1846 1847 String existingValue = nextRef.getValueAsString(); 1848 theTransactionDetails.addRollbackUndoAction(() -> nextRef.setValueAsString(existingValue)); 1849 1850 nextRef.setValueAsString(newId.toVersionless().getValue()); 1851 } else { 1852 ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); 1853 } 1854 } 1855 } 1856 1857 IPrimitiveType<Date> deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(theResource); 1858 Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; 1859 1860 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(theResource.getClass()); 1861 IJpaDao jpaDao = (IJpaDao) dao; 1862 1863 IBasePersistedResource updateOutcome = null; 1864 if (updatedEntities.contains(theDaoMethodOutcome.getEntity())) { 1865 boolean forceUpdateVersion = !theReferencesToAutoVersion.isEmpty(); 1866 String matchUrl = theDaoMethodOutcome.getMatchUrl(); 1867 RestOperationTypeEnum operationType = theDaoMethodOutcome.getOperationType(); 1868 DaoMethodOutcome daoMethodOutcome = jpaDao.updateInternal( 1869 theRequest, 1870 theResource, 1871 matchUrl, 1872 true, 1873 forceUpdateVersion, 1874 theDaoMethodOutcome.getEntity(), 1875 theResource.getIdElement(), 1876 theDaoMethodOutcome.getPreviousResource(), 1877 operationType, 1878 theTransactionDetails); 1879 updateOutcome = daoMethodOutcome.getEntity(); 1880 theDaoMethodOutcome = daoMethodOutcome; 1881 } else if (!nonUpdatedEntities.contains(theDaoMethodOutcome.getId())) { 1882 updateOutcome = jpaDao.updateEntity( 1883 theRequest, 1884 theResource, 1885 theDaoMethodOutcome.getEntity(), 1886 deletedTimestampOrNull, 1887 true, 1888 false, 1889 theTransactionDetails, 1890 false, 1891 true); 1892 } 1893 1894 // Make sure we reflect the actual final version for the resource. 1895 if (updateOutcome != null) { 1896 IIdType newId = updateOutcome.getIdDt(); 1897 1898 IIdType entryId = entriesToProcess.getIdWithVersionlessComparison(newId); 1899 if (entryId != null && !StringUtils.equals(entryId.getValue(), newId.getValue())) { 1900 entryId.setValue(newId.getValue()); 1901 } 1902 1903 theDaoMethodOutcome.setId(newId); 1904 1905 theIdSubstitutions.updateTargets(newId); 1906 1907 if (theDaoMethodOutcome.getOperationOutcome() != null) { 1908 IBase responseEntry = entriesToProcess.getResponseBundleEntryWithVersionlessComparison(newId); 1909 myVersionAdapter.setResponseOutcome(responseEntry, theDaoMethodOutcome.getOperationOutcome()); 1910 } 1911 } 1912 } 1913 1914 private void replaceResourceReference( 1915 IIdType theReferenceId, IBaseReference theResourceReference, TransactionDetails theTransactionDetails) { 1916 addRollbackReferenceRestore(theTransactionDetails, theResourceReference); 1917 theResourceReference.setReference(theReferenceId.getValue()); 1918 theResourceReference.setResource(null); 1919 } 1920 1921 private void addRollbackReferenceRestore( 1922 TransactionDetails theTransactionDetails, IBaseReference resourceReference) { 1923 String existingValue = resourceReference.getReferenceElement().getValue(); 1924 theTransactionDetails.addRollbackUndoAction(() -> resourceReference.setReference(existingValue)); 1925 } 1926 1927 private void validateNoDuplicates( 1928 RequestDetails theRequest, 1929 String theActionName, 1930 Map<String, Class<? extends IBaseResource>> conditionalRequestUrls, 1931 Collection<DaoMethodOutcome> thePersistedOutcomes) { 1932 1933 IdentityHashMap<IBaseResource, ResourceIndexedSearchParams> resourceToIndexedParams = 1934 new IdentityHashMap<>(thePersistedOutcomes.size()); 1935 thePersistedOutcomes.stream() 1936 .filter(t -> !t.isNop()) 1937 .filter(t -> t.getEntity() 1938 instanceof ResourceTable) // N.B. GGG: This validation never occurs for mongo, as nothing is a 1939 // ResourceTable. 1940 .filter(t -> t.getEntity().getDeleted() == null) 1941 .filter(t -> t.getResource() != null) 1942 .forEach(t -> resourceToIndexedParams.put( 1943 t.getResource(), ResourceIndexedSearchParams.withLists((ResourceTable) t.getEntity()))); 1944 1945 for (Map.Entry<String, Class<? extends IBaseResource>> nextEntry : conditionalRequestUrls.entrySet()) { 1946 String matchUrl = nextEntry.getKey(); 1947 if (isNotBlank(matchUrl)) { 1948 if (matchUrl.startsWith("?") 1949 || (!matchUrl.contains("?") 1950 && UNQUALIFIED_MATCH_URL_START.matcher(matchUrl).find())) { 1951 StringBuilder b = new StringBuilder(); 1952 b.append(myContext.getResourceType(nextEntry.getValue())); 1953 if (!matchUrl.startsWith("?")) { 1954 b.append("?"); 1955 } 1956 b.append(matchUrl); 1957 matchUrl = b.toString(); 1958 } 1959 1960 if (!myInMemoryResourceMatcher.canBeEvaluatedInMemory(matchUrl).supported()) { 1961 continue; 1962 } 1963 1964 int counter = 0; 1965 for (Map.Entry<IBaseResource, ResourceIndexedSearchParams> entries : 1966 resourceToIndexedParams.entrySet()) { 1967 ResourceIndexedSearchParams indexedParams = entries.getValue(); 1968 IBaseResource resource = entries.getKey(); 1969 1970 String resourceType = myContext.getResourceType(resource); 1971 if (!matchUrl.startsWith(resourceType + "?")) { 1972 continue; 1973 } 1974 1975 if (myInMemoryResourceMatcher 1976 .match(matchUrl, resource, indexedParams, theRequest) 1977 .matched()) { 1978 counter++; 1979 if (counter > 1) { 1980 throw new InvalidRequestException(Msg.code(542) + "Unable to process " + theActionName 1981 + " - Request would cause multiple resources to match URL: \"" + matchUrl 1982 + "\". Does transaction request contain duplicates?"); 1983 } 1984 } 1985 } 1986 } 1987 } 1988 } 1989 1990 protected abstract void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome); 1991 1992 private void validateResourcePresent(IBaseResource theResource, Integer theOrder, String theVerb) { 1993 if (theResource == null) { 1994 String msg = myContext 1995 .getLocalizer() 1996 .getMessage(BaseTransactionProcessor.class, "missingMandatoryResource", theVerb, theOrder); 1997 throw new InvalidRequestException(Msg.code(543) + msg); 1998 } 1999 } 2000 2001 private IIdType newIdType(String theResourceType, String theResourceId, String theVersion) { 2002 org.hl7.fhir.r4.model.IdType id = new org.hl7.fhir.r4.model.IdType(theResourceType, theResourceId, theVersion); 2003 return myContext.getVersion().newIdType().setValue(id.getValue()); 2004 } 2005 2006 private IIdType newIdType(String theToResourceName, String theIdPart) { 2007 return newIdType(theToResourceName, theIdPart, null); 2008 } 2009 2010 @VisibleForTesting 2011 public void setDaoRegistry(DaoRegistry theDaoRegistry) { 2012 myDaoRegistry = theDaoRegistry; 2013 } 2014 2015 private IFhirResourceDao getDaoOrThrowException(Class<? extends IBaseResource> theClass) { 2016 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(theClass); 2017 if (dao == null) { 2018 Set<String> types = new TreeSet<>(myDaoRegistry.getRegisteredDaoTypes()); 2019 String type = myContext.getResourceType(theClass); 2020 String msg = myContext 2021 .getLocalizer() 2022 .getMessage(BaseTransactionProcessor.class, "unsupportedResourceType", type, types.toString()); 2023 throw new InvalidRequestException(Msg.code(544) + msg); 2024 } 2025 return dao; 2026 } 2027 2028 private String toResourceName(Class<? extends IBaseResource> theResourceType) { 2029 return myContext.getResourceType(theResourceType); 2030 } 2031 2032 public void setContext(FhirContext theContext) { 2033 myContext = theContext; 2034 } 2035 2036 /** 2037 * Extracts the transaction url from the entry and verifies it's: 2038 * * not null or blank 2039 * * is a relative url matching the resourceType it is about 2040 * <p> 2041 * Returns the transaction url (or throws an InvalidRequestException if url is not valid) 2042 */ 2043 private String extractAndVerifyTransactionUrlForEntry(IBase theEntry, String theVerb) { 2044 String url = extractTransactionUrlOrThrowException(theEntry, theVerb); 2045 2046 if (!isValidResourceTypeUrl(url)) { 2047 ourLog.debug("Invalid url. Should begin with a resource type: {}", url); 2048 String msg = 2049 myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, url); 2050 throw new InvalidRequestException(Msg.code(2006) + msg); 2051 } 2052 return url; 2053 } 2054 2055 /** 2056 * Returns true if the provided url is a valid entry request.url. 2057 * <p> 2058 * This means: 2059 * a) not an absolute url (does not start with http/https) 2060 * b) starts with either a ResourceType or /ResourceType 2061 */ 2062 private boolean isValidResourceTypeUrl(@Nonnull String theUrl) { 2063 if (UrlUtil.isAbsolute(theUrl)) { 2064 return false; 2065 } else { 2066 int queryStringIndex = theUrl.indexOf("?"); 2067 String url; 2068 if (queryStringIndex > 0) { 2069 url = theUrl.substring(0, theUrl.indexOf("?")); 2070 } else { 2071 url = theUrl; 2072 } 2073 String[] parts; 2074 if (url.startsWith("/")) { 2075 parts = url.substring(1).split("/"); 2076 } else { 2077 parts = url.split("/"); 2078 } 2079 Set<String> allResourceTypes = myContext.getResourceTypes(); 2080 2081 return allResourceTypes.contains(parts[0]); 2082 } 2083 } 2084 2085 /** 2086 * Extracts the transaction url from the entry and verifies that it is not null/blank 2087 * and returns it 2088 */ 2089 private String extractTransactionUrlOrThrowException(IBase nextEntry, String verb) { 2090 String url = myVersionAdapter.getEntryRequestUrl(nextEntry); 2091 if (isBlank(url)) { 2092 throw new InvalidRequestException(Msg.code(545) 2093 + myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionMissingUrl", verb)); 2094 } 2095 return url; 2096 } 2097 2098 private IFhirResourceDao<? extends IBaseResource> toDao(UrlUtil.UrlParts theParts, String theVerb, String theUrl) { 2099 RuntimeResourceDefinition resType; 2100 try { 2101 resType = myContext.getResourceDefinition(theParts.getResourceType()); 2102 } catch (DataFormatException e) { 2103 String msg = 2104 myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, theUrl); 2105 throw new InvalidRequestException(Msg.code(546) + msg); 2106 } 2107 IFhirResourceDao<? extends IBaseResource> dao = null; 2108 if (resType != null) { 2109 dao = myDaoRegistry.getResourceDao(resType.getImplementingClass()); 2110 } 2111 if (dao == null) { 2112 String msg = 2113 myContext.getLocalizer().getMessage(BaseStorageDao.class, "transactionInvalidUrl", theVerb, theUrl); 2114 throw new InvalidRequestException(Msg.code(547) + msg); 2115 } 2116 2117 return dao; 2118 } 2119 2120 private String toMatchUrl(IBase theEntry) { 2121 String verb = myVersionAdapter.getEntryRequestVerb(myContext, theEntry); 2122 switch (defaultString(verb)) { 2123 case "POST": 2124 return myVersionAdapter.getEntryIfNoneExist(theEntry); 2125 case "PUT": 2126 case "DELETE": 2127 case "PATCH": 2128 String url = extractTransactionUrlOrThrowException(theEntry, verb); 2129 UrlUtil.UrlParts parts = UrlUtil.parseUrl(url); 2130 if (isBlank(parts.getResourceId())) { 2131 return parts.getResourceType() + '?' + parts.getParams(); 2132 } 2133 return null; 2134 default: 2135 return null; 2136 } 2137 } 2138 2139 @VisibleForTesting 2140 public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) { 2141 myPartitionSettings = thePartitionSettings; 2142 } 2143 2144 /** 2145 * Transaction Order, per the spec: 2146 * <p> 2147 * Process any DELETE interactions 2148 * Process any POST interactions 2149 * Process any PUT interactions 2150 * Process any PATCH interactions 2151 * Process any GET interactions 2152 */ 2153 // @formatter:off 2154 public class TransactionSorter implements Comparator<IBase> { 2155 2156 private final Set<String> myPlaceholderIds; 2157 2158 public TransactionSorter(Set<String> thePlaceholderIds) { 2159 myPlaceholderIds = thePlaceholderIds; 2160 } 2161 2162 @Override 2163 public int compare(IBase theO1, IBase theO2) { 2164 int o1 = toOrder(theO1); 2165 int o2 = toOrder(theO2); 2166 2167 if (o1 == o2) { 2168 String matchUrl1 = toMatchUrl(theO1); 2169 String matchUrl2 = toMatchUrl(theO2); 2170 if (isBlank(matchUrl1) && isBlank(matchUrl2)) { 2171 return 0; 2172 } 2173 if (isBlank(matchUrl1)) { 2174 return -1; 2175 } 2176 if (isBlank(matchUrl2)) { 2177 return 1; 2178 } 2179 2180 boolean match1containsSubstitutions = false; 2181 boolean match2containsSubstitutions = false; 2182 for (String nextPlaceholder : myPlaceholderIds) { 2183 if (matchUrl1.contains(nextPlaceholder)) { 2184 match1containsSubstitutions = true; 2185 } 2186 if (matchUrl2.contains(nextPlaceholder)) { 2187 match2containsSubstitutions = true; 2188 } 2189 } 2190 2191 if (match1containsSubstitutions && match2containsSubstitutions) { 2192 return 0; 2193 } 2194 if (!match1containsSubstitutions && !match2containsSubstitutions) { 2195 return 0; 2196 } 2197 if (match1containsSubstitutions) { 2198 return 1; 2199 } else { 2200 return -1; 2201 } 2202 } 2203 2204 return o1 - o2; 2205 } 2206 2207 private int toOrder(IBase theO1) { 2208 int o1 = 0; 2209 if (myVersionAdapter.getEntryRequestVerb(myContext, theO1) != null) { 2210 switch (myVersionAdapter.getEntryRequestVerb(myContext, theO1)) { 2211 case "DELETE": 2212 o1 = 1; 2213 break; 2214 case "POST": 2215 o1 = 2; 2216 break; 2217 case "PUT": 2218 o1 = 3; 2219 break; 2220 case "PATCH": 2221 o1 = 4; 2222 break; 2223 case "GET": 2224 o1 = 5; 2225 break; 2226 default: 2227 o1 = 0; 2228 break; 2229 } 2230 } 2231 return o1; 2232 } 2233 } 2234 2235 public class RetriableBundleTask implements Runnable { 2236 2237 private final CountDownLatch myCompletedLatch; 2238 private final RequestDetails myRequestDetails; 2239 private final IBase myNextReqEntry; 2240 private final Map<Integer, Object> myResponseMap; 2241 private final int myResponseOrder; 2242 private final boolean myNestedMode; 2243 private BaseServerResponseException myLastSeenException; 2244 2245 protected RetriableBundleTask( 2246 CountDownLatch theCompletedLatch, 2247 RequestDetails theRequestDetails, 2248 Map<Integer, Object> theResponseMap, 2249 int theResponseOrder, 2250 IBase theNextReqEntry, 2251 boolean theNestedMode) { 2252 this.myCompletedLatch = theCompletedLatch; 2253 this.myRequestDetails = theRequestDetails; 2254 this.myNextReqEntry = theNextReqEntry; 2255 this.myResponseMap = theResponseMap; 2256 this.myResponseOrder = theResponseOrder; 2257 this.myNestedMode = theNestedMode; 2258 this.myLastSeenException = null; 2259 } 2260 2261 private void processBatchEntry() { 2262 IBaseBundle subRequestBundle = 2263 myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); 2264 myVersionAdapter.addEntry(subRequestBundle, myNextReqEntry); 2265 2266 IBaseBundle nextResponseBundle = processTransactionAsSubRequest( 2267 myRequestDetails, subRequestBundle, "Batch sub-request", myNestedMode); 2268 2269 IBase subResponseEntry = 2270 (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0); 2271 myResponseMap.put(myResponseOrder, subResponseEntry); 2272 2273 /* 2274 * 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 2275 */ 2276 if (myVersionAdapter.getResource(subResponseEntry) == null) { 2277 IBase nextResponseBundleFirstEntry = 2278 (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0); 2279 myResponseMap.put(myResponseOrder, nextResponseBundleFirstEntry); 2280 } 2281 } 2282 2283 private boolean processBatchEntryWithRetry() { 2284 int maxAttempts = 3; 2285 for (int attempt = 1; ; attempt++) { 2286 try { 2287 processBatchEntry(); 2288 return true; 2289 } catch (BaseServerResponseException e) { 2290 // If we catch a known and structured exception from HAPI, just fail. 2291 myLastSeenException = e; 2292 return false; 2293 } catch (Throwable t) { 2294 myLastSeenException = new InternalErrorException(t); 2295 // If we have caught a non-tag-storage failure we are unfamiliar with, or we have exceeded max 2296 // attempts, exit. 2297 if (!DaoFailureUtil.isTagStorageFailure(t) || attempt >= maxAttempts) { 2298 ourLog.error("Failure during BATCH sub transaction processing", t); 2299 return false; 2300 } 2301 } 2302 } 2303 } 2304 2305 @Override 2306 public void run() { 2307 boolean success = processBatchEntryWithRetry(); 2308 if (!success) { 2309 populateResponseMapWithLastSeenException(); 2310 } 2311 2312 // checking for the parallelism 2313 ourLog.debug("processing batch for {} is completed", myVersionAdapter.getEntryRequestUrl(myNextReqEntry)); 2314 myCompletedLatch.countDown(); 2315 } 2316 2317 private void populateResponseMapWithLastSeenException() { 2318 ServerResponseExceptionHolder caughtEx = new ServerResponseExceptionHolder(); 2319 caughtEx.setException(myLastSeenException); 2320 myResponseMap.put(myResponseOrder, caughtEx); 2321 } 2322 } 2323 2324 private static class ServerResponseExceptionHolder { 2325 private BaseServerResponseException myException; 2326 2327 public BaseServerResponseException getException() { 2328 return myException; 2329 } 2330 2331 public void setException(BaseServerResponseException myException) { 2332 this.myException = myException; 2333 } 2334 } 2335 2336 public static boolean isPlaceholder(IIdType theId) { 2337 if (theId != null && theId.getValue() != null) { 2338 return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); 2339 } 2340 return false; 2341 } 2342 2343 private static String toStatusString(int theStatusCode) { 2344 return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); 2345 } 2346 2347 /** 2348 * Given a match URL containing 2349 * 2350 * @param theIdSubstitutions 2351 * @param theMatchUrl 2352 * @return 2353 */ 2354 public static String performIdSubstitutionsInMatchUrl(IdSubstitutionMap theIdSubstitutions, String theMatchUrl) { 2355 String matchUrl = theMatchUrl; 2356 if (isNotBlank(matchUrl) && !theIdSubstitutions.isEmpty()) { 2357 int startIdx = 0; 2358 while (startIdx != -1) { 2359 2360 int endIdx = matchUrl.indexOf('&', startIdx + 1); 2361 if (endIdx == -1) { 2362 endIdx = matchUrl.length(); 2363 } 2364 2365 int equalsIdx = matchUrl.indexOf('=', startIdx + 1); 2366 2367 int searchFrom; 2368 if (equalsIdx == -1) { 2369 searchFrom = matchUrl.length(); 2370 } else if (equalsIdx >= endIdx) { 2371 // First equals we found is from a subsequent parameter 2372 searchFrom = matchUrl.length(); 2373 } else { 2374 String paramValue = matchUrl.substring(equalsIdx + 1, endIdx); 2375 boolean isUrn = isUrn(paramValue); 2376 boolean isUrnEscaped = !isUrn && isUrnEscaped(paramValue); 2377 if (isUrn || isUrnEscaped) { 2378 if (isUrnEscaped) { 2379 paramValue = UrlUtil.unescape(paramValue); 2380 } 2381 IIdType replacement = theIdSubstitutions.getForSource(paramValue); 2382 if (replacement != null) { 2383 String replacementValue; 2384 if (replacement.hasVersionIdPart()) { 2385 replacementValue = replacement.toVersionless().getValue(); 2386 } else { 2387 replacementValue = replacement.getValue(); 2388 } 2389 matchUrl = matchUrl.substring(0, equalsIdx + 1) 2390 + replacementValue 2391 + matchUrl.substring(endIdx); 2392 searchFrom = equalsIdx + 1 + replacementValue.length(); 2393 } else { 2394 searchFrom = endIdx; 2395 } 2396 } else { 2397 searchFrom = endIdx; 2398 } 2399 } 2400 2401 if (searchFrom >= matchUrl.length()) { 2402 break; 2403 } 2404 2405 startIdx = matchUrl.indexOf('&', searchFrom); 2406 } 2407 } 2408 return matchUrl; 2409 } 2410 2411 private static boolean isUrn(@Nonnull String theId) { 2412 return theId.startsWith(URN_PREFIX); 2413 } 2414 2415 private static boolean isUrnEscaped(@Nonnull String theId) { 2416 return theId.startsWith(URN_PREFIX_ESCAPED); 2417 } 2418}