001package ca.uhn.fhir.rest.api.server.storage;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.interceptor.api.HookParams;
024import ca.uhn.fhir.interceptor.api.Pointcut;
025import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
026import com.google.common.collect.ArrayListMultimap;
027import com.google.common.collect.ListMultimap;
028import org.apache.commons.lang3.Validate;
029import org.hl7.fhir.instance.model.api.IIdType;
030
031import javax.annotation.Nonnull;
032import javax.annotation.Nullable;
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.Date;
036import java.util.EnumSet;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Map;
040import java.util.function.Supplier;
041
042/**
043 * This object contains runtime information that is gathered and relevant to a single <i>database transaction</i>.
044 * This doesn't mean a FHIR transaction necessarily, but rather any operation that happens within a single DB transaction
045 * (i.e. a FHIR create, read, transaction, etc.).
046 * <p>
047 * The intent with this class is to hold things we want to pass from operation to operation within a transaction in
048 * order to avoid looking things up multiple times, etc.
049 * </p>
050 *
051 * @since 5.0.0
052 */
053public class TransactionDetails {
054
055        public static final ResourcePersistentId NOT_FOUND = new ResourcePersistentId(-1L);
056
057        private final Date myTransactionDate;
058        private List<Runnable> myRollbackUndoActions = Collections.emptyList();
059        private Map<String, ResourcePersistentId> myResolvedResourceIds = Collections.emptyMap();
060        private Map<String, ResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap();
061        private Map<String, Object> myUserData;
062        private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts;
063        private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts;
064        private boolean myIsPointcutDeferred;
065
066        /**
067         * Constructor
068         */
069        public TransactionDetails() {
070                this(new Date());
071        }
072
073        /**
074         * Constructor
075         */
076        public TransactionDetails(Date theTransactionDate) {
077                myTransactionDate = theTransactionDate;
078        }
079
080        /**
081         * Get the actions that should be executed if the transaction is rolled back
082         *
083         * @since 5.5.0
084         */
085        public List<Runnable> getRollbackUndoActions() {
086                return Collections.unmodifiableList(myRollbackUndoActions);
087        }
088
089        /**
090         * Add an action that should be executed if the transaction is rolled back
091         *
092         * @since 5.5.0
093         */
094        public void addRollbackUndoAction(@Nonnull Runnable theRunnable) {
095                assert theRunnable != null;
096                if (myRollbackUndoActions.isEmpty()) {
097                        myRollbackUndoActions = new ArrayList<>();
098                }
099                myRollbackUndoActions.add(theRunnable);
100        }
101
102        /**
103         * Clears any previously added rollback actions
104         *
105         * @since 5.5.0
106         */
107        public void clearRollbackUndoActions() {
108                if (!myRollbackUndoActions.isEmpty()) {
109                        myRollbackUndoActions.clear();
110                }
111        }
112
113        /**
114         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
115         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
116         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
117         */
118        @Nullable
119        public ResourcePersistentId getResolvedResourceId(IIdType theId) {
120                String idValue = theId.toVersionless().getValue();
121                return myResolvedResourceIds.get(idValue);
122        }
123
124        /**
125         * Was the given resource ID resolved previously in this transaction as not existing
126         */
127        public boolean isResolvedResourceIdEmpty(IIdType theId) {
128                if (myResolvedResourceIds != null) {
129                        if (myResolvedResourceIds.containsKey(theId.toVersionless().getValue())) {
130                                return myResolvedResourceIds.get(theId.toVersionless().getValue()) == null;
131                        }
132                }
133                return false;
134        }
135
136
137        /**
138         * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or
139         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
140         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
141         */
142        public void addResolvedResourceId(IIdType theResourceId, @Nullable ResourcePersistentId thePersistentId) {
143                assert theResourceId != null;
144
145                if (myResolvedResourceIds.isEmpty()) {
146                        myResolvedResourceIds = new HashMap<>();
147                }
148                myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId);
149        }
150
151        public Map<String, ResourcePersistentId> getResolvedMatchUrls() {
152                return myResolvedMatchUrls;
153        }
154
155        /**
156         * A <b>Resolved Conditional URL</b> is a mapping between a conditional URL (e.g. "<code>Patient?identifier=foo|bar</code>" or
157         * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within
158         * the TransactionDetails if they are known to exist and be valid targets for other resources to link to.
159         */
160        public void addResolvedMatchUrl(String theConditionalUrl, @Nonnull ResourcePersistentId thePersistentId) {
161                Validate.notBlank(theConditionalUrl);
162                Validate.notNull(thePersistentId);
163
164                if (myResolvedMatchUrls.isEmpty()) {
165                        myResolvedMatchUrls = new HashMap<>();
166                }
167                myResolvedMatchUrls.put(theConditionalUrl, thePersistentId);
168        }
169
170        /**
171         * This is the wall-clock time that a given transaction started.
172         */
173        public Date getTransactionDate() {
174                return myTransactionDate;
175        }
176
177        /**
178         * Remove an item previously stored in user data
179         *
180         * @see #getUserData(String)
181         */
182        public void clearUserData(String theKey) {
183                if (myUserData != null) {
184                        myUserData.remove(theKey);
185                }
186        }
187
188        /**
189         * Sets an arbitrary object that will last the lifetime of the current transaction
190         *
191         * @see #getUserData(String)
192         */
193        public void putUserData(String theKey, Object theValue) {
194                if (myUserData == null) {
195                        myUserData = new HashMap<>();
196                }
197                myUserData.put(theKey, theValue);
198        }
199
200        /**
201         * Gets an arbitrary object that will last the lifetime of the current transaction
202         *
203         * @see #putUserData(String, Object)
204         */
205        @SuppressWarnings("unchecked")
206        public <T> T getUserData(String theKey) {
207                if (myUserData != null) {
208                        return (T) myUserData.get(theKey);
209                }
210                return null;
211        }
212
213        /**
214         * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and
215         * puts that in the map, and returns it
216         */
217        @SuppressWarnings("unchecked")
218        public <T> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) {
219                T retVal = getUserData(theKey);
220                if (retVal == null) {
221                        retVal = theSupplier.get();
222                        putUserData(theKey, retVal);
223                }
224                return retVal;
225        }
226
227        /**
228         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
229         *
230         * @since 5.2.0
231         */
232        public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) {
233                Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts());
234                myDeferredInterceptorBroadcasts = ArrayListMultimap.create();
235                myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts);
236        }
237
238        /**
239         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
240         *
241         * @since 5.2.0
242         */
243        public boolean isAcceptingDeferredInterceptorBroadcasts() {
244                return myDeferredInterceptorBroadcasts != null;
245        }
246
247        /**
248         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
249         *
250         * @since 5.2.0
251         */
252        public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) {
253                return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut);
254        }
255
256        /**
257         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
258         *
259         * @since 5.2.0
260         */
261        public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() {
262                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts());
263                ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts;
264                myDeferredInterceptorBroadcasts = null;
265                myDeferredInterceptorBroadcastPointcuts = null;
266                return retVal;
267        }
268
269        /**
270         * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed
271         *
272         * @since 5.2.0
273         */
274        public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) {
275                Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut));
276                myIsPointcutDeferred = true;
277                myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams);
278        }
279
280        public InterceptorInvocationTimingEnum getInvocationTiming(Pointcut thePointcut) {
281                if (myDeferredInterceptorBroadcasts == null) {
282                        return InterceptorInvocationTimingEnum.ACTIVE;
283                }
284                List<HookParams> hookParams = myDeferredInterceptorBroadcasts.get(thePointcut);
285                return hookParams == null ? InterceptorInvocationTimingEnum.ACTIVE : InterceptorInvocationTimingEnum.DEFERRED;
286        }
287
288        public void deferredBroadcastProcessingFinished() {
289                myIsPointcutDeferred = false;
290        }
291
292        public void clearResolvedItems() {
293                myResolvedResourceIds.clear();
294                myResolvedMatchUrls.clear();
295        }
296}
297