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