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