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