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