001package ca.uhn.fhir.rest.api.server.storage; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 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.toUnqualifiedVersionless().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 public boolean hasResolvedResourceIds() { 298 return !myResolvedResourceIds.isEmpty(); 299 } 300} 301