001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.api.server.storage;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.Pointcut;
026import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
027import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
028import com.google.common.collect.ArrayListMultimap;
029import com.google.common.collect.ListMultimap;
030import jakarta.annotation.Nonnull;
031import jakarta.annotation.Nullable;
032import org.apache.commons.lang3.Validate;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IIdType;
035
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.Collections;
039import java.util.Date;
040import java.util.EnumSet;
041import java.util.HashMap;
042import java.util.HashSet;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046import java.util.function.Supplier;
047
048/**
049 * This object contains runtime information that is gathered and relevant to a single <i>database transaction</i>.
050 * This doesn't mean a FHIR transaction necessarily, but rather any operation that happens within a single DB transaction
051 * (i.e. a FHIR create, read, transaction, etc.).
052 * <p>
053 * The intent with this class is to hold things we want to pass from operation to operation within a transaction in
054 * order to avoid looking things up multiple times, etc.
055 * </p>
056 *
057 * @since 5.0.0
058 */
059public class TransactionDetails {
060
061        public static final IResourcePersistentId NOT_FOUND = IResourcePersistentId.NOT_FOUND;
062
063        private final Date myTransactionDate;
064        private List<Runnable> myRollbackUndoActions = Collections.emptyList();
065        private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
066        private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap();
067        private Map<String, Supplier<IBaseResource>> myResolvedResources = Collections.emptyMap();
068        private Set<IResourcePersistentId> myDeletedResourceIds = Collections.emptySet();
069        private Set<IResourcePersistentId> myUpdatedResourceIds = Collections.emptySet();
070        private Map<String, Object> myUserData;
071        private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
072        private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
073        private boolean myFhirTransaction;
074
075        /**
076         * Constructor
077         */
078        public TransactionDetails() {
079                this(new Date());
080        }
081
082        /**
083         * Constructor
084         */
085        public TransactionDetails(Date theTransactionDate) {
086                myTransactionDate = theTransactionDate;
087        }
088
089        /**
090         * Get the actions that should be executed if the transaction is rolled back
091         *
092         * @since 5.5.0
093         */
094        public List<Runnable> getRollbackUndoActions() {
095                return Collections.unmodifiableList(myRollbackUndoActions);
096        }
097
098        /**
099         * Add an action that should be executed if the transaction is rolled back
100         *
101         * @since 5.5.0
102         */
103        public void addRollbackUndoAction(@Nonnull Runnable theRunnable) {
104                assert theRunnable != null;
105                if (myRollbackUndoActions.isEmpty()) {
106                        myRollbackUndoActions = new ArrayList<>();
107                }
108                myRollbackUndoActions.add(theRunnable);
109        }
110
111        /**
112         * Clears any previously added rollback actions
113         *
114         * @since 5.5.0
115         */
116        public void clearRollbackUndoActions() {
117                if (!myRollbackUndoActions.isEmpty()) {
118                        myRollbackUndoActions.clear();
119                }
120        }
121
122        /**
123         * @since 7.6.0
124         */
125        @SuppressWarnings("rawtypes")
126        public void addUpdatedResourceId(@Nonnull IResourcePersistentId theResourceId) {
127                Validate.notNull(theResourceId, "theResourceId must not be null");
128                if (myUpdatedResourceIds.isEmpty()) {
129                        myUpdatedResourceIds = new HashSet<>();
130                }
131                myUpdatedResourceIds.add(theResourceId);
132        }
133
134        /**
135         * @since 7.6.0
136         */
137        @SuppressWarnings("rawtypes")
138        public void addUpdatedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) {
139                for (IResourcePersistentId id : theResourceIds) {
140                        addUpdatedResourceId(id);
141                }
142        }
143
144        /**
145         * @since 7.6.0
146         */
147        @SuppressWarnings("rawtypes")
148        public Set<IResourcePersistentId> getUpdatedResourceIds() {
149                return myUpdatedResourceIds;
150        }
151
152        /**
153         * @since 6.8.0
154         */
155        @SuppressWarnings("rawtypes")
156        public void addDeletedResourceId(@Nonnull IResourcePersistentId theResourceId) {
157                Validate.notNull(theResourceId, "theResourceId must not be null");
158                if (myDeletedResourceIds.isEmpty()) {
159                        myDeletedResourceIds = new HashSet<>();
160                }
161                myDeletedResourceIds.add(theResourceId);
162        }
163
164        /**
165         * @since 6.8.0
166         */
167        @SuppressWarnings("rawtypes")
168        public void addDeletedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) {
169                for (IResourcePersistentId<?> next : theResourceIds) {
170                        addDeletedResourceId(next);
171                }
172        }
173
174        /**
175         * @since 6.8.0
176         */
177        @SuppressWarnings("rawtypes")
178        @Nonnull
179        public Set<IResourcePersistentId> getDeletedResourceIds() {
180                return Collections.unmodifiableSet(myDeletedResourceIds);
181        }
182
183        /**
184         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
185         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
186         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
187         */
188        @Nullable
189        public IResourcePersistentId getResolvedResourceId(IIdType theId) {
190                String idValue = theId.toUnqualifiedVersionless().getValue();
191                return myResolvedResourceIds.get(idValue);
192        }
193
194        /**
195         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
196         * "<code>Observation/123</code>") and the actual persisted/resolved resource with this ID.
197         */
198        @Nullable
199        public IBaseResource getResolvedResource(IIdType theId) {
200                String idValue = theId.toUnqualifiedVersionless().getValue();
201                IBaseResource retVal = null;
202                Supplier<IBaseResource> supplier = myResolvedResources.get(idValue);
203                if (supplier != null) {
204                        retVal = supplier.get();
205                }
206                return retVal;
207        }
208
209        /**
210         * Was the given resource ID resolved previously in this transaction as not existing
211         */
212        public boolean isResolvedResourceIdEmpty(IIdType theId) {
213                if (myResolvedResourceIds != null) {
214                        if (myResolvedResourceIds.containsKey(theId.toVersionless().getValue())) {
215                                return myResolvedResourceIds.get(theId.toVersionless().getValue()) == null;
216                        }
217                }
218                return false;
219        }
220
221        /**
222         * Was the given resource ID resolved previously in this transaction
223         */
224        public boolean hasResolvedResourceId(IIdType theId) {
225                if (myResolvedResourceIds != null) {
226                        return myResolvedResourceIds.containsKey(theId.toVersionless().getValue());
227                }
228                return false;
229        }
230
231        /**
232         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
233         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
234         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
235         */
236        public void addResolvedResourceId(IIdType theResourceId, @Nullable IResourcePersistentId thePersistentId) {
237                assert theResourceId != null;
238
239                if (myResolvedResourceIds.isEmpty()) {
240                        myResolvedResourceIds = new HashMap<>();
241                }
242                myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId);
243        }
244
245        /**
246         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
247         * "<code>Observation/123</code>") and the actual persisted/resolved resource.
248         * This version takes a {@link Supplier} which will only be fetched if the
249         * resource is actually needed. This is good in cases where the resource is
250         * lazy loaded.
251         */
252        public void addResolvedResource(IIdType theResourceId, @Nonnull Supplier<IBaseResource> theResource) {
253                assert theResourceId != null;
254
255                if (myResolvedResources.isEmpty()) {
256                        myResolvedResources = new HashMap<>();
257                }
258                myResolvedResources.put(theResourceId.toVersionless().getValue(), theResource);
259        }
260
261        /**
262         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
263         * "<code>Observation/123</code>") and the actual persisted/resolved resource.
264         */
265        public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) {
266                addResolvedResource(theResourceId, () -> theResource);
267        }
268
269        public Map<String, IResourcePersistentId> getResolvedMatchUrls() {
270                return myResolvedMatchUrls;
271        }
272
273        /**
274         * A <b>Resolved Conditional URL</b> is a mapping between a conditional URL (e.g. "<code>Patient?identifier=foo|bar</code>" or
275         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
276         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
277         */
278        public void addResolvedMatchUrl(
279                        FhirContext theFhirContext, String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
280                Validate.notBlank(theConditionalUrl);
281                Validate.notNull(thePersistentId);
282
283                if (myResolvedMatchUrls.isEmpty()) {
284                        myResolvedMatchUrls = new HashMap<>();
285                } else if (matchUrlWithDiffIdExists(theConditionalUrl, thePersistentId)) {
286                        String msg = theFhirContext
287                                        .getLocalizer()
288                                        .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", theConditionalUrl);
289                        throw new PreconditionFailedException(Msg.code(2207) + msg);
290                }
291                myResolvedMatchUrls.put(theConditionalUrl, thePersistentId);
292        }
293
294        /**
295         * @since 6.8.0
296         * @see #addResolvedMatchUrl(FhirContext, String, IResourcePersistentId)
297         */
298        public void removeResolvedMatchUrl(String theMatchUrl) {
299                myResolvedMatchUrls.remove(theMatchUrl);
300        }
301
302        private boolean matchUrlWithDiffIdExists(String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
303                if (myResolvedMatchUrls.containsKey(theConditionalUrl)
304                                && myResolvedMatchUrls.get(theConditionalUrl) != NOT_FOUND) {
305                        return !myResolvedMatchUrls.get(theConditionalUrl).getId().equals(thePersistentId.getId());
306                }
307                return false;
308        }
309
310        /**
311         * This is the wall-clock time that a given transaction started.
312         */
313        public Date getTransactionDate() {
314                return myTransactionDate;
315        }
316
317        /**
318         * Remove an item previously stored in user data
319         *
320         * @see #getUserData(String)
321         */
322        public void clearUserData(String theKey) {
323                if (myUserData != null) {
324                        myUserData.remove(theKey);
325                }
326        }
327
328        /**
329         * Sets an arbitrary object that will last the lifetime of the current transaction
330         *
331         * @see #getUserData(String)
332         */
333        public void putUserData(String theKey, Object theValue) {
334                if (myUserData == null) {
335                        myUserData = new HashMap<>();
336                }
337                myUserData.put(theKey, theValue);
338        }
339
340        /**
341         * Gets an arbitrary object that will last the lifetime of the current transaction
342         *
343         * @see #putUserData(String, Object)
344         */
345        @SuppressWarnings("unchecked")
346        public <T> T getUserData(String theKey) {
347                if (myUserData != null) {
348                        return (T) myUserData.get(theKey);
349                }
350                return null;
351        }
352
353        /**
354         * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and
355         * puts that in the map, and returns it
356         */
357        @SuppressWarnings("unchecked")
358        public <T> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) {
359                T retVal = getUserData(theKey);
360                if (retVal == null) {
361                        retVal = theSupplier.get();
362                        putUserData(theKey, retVal);
363                }
364                return retVal;
365        }
366
367        /**
368         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
369         *
370         * @since 5.2.0
371         */
372        public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) {
373                Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts());
374                myDeferredInterceptorBroadcasts = ArrayListMultimap.create();
375                myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts);
376        }
377
378        /**
379         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
380         *
381         * @since 5.2.0
382         */
383        public boolean isAcceptingDeferredInterceptorBroadcasts() {
384                return myDeferredInterceptorBroadcasts != null;
385        }
386
387        /**
388         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
389         *
390         * @since 5.2.0
391         */
392        public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) {
393                return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut);
394        }
395
396        /**
397         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
398         *
399         * @since 5.2.0
400         */
401        public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() {
402                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts());
403                ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts;
404                myDeferredInterceptorBroadcasts = null;
405                myDeferredInterceptorBroadcastPointcuts = null;
406                return retVal;
407        }
408
409        /**
410         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
411         *
412         * @since 5.2.0
413         */
414        public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) {
415                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut));
416                myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams);
417        }
418
419        public InterceptorInvocationTimingEnum getInvocationTiming(Pointcut thePointcut) {
420                if (myDeferredInterceptorBroadcasts == null) {
421                        return InterceptorInvocationTimingEnum.ACTIVE;
422                }
423                List<HookParams> hookParams = myDeferredInterceptorBroadcasts.get(thePointcut);
424                return hookParams == null ? InterceptorInvocationTimingEnum.ACTIVE : InterceptorInvocationTimingEnum.DEFERRED;
425        }
426
427        public void deferredBroadcastProcessingFinished() {}
428
429        public void clearResolvedItems() {
430                myResolvedResourceIds.clear();
431                myResolvedMatchUrls.clear();
432        }
433
434        public boolean hasResolvedResourceIds() {
435                return !myResolvedResourceIds.isEmpty();
436        }
437
438        public boolean isFhirTransaction() {
439                return myFhirTransaction;
440        }
441
442        public void setFhirTransaction(boolean theFhirTransaction) {
443                myFhirTransaction = theFhirTransaction;
444        }
445}