
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.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 028import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 029import com.google.common.collect.ArrayListMultimap; 030import com.google.common.collect.ListMultimap; 031import jakarta.annotation.Nonnull; 032import jakarta.annotation.Nullable; 033import org.apache.commons.lang3.Validate; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.hl7.fhir.instance.model.api.IIdType; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import java.util.ArrayList; 040import java.util.Collection; 041import java.util.Collections; 042import java.util.Date; 043import java.util.EnumSet; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Map; 048import java.util.Set; 049import java.util.function.Supplier; 050 051/** 052 * This object contains runtime information that is gathered and relevant to a single <i>database transaction</i>. 053 * This doesn't mean a FHIR transaction necessarily, but rather any operation that happens within a single DB transaction 054 * (i.e. a FHIR create, read, transaction, etc.). 055 * <p> 056 * The intent with this class is to hold things we want to pass from operation to operation within a transaction in 057 * order to avoid looking things up multiple times, etc. 058 * </p> 059 * 060 * @since 5.0.0 061 */ 062public class TransactionDetails { 063 064 private static final Logger ourLog = LoggerFactory.getLogger(TransactionDetails.class); 065 public static final IResourcePersistentId NOT_FOUND = IResourcePersistentId.NOT_FOUND; 066 067 private final Date myTransactionDate; 068 private List<Runnable> myRollbackUndoActions = Collections.emptyList(); 069 private Map<String, RequestPartitionId> myResolvedPartitions = Collections.emptyMap(); 070 private Map<String, IResourcePersistentId> myResolvedResourceIds = Collections.emptyMap(); 071 /** The reverse of myResolvedResourceIds. Safe since id:pid is 1:1. */ 072 private Map<IResourcePersistentId, IIdType> myReverseResolvedResourceIds = Collections.emptyMap(); 073 074 private Map<String, IResourcePersistentId> myResolvedMatchUrls = Collections.emptyMap(); 075 private Map<String, Supplier<IBaseResource>> myResolvedResources = Collections.emptyMap(); 076 private Set<IResourcePersistentId> myDeletedResourceIds = Collections.emptySet(); 077 private Set<IResourcePersistentId> myUpdatedResourceIds = Collections.emptySet(); 078 private Map<String, Object> myUserData; 079 private ListMultimap<Pointcut, HookParams> myDeferredInterceptorBroadcasts; 080 private EnumSet<Pointcut> myDeferredInterceptorBroadcastPointcuts; 081 private boolean myFhirTransaction; 082 private List<IIdType> myAutoCreatedPlaceholderResources = Collections.emptyList(); 083 084 /** 085 * Constructor 086 */ 087 public TransactionDetails() { 088 this(new Date()); 089 } 090 091 /** 092 * Constructor 093 */ 094 public TransactionDetails(Date theTransactionDate) { 095 myTransactionDate = theTransactionDate; 096 } 097 098 /** 099 * Get the actions that should be executed if the transaction is rolled back 100 * 101 * @since 5.5.0 102 */ 103 public List<Runnable> getRollbackUndoActions() { 104 return Collections.unmodifiableList(myRollbackUndoActions); 105 } 106 107 /** 108 * Add an action that should be executed if the transaction is rolled back. If a rollback is triggered, the 109 * actions will be executed in reverse order in order to leave . 110 * 111 * @since 5.5.0 112 */ 113 public void addRollbackUndoAction(@Nonnull Runnable theRunnable) { 114 assert theRunnable != null; 115 if (myRollbackUndoActions.isEmpty()) { 116 myRollbackUndoActions = new ArrayList<>(); 117 } 118 myRollbackUndoActions.add(theRunnable); 119 } 120 121 /** 122 * Clears any previously added rollback actions 123 * 124 * @since 5.5.0 125 */ 126 public void clearRollbackUndoActions() { 127 if (!myRollbackUndoActions.isEmpty()) { 128 myRollbackUndoActions.clear(); 129 } 130 } 131 132 /** 133 * @since 7.6.0 134 */ 135 @SuppressWarnings("rawtypes") 136 public void addUpdatedResourceId(@Nonnull IResourcePersistentId theResourceId) { 137 Validate.notNull(theResourceId, "theResourceId must not be null"); 138 if (myUpdatedResourceIds.isEmpty()) { 139 myUpdatedResourceIds = new HashSet<>(); 140 } 141 myUpdatedResourceIds.add(theResourceId); 142 } 143 144 /** 145 * @since 7.6.0 146 */ 147 @SuppressWarnings("rawtypes") 148 public void addUpdatedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) { 149 for (IResourcePersistentId id : theResourceIds) { 150 addUpdatedResourceId(id); 151 } 152 } 153 154 /** 155 * @since 7.6.0 156 */ 157 @SuppressWarnings("rawtypes") 158 public Set<IResourcePersistentId> getUpdatedResourceIds() { 159 return myUpdatedResourceIds; 160 } 161 162 /** 163 * @since 6.8.0 164 */ 165 @SuppressWarnings("rawtypes") 166 public void addDeletedResourceId(@Nonnull IResourcePersistentId theResourceId) { 167 Validate.notNull(theResourceId, "theResourceId must not be null"); 168 if (myDeletedResourceIds.isEmpty()) { 169 myDeletedResourceIds = new HashSet<>(); 170 } 171 myDeletedResourceIds.add(theResourceId); 172 } 173 174 /** 175 * @since 6.8.0 176 */ 177 @SuppressWarnings("rawtypes") 178 public void addDeletedResourceIds(Collection<? extends IResourcePersistentId> theResourceIds) { 179 for (IResourcePersistentId<?> next : theResourceIds) { 180 addDeletedResourceId(next); 181 } 182 } 183 184 /** 185 * @since 6.8.0 186 */ 187 @SuppressWarnings("rawtypes") 188 @Nonnull 189 public Set<IResourcePersistentId> getDeletedResourceIds() { 190 return Collections.unmodifiableSet(myDeletedResourceIds); 191 } 192 193 /** 194 * If a resource has been resolved within the current transaction to a specific partition, we 195 * cache it here to avoid repeated lookups. 196 */ 197 public void addResolvedPartition(String theId, RequestPartitionId thePartitionId) { 198 Validate.notBlank(theId, "theId must not be blank"); 199 if (myResolvedPartitions.isEmpty()) { 200 myResolvedPartitions = new HashMap<>(); 201 } 202 myResolvedPartitions.put(theId, thePartitionId); 203 } 204 205 /** 206 * If a resource has been resolved within the current transaction to a specific partition, we 207 * cache it here to avoid repeated lookups. 208 */ 209 public RequestPartitionId getResolvedPartition(String theId) { 210 return myResolvedPartitions.get(theId); 211 } 212 213 /** 214 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 215 * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within 216 * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. 217 */ 218 @Nullable 219 public IResourcePersistentId getResolvedResourceId(IIdType theId) { 220 String idValue = theId.toUnqualifiedVersionless().getValue(); 221 return myResolvedResourceIds.get(idValue); 222 } 223 224 /** 225 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 226 * "<code>Observation/123</code>") and the actual persisted/resolved resource with this ID. 227 */ 228 @Nullable 229 public IBaseResource getResolvedResource(IIdType theId) { 230 String idValue = theId.toUnqualifiedVersionless().getValue(); 231 IBaseResource retVal = null; 232 Supplier<IBaseResource> supplier = myResolvedResources.get(idValue); 233 if (supplier != null) { 234 retVal = supplier.get(); 235 } 236 return retVal; 237 } 238 239 /** 240 * Was the given resource ID resolved previously in this transaction as not existing 241 */ 242 public boolean isResolvedResourceIdEmpty(IIdType theId) { 243 if (myResolvedResourceIds != null) { 244 if (myResolvedResourceIds.containsKey(theId.toVersionless().getValue())) { 245 return myResolvedResourceIds.get(theId.toVersionless().getValue()) == null; 246 } 247 } 248 return false; 249 } 250 251 /** 252 * Was the given resource ID resolved previously in this transaction 253 */ 254 public boolean hasResolvedResourceId(IIdType theId) { 255 if (myResolvedResourceIds != null) { 256 return myResolvedResourceIds.containsKey(theId.toVersionless().getValue()); 257 } 258 return false; 259 } 260 261 /** 262 * Returns true if the given ID was marked as not existing (i.e. someone called 263 * {@link #addResolvedResourceId(IIdType, IResourcePersistentId)} with an 264 * ID of null). 265 * 266 * @param theId The resource ID 267 * @since 8.0.0 268 */ 269 public boolean hasNullResolvedResourceId(IIdType theId) { 270 if (myResolvedResourceIds != null) { 271 String key = theId.toVersionless().getValue(); 272 if (myResolvedResourceIds.containsKey(key)) { 273 return myResolvedResourceIds.get(key) == null; 274 } 275 } 276 return false; 277 } 278 279 /** 280 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 281 * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within 282 * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. 283 */ 284 public void addResolvedResourceId(IIdType theResourceId, @Nullable IResourcePersistentId thePersistentId) { 285 assert theResourceId != null; 286 287 if (myResolvedResourceIds.isEmpty()) { 288 myResolvedResourceIds = new HashMap<>(); 289 myReverseResolvedResourceIds = new HashMap<>(); 290 } 291 String fhirId = theResourceId.toVersionless().getValue(); 292 myResolvedResourceIds.put(fhirId, thePersistentId); 293 myReverseResolvedResourceIds.put(thePersistentId, theResourceId); 294 } 295 296 /** 297 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 298 * "<code>Observation/123</code>") and the actual persisted/resolved resource. 299 * This version takes a {@link Supplier} which will only be fetched if the 300 * resource is actually needed. This is good in cases where the resource is 301 * lazy loaded. 302 */ 303 public void addResolvedResource(IIdType theResourceId, @Nonnull Supplier<IBaseResource> theResource) { 304 assert theResourceId != null; 305 306 if (myResolvedResources.isEmpty()) { 307 myResolvedResources = new HashMap<>(); 308 } 309 IIdType versionless = theResourceId.toVersionless(); 310 myResolvedResources.put(versionless.getValue(), theResource); 311 } 312 313 /** 314 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 315 * "<code>Observation/123</code>") and the actual persisted/resolved resource. 316 */ 317 public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) { 318 addResolvedResource(theResourceId, () -> theResource); 319 } 320 321 public Map<String, IResourcePersistentId> getResolvedMatchUrls() { 322 return myResolvedMatchUrls; 323 } 324 325 /** 326 * A <b>Resolved Conditional URL</b> is a mapping between a conditional URL (e.g. "<code>Patient?identifier=foo|bar</code>" or 327 * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within 328 * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. 329 */ 330 public void addResolvedMatchUrl( 331 FhirContext theFhirContext, String theConditionalUrl, @Nonnull IResourcePersistentId<?> thePersistentId) { 332 Validate.notBlank(theConditionalUrl, "theConditionalUrl must not be blank"); 333 Validate.notNull(thePersistentId, "thePersistentId must not be null"); 334 335 if (myResolvedMatchUrls.isEmpty()) { 336 myResolvedMatchUrls = new HashMap<>(); 337 } else if (matchUrlWithDiffIdExists(theConditionalUrl, thePersistentId)) { 338 String msg = theFhirContext 339 .getLocalizer() 340 .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", theConditionalUrl); 341 throw new PreconditionFailedException(Msg.code(2207) + msg); 342 } 343 myResolvedMatchUrls.put(theConditionalUrl, thePersistentId); 344 } 345 346 /** 347 * @see #addResolvedMatchUrl(FhirContext, String, IResourcePersistentId) 348 * @since 6.8.0 349 */ 350 public void removeResolvedMatchUrl(String theMatchUrl) { 351 myResolvedMatchUrls.remove(theMatchUrl); 352 } 353 354 private boolean matchUrlWithDiffIdExists(String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) { 355 if (myResolvedMatchUrls.containsKey(theConditionalUrl) 356 && myResolvedMatchUrls.get(theConditionalUrl) != NOT_FOUND) { 357 return !myResolvedMatchUrls.get(theConditionalUrl).getId().equals(thePersistentId.getId()); 358 } 359 return false; 360 } 361 362 /** 363 * This is the wall-clock time that a given transaction started. 364 */ 365 public Date getTransactionDate() { 366 return myTransactionDate; 367 } 368 369 /** 370 * Remove an item previously stored in user data 371 * 372 * @see #getUserData(String) 373 */ 374 public void clearUserData(String theKey) { 375 if (myUserData != null) { 376 myUserData.remove(theKey); 377 } 378 } 379 380 /** 381 * Sets an arbitrary object that will last the lifetime of the current transaction 382 * 383 * @see #getUserData(String) 384 */ 385 public void putUserData(String theKey, Object theValue) { 386 if (myUserData == null) { 387 myUserData = new HashMap<>(); 388 } 389 myUserData.put(theKey, theValue); 390 } 391 392 /** 393 * Gets an arbitrary object that will last the lifetime of the current transaction 394 * 395 * @see #putUserData(String, Object) 396 */ 397 @SuppressWarnings("unchecked") 398 public <T> T getUserData(String theKey) { 399 if (myUserData != null) { 400 return (T) myUserData.get(theKey); 401 } 402 return null; 403 } 404 405 /** 406 * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and 407 * puts that in the map, and returns it 408 */ 409 @SuppressWarnings("unchecked") 410 public <T> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) { 411 T retVal = getUserData(theKey); 412 if (retVal == null) { 413 retVal = theSupplier.get(); 414 putUserData(theKey, retVal); 415 } 416 return retVal; 417 } 418 419 /** 420 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 421 * 422 * @since 5.2.0 423 */ 424 public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) { 425 Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts()); 426 myDeferredInterceptorBroadcasts = ArrayListMultimap.create(); 427 myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts); 428 } 429 430 /** 431 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 432 * 433 * @since 5.2.0 434 */ 435 public boolean isAcceptingDeferredInterceptorBroadcasts() { 436 return myDeferredInterceptorBroadcasts != null; 437 } 438 439 /** 440 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 441 * 442 * @since 5.2.0 443 */ 444 public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) { 445 return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut); 446 } 447 448 /** 449 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 450 * 451 * @since 5.2.0 452 */ 453 public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() { 454 Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts()); 455 ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts; 456 myDeferredInterceptorBroadcasts = null; 457 myDeferredInterceptorBroadcastPointcuts = null; 458 return retVal; 459 } 460 461 /** 462 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 463 * 464 * @since 5.2.0 465 */ 466 public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) { 467 Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut)); 468 myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams); 469 } 470 471 public InterceptorInvocationTimingEnum getInvocationTiming(Pointcut thePointcut) { 472 if (myDeferredInterceptorBroadcasts == null) { 473 return InterceptorInvocationTimingEnum.ACTIVE; 474 } 475 List<HookParams> hookParams = myDeferredInterceptorBroadcasts.get(thePointcut); 476 return hookParams == null ? InterceptorInvocationTimingEnum.ACTIVE : InterceptorInvocationTimingEnum.DEFERRED; 477 } 478 479 public void deferredBroadcastProcessingFinished() {} 480 481 public void clearResolvedItems() { 482 myResolvedResourceIds.clear(); 483 myReverseResolvedResourceIds.clear(); 484 myResolvedMatchUrls.clear(); 485 } 486 487 public boolean hasResolvedResourceIds() { 488 return !myResolvedResourceIds.isEmpty(); 489 } 490 491 public boolean isFhirTransaction() { 492 return myFhirTransaction; 493 } 494 495 public void setFhirTransaction(boolean theFhirTransaction) { 496 myFhirTransaction = theFhirTransaction; 497 } 498 499 public void addAutoCreatedPlaceholderResource(IIdType theResource) { 500 if (myAutoCreatedPlaceholderResources.isEmpty()) { 501 myAutoCreatedPlaceholderResources = new ArrayList<>(); 502 } 503 myAutoCreatedPlaceholderResources.add(theResource); 504 } 505 506 @Nonnull 507 public List<IIdType> getAutoCreatedPlaceholderResourcesAndClear() { 508 List<IIdType> retVal = myAutoCreatedPlaceholderResources; 509 if (!myAutoCreatedPlaceholderResources.isEmpty()) { 510 myAutoCreatedPlaceholderResources = Collections.emptyList(); 511 } 512 return retVal; 513 } 514 515 public <T extends IResourcePersistentId<?>> IIdType getReverseResolvedId(T thePid) { 516 return myReverseResolvedResourceIds.get(thePid); 517 } 518}