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