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