001package ca.uhn.fhir.interceptor.executor;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster;
026import ca.uhn.fhir.interceptor.api.IBaseInterceptorService;
027import ca.uhn.fhir.interceptor.api.IPointcut;
028import ca.uhn.fhir.interceptor.api.Interceptor;
029import ca.uhn.fhir.interceptor.api.Pointcut;
030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
031import ca.uhn.fhir.util.ReflectionUtil;
032import com.google.common.annotations.VisibleForTesting;
033import com.google.common.collect.ArrayListMultimap;
034import com.google.common.collect.ListMultimap;
035import com.google.common.collect.Multimaps;
036import org.apache.commons.lang3.Validate;
037import org.apache.commons.lang3.builder.ToStringBuilder;
038import org.apache.commons.lang3.builder.ToStringStyle;
039import org.apache.commons.lang3.reflect.MethodUtils;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import javax.annotation.Nonnull;
044import javax.annotation.Nullable;
045import java.lang.annotation.Annotation;
046import java.lang.reflect.AnnotatedElement;
047import java.lang.reflect.InvocationTargetException;
048import java.lang.reflect.Method;
049import java.util.ArrayList;
050import java.util.Arrays;
051import java.util.Collection;
052import java.util.Collections;
053import java.util.Comparator;
054import java.util.HashMap;
055import java.util.IdentityHashMap;
056import java.util.List;
057import java.util.Map;
058import java.util.Optional;
059import java.util.concurrent.atomic.AtomicInteger;
060import java.util.function.Predicate;
061import java.util.stream.Collectors;
062
063public abstract class BaseInterceptorService<POINTCUT extends IPointcut> implements IBaseInterceptorService<POINTCUT>, IBaseInterceptorBroadcaster<POINTCUT> {
064        private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.class);
065        private final List<Object> myInterceptors = new ArrayList<>();
066        private final ListMultimap<POINTCUT, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
067        private final ListMultimap<POINTCUT, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
068        private final Object myRegistryMutex = new Object();
069        private final ThreadLocal<ListMultimap<POINTCUT, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>();
070        private String myName;
071        private boolean myThreadlocalInvokersEnabled = true;
072        private boolean myWarnOnInterceptorWithNoHooks = true;
073
074        /**
075         * Constructor which uses a default name of "default"
076         */
077        public BaseInterceptorService() {
078                this("default");
079        }
080
081        /**
082         * Constructor
083         *
084         * @param theName The name for this registry (useful for troubleshooting)
085         */
086        public BaseInterceptorService(String theName) {
087                super();
088                myName = theName;
089        }
090
091        /**
092         * Should a warning be issued if an interceptor is registered and it has no hooks
093         */
094        public void setWarnOnInterceptorWithNoHooks(boolean theWarnOnInterceptorWithNoHooks) {
095                myWarnOnInterceptorWithNoHooks = theWarnOnInterceptorWithNoHooks;
096        }
097
098        /**
099         * Are threadlocal interceptors enabled on this registry (defaults to true)
100         */
101        public boolean isThreadlocalInvokersEnabled() {
102                return myThreadlocalInvokersEnabled;
103        }
104
105        /**
106         * Are threadlocal interceptors enabled on this registry (defaults to true)
107         */
108        public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) {
109                myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled;
110        }
111
112        @VisibleForTesting
113        List<Object> getGlobalInterceptorsForUnitTest() {
114                return myInterceptors;
115        }
116
117        public void setName(String theName) {
118                myName = theName;
119        }
120
121        protected void registerAnonymousInterceptor(POINTCUT thePointcut, Object theInterceptor, BaseInvoker theInvoker) {
122                Validate.notNull(thePointcut);
123                Validate.notNull(theInterceptor);
124                synchronized (myRegistryMutex) {
125
126                        myAnonymousInvokers.put(thePointcut, theInvoker);
127                        if (!isInterceptorAlreadyRegistered(theInterceptor)) {
128                                myInterceptors.add(theInterceptor);
129                        }
130                }
131        }
132
133        @Override
134        public List<Object> getAllRegisteredInterceptors() {
135                synchronized (myRegistryMutex) {
136                        List<Object> retVal = new ArrayList<>();
137                        retVal.addAll(myInterceptors);
138                        return Collections.unmodifiableList(retVal);
139                }
140        }
141
142        @Override
143        @VisibleForTesting
144        public void unregisterAllInterceptors() {
145                synchronized (myRegistryMutex) {
146                        unregisterInterceptors(myAnonymousInvokers.values());
147                        unregisterInterceptors(myGlobalInvokers.values());
148                        unregisterInterceptors(myInterceptors);
149                }
150        }
151
152        @Override
153        public void unregisterInterceptors(@Nullable Collection<?> theInterceptors) {
154                if (theInterceptors != null) {
155                        new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t));
156                }
157        }
158
159        @Override
160        public void registerInterceptors(@Nullable Collection<?> theInterceptors) {
161                if (theInterceptors != null) {
162                        theInterceptors.forEach(t -> registerInterceptor(t));
163                }
164        }
165
166        @Override
167        public void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction) {
168                unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
169                unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
170        }
171
172        private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<POINTCUT, BaseInvoker> theGlobalInvokers) {
173                theGlobalInvokers.entries().removeIf(t -> theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
174        }
175
176        @Override
177        public boolean registerThreadLocalInterceptor(Object theInterceptor) {
178                if (!myThreadlocalInvokersEnabled) {
179                        return false;
180                }
181                ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
182                scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers);
183                return !invokers.isEmpty();
184
185        }
186
187        @Override
188        public void unregisterThreadLocalInterceptor(Object theInterceptor) {
189                if (myThreadlocalInvokersEnabled) {
190                        ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
191                        invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
192                        if (invokers.isEmpty()) {
193                                myThreadlocalInvokers.remove();
194                        }
195                }
196        }
197
198        private ListMultimap<POINTCUT, BaseInvoker> getThreadLocalInvokerMultimap() {
199                ListMultimap<POINTCUT, BaseInvoker> invokers = myThreadlocalInvokers.get();
200                if (invokers == null) {
201                        invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
202                        myThreadlocalInvokers.set(invokers);
203                }
204                return invokers;
205        }
206
207        @Override
208        public boolean registerInterceptor(Object theInterceptor) {
209                synchronized (myRegistryMutex) {
210
211                        if (isInterceptorAlreadyRegistered(theInterceptor)) {
212                                return false;
213                        }
214
215                        List<HookInvoker> addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers);
216                        if (addedInvokers.isEmpty()) {
217                                if (myWarnOnInterceptorWithNoHooks) {
218                                        ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName());
219                                }
220                                return false;
221                        }
222
223                        // Add to the global list
224                        myInterceptors.add(theInterceptor);
225                        sortByOrderAnnotation(myInterceptors);
226
227                        return true;
228                }
229        }
230
231        private boolean isInterceptorAlreadyRegistered(Object theInterceptor) {
232                for (Object next : myInterceptors) {
233                        if (next == theInterceptor) {
234                                return true;
235                        }
236                }
237                return false;
238        }
239
240        @Override
241        public boolean unregisterInterceptor(Object theInterceptor) {
242                synchronized (myRegistryMutex) {
243                        boolean removed = myInterceptors.removeIf(t -> t == theInterceptor);
244                        removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
245                        removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
246                        return removed;
247                }
248        }
249
250        private void sortByOrderAnnotation(List<Object> theObjects) {
251                IdentityHashMap<Object, Integer> interceptorToOrder = new IdentityHashMap<>();
252                for (Object next : theObjects) {
253                        Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class);
254                        int order = orderAnnotation != null ? orderAnnotation.order() : 0;
255                        interceptorToOrder.put(next, order);
256                }
257
258                theObjects.sort((a, b) -> {
259                        Integer orderA = interceptorToOrder.get(a);
260                        Integer orderB = interceptorToOrder.get(b);
261                        return orderA - orderB;
262                });
263        }
264
265        @Override
266        public Object callHooksAndReturnObject(POINTCUT thePointcut, HookParams theParams) {
267                assert haveAppropriateParams(thePointcut, theParams);
268                assert thePointcut.getReturnType() != void.class;
269
270                return doCallHooks(thePointcut, theParams, null);
271        }
272
273        @Override
274        public boolean hasHooks(POINTCUT thePointcut) {
275                return myGlobalInvokers.containsKey(thePointcut)
276                        || myAnonymousInvokers.containsKey(thePointcut)
277                        || hasThreadLocalHooks(thePointcut);
278        }
279
280        private boolean hasThreadLocalHooks(POINTCUT thePointcut) {
281                ListMultimap<POINTCUT, BaseInvoker> hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null;
282                return hooks != null && hooks.containsKey(thePointcut);
283        }
284
285        @Override
286        public boolean callHooks(POINTCUT thePointcut, HookParams theParams) {
287                assert haveAppropriateParams(thePointcut, theParams);
288                assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class;
289
290                Object retValObj = doCallHooks(thePointcut, theParams, true);
291                return (Boolean) retValObj;
292        }
293
294        private Object doCallHooks(POINTCUT thePointcut, HookParams theParams, Object theRetVal) {
295                // use new list for loop to avoid ConcurrentModificationException in case invoker gets added while looping
296                List<BaseInvoker> invokers = new ArrayList<>(getInvokersForPointcut(thePointcut));
297
298                /*
299                 * Call each hook in order
300                 */
301                for (BaseInvoker nextInvoker : invokers) {
302                        Object nextOutcome = nextInvoker.invoke(theParams);
303                        Class<?> pointcutReturnType = thePointcut.getReturnType();
304                        if (pointcutReturnType.equals(boolean.class)) {
305                                Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome;
306                                if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) {
307                                        ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker);
308                                        theRetVal = false;
309                                        break;
310                                }
311                        } else if (pointcutReturnType.equals(void.class) == false) {
312                                if (nextOutcome != null) {
313                                        theRetVal = nextOutcome;
314                                        break;
315                                }
316                        }
317                }
318
319                return theRetVal;
320        }
321
322        @VisibleForTesting
323        List<Object> getInterceptorsWithInvokersForPointcut(POINTCUT thePointcut) {
324                return getInvokersForPointcut(thePointcut)
325                        .stream()
326                        .map(BaseInvoker::getInterceptor)
327                        .collect(Collectors.toList());
328        }
329
330        /**
331         * Returns an ordered list of invokers for the given pointcut. Note that
332         * a new and stable list is returned to.. do whatever you want with it.
333         */
334        private List<BaseInvoker> getInvokersForPointcut(POINTCUT thePointcut) {
335                List<BaseInvoker> invokers;
336
337                synchronized (myRegistryMutex) {
338                        List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut);
339                        List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
340                        List<BaseInvoker> threadLocalInvokers = null;
341                        if (myThreadlocalInvokersEnabled) {
342                                ListMultimap<POINTCUT, BaseInvoker> pointcutToInvokers = myThreadlocalInvokers.get();
343                                if (pointcutToInvokers != null) {
344                                        threadLocalInvokers = pointcutToInvokers.get(thePointcut);
345                                }
346                        }
347                        invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers);
348                }
349
350                return invokers;
351        }
352
353        /**
354         * First argument must be the global invoker list!!
355         */
356        @SafeVarargs
357        private final List<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) {
358                List<BaseInvoker> haveOne = null;
359                boolean haveMultiple = false;
360                for (List<BaseInvoker> nextInvokerList : theInvokersLists) {
361                        if (nextInvokerList == null || nextInvokerList.isEmpty()) {
362                                continue;
363                        }
364
365                        if (haveOne == null) {
366                                haveOne = nextInvokerList;
367                        } else {
368                                haveMultiple = true;
369                        }
370                }
371
372                if (haveOne == null) {
373                        return Collections.emptyList();
374                }
375
376                List<BaseInvoker> retVal;
377
378                if (haveMultiple == false) {
379
380                        // The global list doesn't need to be sorted every time since it's sorted on
381                        // insertion each time. Doing so is a waste of cycles..
382                        if (haveOne == theInvokersLists[0]) {
383                                retVal = haveOne;
384                        } else {
385                                retVal = new ArrayList<>(haveOne);
386                                retVal.sort(Comparator.naturalOrder());
387                        }
388
389                } else {
390
391                        retVal = Arrays
392                                .stream(theInvokersLists)
393                                .filter(t -> t != null)
394                                .flatMap(t -> t.stream())
395                                .sorted()
396                                .collect(Collectors.toList());
397
398                }
399
400                return retVal;
401        }
402
403        /**
404         * Only call this when assertions are enabled, it's expensive
405         */
406        boolean haveAppropriateParams(POINTCUT thePointcut, HookParams theParams) {
407                if (theParams.getParamsForType().values().size() != thePointcut.getParameterTypes().size()) {
408                        throw new IllegalArgumentException(Msg.code(1909) + String.format("Wrong number of params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), theParams.getParamsForType().values().stream().map(t -> t != null ? t.getClass().getSimpleName() : "null").sorted().collect(Collectors.toList())));
409                }
410
411                List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
412
413                ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
414                for (Class<?> nextTypeClass : givenTypes.keySet()) {
415                        String nextTypeName = nextTypeClass.getName();
416                        for (Object nextParamValue : givenTypes.get(nextTypeClass)) {
417                                Validate.isTrue(nextParamValue == null || nextTypeClass.isAssignableFrom(nextParamValue.getClass()), "Invalid params for pointcut %s - %s is not of type %s", thePointcut.name(), nextParamValue != null ? nextParamValue.getClass() : "null", nextTypeClass);
418                                Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName);
419                        }
420                }
421
422                return true;
423        }
424
425        private List<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<POINTCUT, BaseInvoker> theInvokers) {
426                Class<?> interceptorClass = theInterceptor.getClass();
427                int typeOrder = determineOrder(interceptorClass);
428
429                List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
430
431                // Invoke the REGISTERED pointcut for any added hooks
432                addedInvokers.stream()
433                        .filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut()))
434                        .forEach(t -> t.invoke(new HookParams()));
435
436                // Register the interceptor and its various hooks
437                for (HookInvoker nextAddedHook : addedInvokers) {
438                        IPointcut nextPointcut = nextAddedHook.getPointcut();
439                        if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) {
440                                continue;
441                        }
442                        theInvokers.put((POINTCUT) nextPointcut, nextAddedHook);
443                }
444
445                // Make sure we're always sorted according to the order declared in
446                // @Order
447                for (IPointcut nextPointcut : theInvokers.keys()) {
448                        List<BaseInvoker> nextInvokerList = theInvokers.get((POINTCUT) nextPointcut);
449                        nextInvokerList.sort(Comparator.naturalOrder());
450                }
451
452                return addedInvokers;
453        }
454
455        /**
456         * @return Returns a list of any added invokers
457         */
458        private List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
459                ArrayList<HookInvoker> retVal = new ArrayList<>();
460                for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
461                        Optional<HookDescriptor> hook = scanForHook(nextMethod);
462
463                        if (hook.isPresent()) {
464                                int methodOrder = theTypeOrder;
465                                int methodOrderAnnotation = hook.get().getOrder();
466                                if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) {
467                                        methodOrder = methodOrderAnnotation;
468                                }
469
470                                retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder));
471                        }
472                }
473
474                return retVal;
475        }
476
477        protected abstract Optional<HookDescriptor> scanForHook(Method nextMethod);
478
479        protected abstract static class BaseInvoker implements Comparable<BaseInvoker> {
480
481                private final int myOrder;
482                private final Object myInterceptor;
483
484                BaseInvoker(Object theInterceptor, int theOrder) {
485                        myInterceptor = theInterceptor;
486                        myOrder = theOrder;
487                }
488
489                public Object getInterceptor() {
490                        return myInterceptor;
491                }
492
493                abstract Object invoke(HookParams theParams);
494
495                @Override
496                public int compareTo(BaseInvoker theInvoker) {
497                        return myOrder - theInvoker.myOrder;
498                }
499        }
500
501        private static class HookInvoker extends BaseInvoker {
502
503                private final Method myMethod;
504                private final Class<?>[] myParameterTypes;
505                private final int[] myParameterIndexes;
506                private final IPointcut myPointcut;
507
508                /**
509                 * Constructor
510                 */
511                private HookInvoker(HookDescriptor theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
512                        super(theInterceptor, theOrder);
513                        myPointcut = theHook.getPointcut();
514                        myParameterTypes = theHookMethod.getParameterTypes();
515                        myMethod = theHookMethod;
516
517                        Class<?> returnType = theHookMethod.getReturnType();
518                        if (myPointcut.getReturnType().equals(boolean.class)) {
519                                Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod);
520                        } else if (myPointcut.getReturnType().equals(void.class)) {
521                                Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod);
522                        } else {
523                                Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod);
524                        }
525
526                        myParameterIndexes = new int[myParameterTypes.length];
527                        Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>();
528                        for (int i = 0; i < myParameterTypes.length; i++) {
529                                AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0));
530                                myParameterIndexes[i] = counter.getAndIncrement();
531                        }
532
533                        myMethod.setAccessible(true);
534                }
535
536                @Override
537                public String toString() {
538                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
539                                .append("method", myMethod)
540                                .toString();
541                }
542
543                public IPointcut getPointcut() {
544                        return myPointcut;
545                }
546
547                /**
548                 * @return Returns true/false if the hook method returns a boolean, returns true otherwise
549                 */
550                @Override
551                Object invoke(HookParams theParams) {
552
553                        Object[] args = new Object[myParameterTypes.length];
554                        for (int i = 0; i < myParameterTypes.length; i++) {
555                                Class<?> nextParamType = myParameterTypes[i];
556                                if (nextParamType.equals(Pointcut.class)) {
557                                        args[i] = myPointcut;
558                                } else {
559                                        int nextParamIndex = myParameterIndexes[i];
560                                        Object nextParamValue = theParams.get(nextParamType, nextParamIndex);
561                                        args[i] = nextParamValue;
562                                }
563                        }
564
565                        // Invoke the method
566                        try {
567                                return myMethod.invoke(getInterceptor(), args);
568                        } catch (InvocationTargetException e) {
569                                Throwable targetException = e.getTargetException();
570                                if (myPointcut.isShouldLogAndSwallowException(targetException)) {
571                                        ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException);
572                                        return null;
573                                }
574
575                                if (targetException instanceof RuntimeException) {
576                                        throw ((RuntimeException) targetException);
577                                } else {
578                                        throw new InternalErrorException(Msg.code(1910) + "Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException);
579                                }
580                        } catch (Exception e) {
581                                throw new InternalErrorException(Msg.code(1911) + e);
582                        }
583
584                }
585
586        }
587
588        protected static class HookDescriptor {
589
590                private final IPointcut myPointcut;
591                private final int myOrder;
592
593                public HookDescriptor(IPointcut thePointcut, int theOrder) {
594                        myPointcut = thePointcut;
595                        myOrder = theOrder;
596                }
597
598                IPointcut getPointcut() {
599                        return myPointcut;
600                }
601
602                int getOrder() {
603                        return myOrder;
604                }
605
606        }
607
608        protected static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> theHookClass) {
609                T annotation;
610                if (theObject instanceof Method) {
611                        annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true);
612                } else {
613                        annotation = theObject.getAnnotation(theHookClass);
614                }
615                return Optional.ofNullable(annotation);
616        }
617
618        private static int determineOrder(Class<?> theInterceptorClass) {
619                int typeOrder = Interceptor.DEFAULT_ORDER;
620                Optional<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
621                if (typeOrderAnnotation.isPresent()) {
622                        typeOrder = typeOrderAnnotation.get().order();
623                }
624                return typeOrder;
625        }
626
627        private static String toErrorString(List<String> theParameterTypes) {
628                return theParameterTypes
629                        .stream()
630                        .sorted()
631                        .collect(Collectors.joining(","));
632        }
633
634}