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