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 258 if (myResolvedResourceIds.isEmpty()) { 259 myResolvedResourceIds = new HashMap<>(); 260 } 261 myResolvedResourceIds.put(theResourceId.toVersionless().getValue(), thePersistentId); 262 } 263 264 /** 265 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 266 * "<code>Observation/123</code>") and the actual persisted/resolved resource. 267 * This version takes a {@link Supplier} which will only be fetched if the 268 * resource is actually needed. This is good in cases where the resource is 269 * lazy loaded. 270 */ 271 public void addResolvedResource(IIdType theResourceId, @Nonnull Supplier<IBaseResource> theResource) { 272 assert theResourceId != null; 273 274 if (myResolvedResources.isEmpty()) { 275 myResolvedResources = new HashMap<>(); 276 } 277 myResolvedResources.put(theResourceId.toVersionless().getValue(), theResource); 278 } 279 280 /** 281 * A <b>Resolved Resource ID</b> is a mapping between a resource ID (e.g. "<code>Patient/ABC</code>" or 282 * "<code>Observation/123</code>") and the actual persisted/resolved resource. 283 */ 284 public void addResolvedResource(IIdType theResourceId, @Nonnull IBaseResource theResource) { 285 addResolvedResource(theResourceId, () -> theResource); 286 } 287 288 public Map<String, IResourcePersistentId> getResolvedMatchUrls() { 289 return myResolvedMatchUrls; 290 } 291 292 /** 293 * A <b>Resolved Conditional URL</b> is a mapping between a conditional URL (e.g. "<code>Patient?identifier=foo|bar</code>" or 294 * "<code>Observation/123</code>") and a storage ID for that resource. Resources should only be placed within 295 * the TransactionDetails if they are known to exist and be valid targets for other resources to link to. 296 */ 297 public void addResolvedMatchUrl( 298 FhirContext theFhirContext, String theConditionalUrl, @Nonnull IResourcePersistentId<?> thePersistentId) { 299 Validate.notBlank(theConditionalUrl, "theConditionalUrl must not be blank"); 300 Validate.notNull(thePersistentId, "thePersistentId must not be null"); 301 302 if (myResolvedMatchUrls.isEmpty()) { 303 myResolvedMatchUrls = new HashMap<>(); 304 } else if (matchUrlWithDiffIdExists(theConditionalUrl, thePersistentId)) { 305 String msg = theFhirContext 306 .getLocalizer() 307 .getMessage(TransactionDetails.class, "invalidMatchUrlMultipleMatches", theConditionalUrl); 308 throw new PreconditionFailedException(Msg.code(2207) + msg); 309 } 310 myResolvedMatchUrls.put(theConditionalUrl, thePersistentId); 311 } 312 313 /** 314 * @see #addResolvedMatchUrl(FhirContext, String, IResourcePersistentId) 315 * @since 6.8.0 316 */ 317 public void removeResolvedMatchUrl(String theMatchUrl) { 318 myResolvedMatchUrls.remove(theMatchUrl); 319 } 320 321 private boolean matchUrlWithDiffIdExists(String theConditionalUrl, @Nonnull IResourcePersistentId thePersistentId) { 322 if (myResolvedMatchUrls.containsKey(theConditionalUrl) 323 && myResolvedMatchUrls.get(theConditionalUrl) != NOT_FOUND) { 324 return !myResolvedMatchUrls.get(theConditionalUrl).getId().equals(thePersistentId.getId()); 325 } 326 return false; 327 } 328 329 /** 330 * This is the wall-clock time that a given transaction started. 331 */ 332 public Date getTransactionDate() { 333 return myTransactionDate; 334 } 335 336 /** 337 * Remove an item previously stored in user data 338 * 339 * @see #getUserData(String) 340 */ 341 public void clearUserData(String theKey) { 342 if (myUserData != null) { 343 myUserData.remove(theKey); 344 } 345 } 346 347 /** 348 * Sets an arbitrary object that will last the lifetime of the current transaction 349 * 350 * @see #getUserData(String) 351 */ 352 public void putUserData(String theKey, Object theValue) { 353 if (myUserData == null) { 354 myUserData = new HashMap<>(); 355 } 356 myUserData.put(theKey, theValue); 357 } 358 359 /** 360 * Gets an arbitrary object that will last the lifetime of the current transaction 361 * 362 * @see #putUserData(String, Object) 363 */ 364 @SuppressWarnings("unchecked") 365 public <T> T getUserData(String theKey) { 366 if (myUserData != null) { 367 return (T) myUserData.get(theKey); 368 } 369 return null; 370 } 371 372 /** 373 * Fetches the existing value in the user data map, or uses {@literal theSupplier} to create a new object and 374 * puts that in the map, and returns it 375 */ 376 @SuppressWarnings("unchecked") 377 public <T> T getOrCreateUserData(String theKey, Supplier<T> theSupplier) { 378 T retVal = getUserData(theKey); 379 if (retVal == null) { 380 retVal = theSupplier.get(); 381 putUserData(theKey, retVal); 382 } 383 return retVal; 384 } 385 386 /** 387 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 388 * 389 * @since 5.2.0 390 */ 391 public void beginAcceptingDeferredInterceptorBroadcasts(Pointcut... thePointcuts) { 392 Validate.isTrue(!isAcceptingDeferredInterceptorBroadcasts()); 393 myDeferredInterceptorBroadcasts = ArrayListMultimap.create(); 394 myDeferredInterceptorBroadcastPointcuts = EnumSet.of(thePointcuts[0], thePointcuts); 395 } 396 397 /** 398 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 399 * 400 * @since 5.2.0 401 */ 402 public boolean isAcceptingDeferredInterceptorBroadcasts() { 403 return myDeferredInterceptorBroadcasts != null; 404 } 405 406 /** 407 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 408 * 409 * @since 5.2.0 410 */ 411 public boolean isAcceptingDeferredInterceptorBroadcasts(Pointcut thePointcut) { 412 return myDeferredInterceptorBroadcasts != null && myDeferredInterceptorBroadcastPointcuts.contains(thePointcut); 413 } 414 415 /** 416 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 417 * 418 * @since 5.2.0 419 */ 420 public ListMultimap<Pointcut, HookParams> endAcceptingDeferredInterceptorBroadcasts() { 421 Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts()); 422 ListMultimap<Pointcut, HookParams> retVal = myDeferredInterceptorBroadcasts; 423 myDeferredInterceptorBroadcasts = null; 424 myDeferredInterceptorBroadcastPointcuts = null; 425 return retVal; 426 } 427 428 /** 429 * This can be used by processors for FHIR transactions to defer interceptor broadcasts on sub-requests if needed 430 * 431 * @since 5.2.0 432 */ 433 public void addDeferredInterceptorBroadcast(Pointcut thePointcut, HookParams theHookParams) { 434 Validate.isTrue(isAcceptingDeferredInterceptorBroadcasts(thePointcut)); 435 myDeferredInterceptorBroadcasts.put(thePointcut, theHookParams); 436 } 437 438 public InterceptorInvocationTimingEnum getInvocationTiming(Pointcut thePointcut) { 439 if (myDeferredInterceptorBroadcasts == null) { 440 return InterceptorInvocationTimingEnum.ACTIVE; 441 } 442 List<HookParams> hookParams = myDeferredInterceptorBroadcasts.get(thePointcut); 443 return hookParams == null ? InterceptorInvocationTimingEnum.ACTIVE : InterceptorInvocationTimingEnum.DEFERRED; 444 } 445 446 public void deferredBroadcastProcessingFinished() {} 447 448 public void clearResolvedItems() { 449 myResolvedResourceIds.clear(); 450 myResolvedMatchUrls.clear(); 451 } 452 453 public boolean hasResolvedResourceIds() { 454 return !myResolvedResourceIds.isEmpty(); 455 } 456 457 public boolean isFhirTransaction() { 458 return myFhirTransaction; 459 } 460 461 public void setFhirTransaction(boolean theFhirTransaction) { 462 myFhirTransaction = theFhirTransaction; 463 } 464 465 public void addAutoCreatedPlaceholderResource(IIdType theResource) { 466 if (myAutoCreatedPlaceholderResources.isEmpty()) { 467 myAutoCreatedPlaceholderResources = new ArrayList<>(); 468 } 469 myAutoCreatedPlaceholderResources.add(theResource); 470 } 471 472 @Nonnull 473 public List<IIdType> getAutoCreatedPlaceholderResourcesAndClear() { 474 List<IIdType> retVal = myAutoCreatedPlaceholderResources; 475 if (!myAutoCreatedPlaceholderResources.isEmpty()) { 476 myAutoCreatedPlaceholderResources = Collections.emptyList(); 477 } 478 return retVal; 479 } 480}