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