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 Map<String, Object> myUserData;
070        private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
071        private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
072        private boolean myFhirTransaction;
073
074        /**
075         * Constructor
076         */
077        public TransactionDetails() {
078                this(new Date());
079        }
080
081        /**
082         * Constructor
083         */
084        public TransactionDetails(Date theTransactionDate) {
085                myTransactionDate = theTransactionDate;
086        }
087
088        /**
089         * Get the actions that should be executed if the transaction is rolled back
090         *
091         * @since 5.5.0
092         */
093        public List<Runnable> getRollbackUndoActions() {
094                return Collections.unmodifiableList(myRollbackUndoActions);
095        }
096
097        /**
098         * Add an action that should be executed if the transaction is rolled back
099         *
100         * @since 5.5.0
101         */
102        public void addRollbackUndoAction(@Nonnull Runnable theRunnable) {
103                assert theRunnable != null;
104                if (myRollbackUndoActions.isEmpty()) {
105                        myRollbackUndoActions = new ArrayList<>();
106                }
107                myRollbackUndoActions.add(theRunnable);
108        }
109
110        /**
111         * Clears any previously added rollback actions
112         *
113         * @since 5.5.0
114         */
115        public void clearRollbackUndoActions() {
116                if (!myRollbackUndoActions.isEmpty()) {
117                        myRollbackUndoActions.clear();
118                }
119        }
120
121        /**
122         * @since 6.8.0
123         */
124        public void addDeletedResourceId(@Nonnull IResourcePersistentId theResourceId) {
125                Validate.notNull(theResourceId, "theResourceId must not be null");
126                if (myDeletedResourceIds.isEmpty()) {
127                        myDeletedResourceIds = new HashSet<>();
128                }
129                myDeletedResourceIds.add(theResourceId);
130        }
131
132        /**
133         * @since 6.8.0
134         */
135        public void addDeletedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) {
136                for (IResourcePersistentId<?> next : theResourceIds) {
137                        addDeletedResourceId(next);
138                }
139        }
140
141        /**
142         * @since 6.8.0
143         */
144        @Nonnull
145        public Set<IResourcePersistentId> getDeletedResourceIds() {
146                return Collections.unmodifiableSet(myDeletedResourceIds);
147        }
148
149        /**
150         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
151         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
152         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
153         */
154        @Nullable
155        public IResourcePersistentId getResolvedResourceId(IIdType theId) {
156                String idValue = theId.toUnqualifiedVersionless().getValue();
157                return myResolvedResourceIds.get(idValue);
158        }
159
160        /**
161         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
162         * "<code>Observation/123</code>") and the actual persisted/resolved resource with this ID.
163         */
164        @Nullable
165        public IBaseResource getResolvedResource(IIdType theId) {
166                String idValue = theId.toUnqualifiedVersionless().getValue();
167                IBaseResource retVal = null;
168                Supplier<IBaseResource> supplier = myResolvedResources.get(idValue);
169                if (supplier != null) {
170                        retVal = supplier.get();
171                }
172                return retVal;
173        }
174
175        /**
176         * Was the given resource ID resolved previously in this transaction as not existing
177         */
178        public boolean isResolvedResourceIdEmpty(IIdType theId) {
179                if (myResolvedResourceIds != null) {
180                        if (myResolvedResourceIds.containsKey(theId.toVersionless().getValue())) {
181                                return myResolvedResourceIds.get(theId.toVersionless().getValue()) == null;
182                        }
183                }
184                return false;
185        }
186
187        /**
188         * Was the given resource ID resolved previously in this transaction
189         */
190        public boolean hasResolvedResourceId(IIdType theId) {
191                if (myResolvedResourceIds != null) {
192                        return myResolvedResourceIds.containsKey(theId.toVersionless().getValue());
193                }
194                return false;
195        }
196
197        /**
198         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
199         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
200         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
201         */
202        public void addResolvedResourceId(IIdType theResourceId, @Nullable IResourcePersistentId thePersistentId) {
203                assert theResourceId != null;
204
205                if (myResolvedResourceIds.isEmpty()) {
206                        myResolvedResourceIds = new HashMap<>();
207                }
208                myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId);
209        }
210
211        /**
212         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
213         * "<code>Observation/123</code>") and the actual persisted/resolved resource.
214         * This version takes a {@link Supplier} which will only be fetched if the
215         * resource is actually needed. This is good in cases where the resource is
216         * lazy loaded.
217         */
218        public void addResolvedResource(IIdType theResourceId, @Nonnull Supplier<IBaseResource> theResource) {
219                assert theResourceId != null;
220
221                if (myResolvedResources.isEmpty()) {
222                        myResolvedResources = new HashMap<>();
223                }
224                myResolvedResources.put(theResourceId.toVersionless().getValue(), theResource);
225        }
226
227        /**
228         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
229         * "<code>Observation/123</code>") and the actual persisted/resolved resource.
230         */
231        public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) {
232                addResolvedResource(theResourceId, () -> theResource);
233        }
234
235        public Map<String, IResourcePersistentId> getResolvedMatchUrls() {
236                return myResolvedMatchUrls;
237        }
238
239        /**
240         * A <b>Resolved Conditional URL</b> is a mapping between a conditional URL (e.g. "<code>Patient?identifier=foo|bar</code>" or
241         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
242         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
243         */
244        public void addResolvedMatchUrl(
245                        FhirContext theFhirContext, String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
246                Validate.notBlank(theConditionalUrl);
247                Validate.notNull(thePersistentId);
248
249                if (myResolvedMatchUrls.isEmpty()) {
250                        myResolvedMatchUrls = new HashMap<>();
251                } else if (matchUrlWithDiffIdExists(theConditionalUrl, thePersistentId)) {
252                        String msg = theFhirContext
253                                        .getLocalizer()
254                                        .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", theConditionalUrl);
255                        throw new PreconditionFailedException(Msg.code(2207) + msg);
256                }
257                myResolvedMatchUrls.put(theConditionalUrl, thePersistentId);
258        }
259
260        /**
261         * @since 6.8.0
262         * @see #addResolvedMatchUrl(FhirContext, String, IResourcePersistentId)
263         */
264        public void removeResolvedMatchUrl(String theMatchUrl) {
265                myResolvedMatchUrls.remove(theMatchUrl);
266        }
267
268        private boolean matchUrlWithDiffIdExists(String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) {
269                if (myResolvedMatchUrls.containsKey(theConditionalUrl)
270                                && myResolvedMatchUrls.get(theConditionalUrl) != NOT_FOUND) {
271                        return !myResolvedMatchUrls.get(theConditionalUrl).getId().equals(thePersistentId.getId());
272                }
273                return false;
274        }
275
276        /**
277         * This is the wall-clock time that a given transaction started.
278         */
279        public Date getTransactionDate() {
280                return myTransactionDate;
281        }
282
283        /**
284         * Remove an item previously stored in user data
285         *
286         * @see #getUserData(String)
287         */
288        public void clearUserData(String theKey) {
289                if (myUserData != null) {
290                        myUserData.remove(theKey);
291                }
292        }
293
294        /**
295         * Sets an arbitrary object that will last the lifetime of the current transaction
296         *
297         * @see #getUserData(String)
298         */
299        public void putUserData(String theKey, Object theValue) {
300                if (myUserData == null) {
301                        myUserData = new HashMap<>();
302                }
303                myUserData.put(theKey, theValue);
304        }
305
306        /**
307         * Gets an arbitrary object that will last the lifetime of the current transaction
308         *
309         * @see #putUserData(String, Object)
310         */
311        @SuppressWarnings("unchecked")
312        public <T> T getUserData(String theKey) {
313                if (myUserData != null) {
314                        return (T) myUserData.get(theKey);
315                }
316                return null;
317        }
318
319        /**
320         * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and
321         * puts that in the map, and returns it
322         */
323        @SuppressWarnings("unchecked")
324        public <T> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) {
325                T retVal = getUserData(theKey);
326                if (retVal == null) {
327                        retVal = theSupplier.get();
328                        putUserData(theKey, retVal);
329                }
330                return retVal;
331        }
332
333        /**
334         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
335         *
336         * @since 5.2.0
337         */
338        public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) {
339                Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts());
340                myDeferredInterceptorBroadcasts = ArrayListMultimap.create();
341                myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts);
342        }
343
344        /**
345         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
346         *
347         * @since 5.2.0
348         */
349        public boolean isAcceptingDeferredInterceptorBroadcasts() {
350                return myDeferredInterceptorBroadcasts != null;
351        }
352
353        /**
354         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
355         *
356         * @since 5.2.0
357         */
358        public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) {
359                return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut);
360        }
361
362        /**
363         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
364         *
365         * @since 5.2.0
366         */
367        public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() {
368                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts());
369                ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts;
370                myDeferredInterceptorBroadcasts = null;
371                myDeferredInterceptorBroadcastPointcuts = null;
372                return retVal;
373        }
374
375        /**
376         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
377         *
378         * @since 5.2.0
379         */
380        public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) {
381                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut));
382                myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams);
383        }
384
385        public InterceptorInvocationTimingEnum getInvocationTiming(Pointcut thePointcut) {
386                if (myDeferredInterceptorBroadcasts == null) {
387                        return InterceptorInvocationTimingEnum.ACTIVE;
388                }
389                List<HookParams> hookParams = myDeferredInterceptorBroadcasts.get(thePointcut);
390                return hookParams == null ? InterceptorInvocationTimingEnum.ACTIVE : InterceptorInvocationTimingEnum.DEFERRED;
391        }
392
393        public void deferredBroadcastProcessingFinished() {}
394
395        public void clearResolvedItems() {
396                myResolvedResourceIds.clear();
397                myResolvedMatchUrls.clear();
398        }
399
400        public boolean hasResolvedResourceIds() {
401                return !myResolvedResourceIds.isEmpty();
402        }
403
404        public boolean isFhirTransaction() {
405                return myFhirTransaction;
406        }
407
408        public void setFhirTransaction(boolean theFhirTransaction) {
409                myFhirTransaction = theFhirTransaction;
410        }
411}