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