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}