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}