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