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