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