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.server.provider; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 028import ca.uhn.fhir.interceptor.api.Pointcut; 029import ca.uhn.fhir.interceptor.model.RequestPartitionId; 030import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 031import ca.uhn.fhir.model.primitive.IdDt; 032import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; 033import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; 034import ca.uhn.fhir.rest.annotation.Create; 035import ca.uhn.fhir.rest.annotation.Delete; 036import ca.uhn.fhir.rest.annotation.History; 037import ca.uhn.fhir.rest.annotation.IdParam; 038import ca.uhn.fhir.rest.annotation.Read; 039import ca.uhn.fhir.rest.annotation.ResourceParam; 040import ca.uhn.fhir.rest.annotation.Search; 041import ca.uhn.fhir.rest.annotation.Update; 042import ca.uhn.fhir.rest.api.Constants; 043import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 044import ca.uhn.fhir.rest.api.MethodOutcome; 045import ca.uhn.fhir.rest.api.server.IBundleProvider; 046import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 047import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 048import ca.uhn.fhir.rest.api.server.RequestDetails; 049import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 050import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 051import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 052import ca.uhn.fhir.rest.server.IResourceProvider; 053import ca.uhn.fhir.rest.server.SimpleBundleProvider; 054import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 055import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 056import ca.uhn.fhir.rest.server.method.ResponsePage; 057import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 058import ca.uhn.fhir.util.ValidateUtil; 059import com.google.common.collect.Lists; 060import jakarta.annotation.Nonnull; 061import org.apache.commons.lang3.StringUtils; 062import org.hl7.fhir.instance.model.api.IBase; 063import org.hl7.fhir.instance.model.api.IBaseResource; 064import org.hl7.fhir.instance.model.api.IIdType; 065import org.hl7.fhir.instance.model.api.IPrimitiveType; 066import org.slf4j.Logger; 067import org.slf4j.LoggerFactory; 068 069import java.util.ArrayList; 070import java.util.Arrays; 071import java.util.Collections; 072import java.util.Date; 073import java.util.Iterator; 074import java.util.LinkedHashMap; 075import java.util.LinkedList; 076import java.util.List; 077import java.util.Map; 078import java.util.TreeMap; 079import java.util.concurrent.atomic.AtomicLong; 080import java.util.stream.Collectors; 081 082import static java.lang.Math.max; 083import static java.lang.Math.min; 084import static org.apache.commons.lang3.StringUtils.isBlank; 085 086/** 087 * This class is a simple implementation of the resource provider 088 * interface that uses a HashMap to store all resources in memory. 089 * <p> 090 * This class currently supports the following FHIR operations: 091 * </p> 092 * <ul> 093 * <li>Create</li> 094 * <li>Update existing resource</li> 095 * <li>Update non-existing resource (e.g. create with client-supplied ID)</li> 096 * <li>Delete</li> 097 * <li>Search by resource type with no parameters</li> 098 * </ul> 099 * 100 * @param <T> The resource type to support 101 */ 102public class HashMapResourceProvider<T extends IBaseResource> implements IResourceProvider { 103 private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProvider.class); 104 private final Class<T> myResourceType; 105 private final FhirContext myFhirContext; 106 private final String myResourceName; 107 private final AtomicLong myDeleteCount = new AtomicLong(0); 108 private final AtomicLong myUpdateCount = new AtomicLong(0); 109 private final AtomicLong myCreateCount = new AtomicLong(0); 110 private final AtomicLong myReadCount = new AtomicLong(0); 111 protected Map<String, TreeMap<Long, T>> myIdToVersionToResourceMap = new LinkedHashMap<>(); 112 protected Map<String, LinkedList<T>> myIdToHistory = new LinkedHashMap<>(); 113 protected LinkedList<T> myTypeHistory = new LinkedList<>(); 114 protected AtomicLong mySearchCount = new AtomicLong(0); 115 private long myNextId; 116 117 /** 118 * Constructor 119 * 120 * @param theFhirContext The FHIR context 121 * @param theResourceType The resource type to support 122 */ 123 public HashMapResourceProvider(FhirContext theFhirContext, Class<T> theResourceType) { 124 myFhirContext = theFhirContext; 125 myResourceType = theResourceType; 126 myResourceName = myFhirContext.getResourceType(theResourceType); 127 clear(); 128 } 129 130 /** 131 * Clear all data held in this resource provider 132 */ 133 public synchronized void clear() { 134 myNextId = 1; 135 myIdToVersionToResourceMap.clear(); 136 myIdToHistory.clear(); 137 myTypeHistory.clear(); 138 } 139 140 /** 141 * Clear the counts used by {@link #getCountRead()} and other count methods 142 */ 143 public synchronized void clearCounts() { 144 myReadCount.set(0L); 145 myUpdateCount.set(0L); 146 myCreateCount.set(0L); 147 myDeleteCount.set(0L); 148 mySearchCount.set(0L); 149 } 150 151 @Create 152 public synchronized MethodOutcome create(@ResourceParam T theResource, RequestDetails theRequestDetails) { 153 TransactionDetails transactionDetails = new TransactionDetails(); 154 155 createInternal(theResource, theRequestDetails, transactionDetails); 156 157 myCreateCount.incrementAndGet(); 158 159 return new MethodOutcome().setCreated(true).setResource(theResource).setId(theResource.getIdElement()); 160 } 161 162 private void createInternal( 163 @ResourceParam T theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 164 long idPart = myNextId++; 165 String idPartAsString = Long.toString(idPart); 166 Long versionIdPart = 1L; 167 168 assert !myIdToVersionToResourceMap.containsKey(idPartAsString); 169 170 store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false); 171 } 172 173 @SuppressWarnings({"unchecked"}) 174 @Delete 175 public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) { 176 TransactionDetails transactionDetails = new TransactionDetails(); 177 178 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 179 if (versions == null || versions.isEmpty()) { 180 throw new ResourceNotFoundException(Msg.code(2250) + theId); 181 } 182 183 T deletedInstance = 184 (T) myFhirContext.getResourceDefinition(myResourceType).newInstance(); 185 long nextVersion = versions.lastEntry().getKey() + 1L; 186 IIdType id = 187 store(deletedInstance, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails, true); 188 189 myDeleteCount.incrementAndGet(); 190 191 return new MethodOutcome().setId(id); 192 } 193 194 /** 195 * This method returns a simple operation count. This is mostly 196 * useful for testing purposes. 197 */ 198 public synchronized long getCountCreate() { 199 return myCreateCount.get(); 200 } 201 202 /** 203 * This method returns a simple operation count. This is mostly 204 * useful for testing purposes. 205 */ 206 public synchronized long getCountDelete() { 207 return myDeleteCount.get(); 208 } 209 210 /** 211 * This method returns a simple operation count. This is mostly 212 * useful for testing purposes. 213 */ 214 public synchronized long getCountRead() { 215 return myReadCount.get(); 216 } 217 218 /** 219 * This method returns a simple operation count. This is mostly 220 * useful for testing purposes. 221 */ 222 public synchronized long getCountSearch() { 223 return mySearchCount.get(); 224 } 225 226 /** 227 * This method returns a simple operation count. This is mostly 228 * useful for testing purposes. 229 */ 230 public synchronized long getCountUpdate() { 231 return myUpdateCount.get(); 232 } 233 234 @Override 235 public Class<T> getResourceType() { 236 return myResourceType; 237 } 238 239 private TreeMap<Long, T> getVersionToResource(String theIdPart) { 240 myIdToVersionToResourceMap.computeIfAbsent(theIdPart, t -> new TreeMap<>()); 241 return myIdToVersionToResourceMap.get(theIdPart); 242 } 243 244 @History 245 public synchronized List<IBaseResource> historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { 246 LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart()); 247 if (retVal == null) { 248 throw new ResourceNotFoundException(Msg.code(2248) + theId); 249 } 250 251 return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 252 } 253 254 @History 255 public List<T> historyType() { 256 return myTypeHistory; 257 } 258 259 @Read(version = true) 260 public T read(@IdParam IIdType theId, RequestDetails theRequestDetails) { 261 return read(theId, theRequestDetails, false); 262 } 263 264 public synchronized T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) { 265 TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart()); 266 if (versions == null || versions.isEmpty()) { 267 throw new ResourceNotFoundException(Msg.code(2247) + theId); 268 } 269 270 T retVal; 271 if (theId.hasVersionIdPart()) { 272 Long versionId = theId.getVersionIdPartAsLong(); 273 if (!versions.containsKey(versionId)) { 274 throw new ResourceNotFoundException(Msg.code(1982) + theId); 275 } else { 276 retVal = versions.get(versionId); 277 } 278 } else { 279 retVal = versions.lastEntry().getValue(); 280 } 281 282 if (retVal == null || retVal.isDeleted()) { 283 if (!theDeletedOk) { 284 throw new ResourceGoneException(Msg.code(2244) + theId); 285 } 286 } 287 288 myReadCount.incrementAndGet(); 289 290 retVal = fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 291 if (retVal == null) { 292 throw new ResourceNotFoundException(Msg.code(2243) + theId); 293 } 294 return retVal; 295 } 296 297 @Search(allowUnknownParams = true) 298 public synchronized IBundleProvider searchAll(RequestDetails theRequestDetails) { 299 mySearchCount.incrementAndGet(); 300 List<T> allResources = getAllResources(); 301 302 if (theRequestDetails.getParameters().containsKey(Constants.PARAM_ID)) { 303 for (String nextParam : theRequestDetails.getParameters().get(Constants.PARAM_ID)) { 304 List<IdDt> wantIds = Arrays.stream(nextParam.split(",")) 305 .map(StringUtils::trim) 306 .filter(StringUtils::isNotBlank) 307 .map(IdDt::new) 308 .collect(Collectors.toList()); 309 for (Iterator<T> iter = allResources.iterator(); iter.hasNext(); ) { 310 T next = iter.next(); 311 boolean found = wantIds.stream().anyMatch(t -> resourceIdMatches(next, t)); 312 if (!found) { 313 iter.remove(); 314 } 315 } 316 } 317 } 318 319 return new SimpleBundleProvider(allResources) { 320 @SuppressWarnings("unchecked") 321 @Nonnull 322 @Override 323 public List<IBaseResource> getResources( 324 int theFromIndex, 325 int theToIndex, 326 @Nonnull ResponsePage.ResponsePageBuilder theResponsePageBuilder) { 327 328 // Make sure that "from" isn't less than 0, "to" isn't more than the number available, 329 // and "from" <= "to" 330 int from = max(0, theFromIndex); 331 int to = min(theToIndex, allResources.size()); 332 to = max(from, to); 333 334 List<IBaseResource> retVal = (List<IBaseResource>) allResources.subList(from, to); 335 retVal = fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); 336 return retVal; 337 } 338 }; 339 } 340 341 @Nonnull 342 protected synchronized List<T> getAllResources() { 343 List<T> retVal = new ArrayList<>(); 344 345 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 346 if (next.isEmpty() == false) { 347 T nextResource = next.lastEntry().getValue(); 348 if (nextResource != null) { 349 if (!nextResource.isDeleted()) { 350 // Clone the resource for search results so that the 351 // stored metadata doesn't appear in the results 352 T nextResourceClone = myFhirContext.newTerser().clone(nextResource); 353 retVal.add(nextResourceClone); 354 } 355 } 356 } 357 } 358 359 return retVal; 360 } 361 362 @SuppressWarnings({"unchecked", "DataFlowIssue"}) 363 private IIdType store( 364 @Nonnull T theResource, 365 String theIdPart, 366 Long theVersionIdPart, 367 RequestDetails theRequestDetails, 368 TransactionDetails theTransactionDetails, 369 boolean theDeleted) { 370 IIdType id = myFhirContext.getVersion().newIdType(); 371 String versionIdPart = Long.toString(theVersionIdPart); 372 id.setParts(null, myResourceName, theIdPart, versionIdPart); 373 theResource.setId(id); 374 375 if (theDeleted) { 376 IPrimitiveType<Date> deletedAt = (IPrimitiveType<Date>) 377 myFhirContext.getElementDefinition("instant").newInstance(); 378 deletedAt.setValue(new Date()); 379 ResourceMetadataKeyEnum.DELETED_AT.put(theResource, deletedAt); 380 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(theResource, BundleEntryTransactionMethodEnum.DELETE); 381 } else { 382 ResourceMetadataKeyEnum.DELETED_AT.put(theResource, null); 383 if (theVersionIdPart > 1) { 384 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(theResource, BundleEntryTransactionMethodEnum.PUT); 385 } else { 386 ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put( 387 theResource, BundleEntryTransactionMethodEnum.POST); 388 } 389 } 390 391 /* 392 * This is a bit of magic to make sure that the versionId attribute 393 * in the resource being stored accurately represents the version 394 * that was assigned by this provider 395 */ 396 if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { 397 ResourceMetadataKeyEnum.VERSION.put(theResource, versionIdPart); 398 } else { 399 BaseRuntimeChildDefinition metaChild = 400 myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta"); 401 List<IBase> metaValues = metaChild.getAccessor().getValues(theResource); 402 if (metaValues.size() > 0) { 403 theResource.getMeta().setVersionId(versionIdPart); 404 } 405 } 406 407 ourLog.info("Storing resource with ID: {}", id.getValue()); 408 409 if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) { 410 IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); 411 412 if (theDeleted) { 413 414 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_DELETED 415 HookParams preStorageParams = new HookParams() 416 .add(RequestDetails.class, theRequestDetails) 417 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 418 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 419 .add(TransactionDetails.class, theTransactionDetails); 420 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, preStorageParams); 421 422 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_DELETED 423 HookParams preCommitParams = new HookParams() 424 .add(RequestDetails.class, theRequestDetails) 425 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 426 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 427 .add(TransactionDetails.class, theTransactionDetails) 428 .add( 429 InterceptorInvocationTimingEnum.class, 430 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 431 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, preCommitParams); 432 433 } else if (!myIdToHistory.containsKey(theIdPart)) { 434 435 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED 436 HookParams preStorageParams = new HookParams() 437 .add(RequestDetails.class, theRequestDetails) 438 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 439 .add(IBaseResource.class, theResource) 440 .add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage 441 .add(TransactionDetails.class, theTransactionDetails); 442 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams); 443 444 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED 445 HookParams preCommitParams = new HookParams() 446 .add(RequestDetails.class, theRequestDetails) 447 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 448 .add(IBaseResource.class, theResource) 449 .add(TransactionDetails.class, theTransactionDetails) 450 .add( 451 InterceptorInvocationTimingEnum.class, 452 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 453 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams); 454 455 } else { 456 457 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED 458 HookParams preStorageParams = new HookParams() 459 .add(RequestDetails.class, theRequestDetails) 460 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 461 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 462 .add(IBaseResource.class, theResource) 463 .add(TransactionDetails.class, theTransactionDetails); 464 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 465 466 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 467 HookParams preCommitParams = new HookParams() 468 .add(RequestDetails.class, theRequestDetails) 469 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 470 .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) 471 .add(IBaseResource.class, theResource) 472 .add(TransactionDetails.class, theTransactionDetails) 473 .add( 474 InterceptorInvocationTimingEnum.class, 475 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 476 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 477 } 478 } 479 480 // Store to ID->version->resource map 481 TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart); 482 versionToResource.put(theVersionIdPart, theResource); 483 484 // Store to type history map 485 myTypeHistory.addFirst(theResource); 486 487 // Store to ID history map 488 myIdToHistory.computeIfAbsent(theIdPart, t -> new LinkedList<>()); 489 myIdToHistory.get(theIdPart).addFirst(theResource); 490 491 // Return the newly assigned ID including the version ID 492 return id; 493 } 494 495 /** 496 * @param theConditional This is provided only so that subclasses can implement if they want 497 */ 498 @Update 499 public synchronized MethodOutcome update( 500 @ResourceParam T theResource, 501 @ConditionalUrlParam String theConditional, 502 RequestDetails theRequestDetails) { 503 TransactionDetails transactionDetails = new TransactionDetails(); 504 505 ValidateUtil.isTrueOrThrowInvalidRequest( 506 isBlank(theConditional), "This server doesn't support conditional update"); 507 508 boolean created = updateInternal(theResource, theRequestDetails, transactionDetails); 509 myUpdateCount.incrementAndGet(); 510 511 return new MethodOutcome().setCreated(created).setResource(theResource).setId(theResource.getIdElement()); 512 } 513 514 private boolean updateInternal( 515 @ResourceParam T theResource, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 516 String idPartAsString = theResource.getIdElement().getIdPart(); 517 TreeMap<Long, T> versionToResource = getVersionToResource(idPartAsString); 518 519 Long versionIdPart; 520 boolean created; 521 if (versionToResource.isEmpty()) { 522 versionIdPart = 1L; 523 created = true; 524 } else { 525 versionIdPart = versionToResource.lastKey() + 1L; 526 created = false; 527 } 528 529 IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false); 530 theResource.setId(id); 531 return created; 532 } 533 534 public FhirContext getFhirContext() { 535 return myFhirContext; 536 } 537 538 /** 539 * This is a utility method that can be used to store a resource without 540 * having to use the outside API. In this case, the storage happens without 541 * any interaction with interceptors, etc. 542 * 543 * @param theResource The resource to store. If the resource has an ID, that ID is updated. 544 * @return Return the ID assigned to the stored resource 545 */ 546 public synchronized IIdType store(T theResource) { 547 if (theResource.getIdElement().hasIdPart()) { 548 updateInternal(theResource, null, new TransactionDetails()); 549 } else { 550 createInternal(theResource, null, new TransactionDetails()); 551 } 552 return theResource.getIdElement(); 553 } 554 555 /** 556 * Returns an unmodifiable list containing the current version of all resources stored in this provider 557 * 558 * @since 4.1.0 559 */ 560 public synchronized List<T> getStoredResources() { 561 List<T> retVal = new ArrayList<>(); 562 for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) { 563 retVal.add(next.lastEntry().getValue()); 564 } 565 return Collections.unmodifiableList(retVal); 566 } 567 568 private boolean resourceIdMatches(T theResource, IdDt theId) { 569 if (theId.getResourceType() == null 570 || theId.getResourceType().equals(myFhirContext.getResourceType(theResource))) { 571 if (theResource.getIdElement().getIdPart().equals(theId.getIdPart())) { 572 return true; 573 } 574 } 575 return false; 576 } 577 578 private static <T extends IBaseResource> T fireInterceptorsAndFilterAsNeeded( 579 T theResource, RequestDetails theRequestDetails) { 580 List<IBaseResource> output = 581 fireInterceptorsAndFilterAsNeeded(Lists.newArrayList(theResource), theRequestDetails); 582 if (output.size() == 1) { 583 // do not return theResource here but return whatever the interceptor returned in the list because 584 // the interceptor might have set the resource in the list to null (if it didn't want it to be returned). 585 // ConsentInterceptor might do this for example. 586 return (T) output.get(0); 587 } else { 588 return null; 589 } 590 } 591 592 protected static <T extends IBaseResource> List<IBaseResource> fireInterceptorsAndFilterAsNeeded( 593 List<T> theResources, RequestDetails theRequestDetails) { 594 List<IBaseResource> resourcesToReturn = new ArrayList<>(theResources); 595 596 if (theRequestDetails != null) { 597 IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); 598 599 // Call the STORAGE_PREACCESS_RESOURCES pointcut (used for consent/auth interceptors) 600 SimplePreResourceAccessDetails preResourceAccessDetails = 601 new SimplePreResourceAccessDetails(resourcesToReturn); 602 HookParams params = new HookParams() 603 .add(RequestDetails.class, theRequestDetails) 604 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 605 .add(IPreResourceAccessDetails.class, preResourceAccessDetails); 606 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 607 preResourceAccessDetails.applyFilterToList(); 608 609 // Call the STORAGE_PREACCESS_RESOURCES pointcut (used for consent/auth interceptors) 610 SimplePreResourceShowDetails preResourceShowDetails = new SimplePreResourceShowDetails(resourcesToReturn); 611 HookParams preShowParams = new HookParams() 612 .add(RequestDetails.class, theRequestDetails) 613 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 614 .add(IPreResourceShowDetails.class, preResourceShowDetails); 615 interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, preShowParams); 616 resourcesToReturn = preResourceShowDetails.toList(); 617 } 618 619 return resourcesToReturn; 620 } 621}