001package ca.uhn.fhir.interceptor.executor;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 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                Validate.isTrue(theParams.getParamsForType().values().size() == thePointcut.getParameterTypes().size(), "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()));
406
407                List<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
408
409                ListMultimap<Class<?>, Object> givenTypes = theParams.getParamsForType();
410                for (Class<?> nextTypeClass : givenTypes.keySet()) {
411                        String nextTypeName = nextTypeClass.getName();
412                        for (Object nextParamValue : givenTypes.get(nextTypeClass)) {
413                                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);
414                                Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName);
415                        }
416                }
417
418                return true;
419        }
420
421        private List<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<POINTCUT, BaseInvoker> theInvokers) {
422                Class<?> interceptorClass = theInterceptor.getClass();
423                int typeOrder = determineOrder(interceptorClass);
424
425                List<HookInvoker> addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
426
427                // Invoke the REGISTERED pointcut for any added hooks
428                addedInvokers.stream()
429                        .filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut()))
430                        .forEach(t -> t.invoke(new HookParams()));
431
432                // Register the interceptor and its various hooks
433                for (HookInvoker nextAddedHook : addedInvokers) {
434                        IPointcut nextPointcut = nextAddedHook.getPointcut();
435                        if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) {
436                                continue;
437                        }
438                        theInvokers.put((POINTCUT) nextPointcut, nextAddedHook);
439                }
440
441                // Make sure we're always sorted according to the order declared in
442                // @Order
443                for (IPointcut nextPointcut : theInvokers.keys()) {
444                        List<BaseInvoker> nextInvokerList = theInvokers.get((POINTCUT) nextPointcut);
445                        nextInvokerList.sort(Comparator.naturalOrder());
446                }
447
448                return addedInvokers;
449        }
450
451        /**
452         * @return Returns a list of any added invokers
453         */
454        private List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
455                ArrayList<HookInvoker> retVal = new ArrayList<>();
456                for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
457                        Optional<HookDescriptor> hook = scanForHook(nextMethod);
458
459                        if (hook.isPresent()) {
460                                int methodOrder = theTypeOrder;
461                                int methodOrderAnnotation = hook.get().getOrder();
462                                if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) {
463                                        methodOrder = methodOrderAnnotation;
464                                }
465
466                                retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder));
467                        }
468                }
469
470                return retVal;
471        }
472
473        protected abstract Optional<HookDescriptor> scanForHook(Method nextMethod);
474
475        protected abstract static class BaseInvoker implements Comparable<BaseInvoker> {
476
477                private final int myOrder;
478                private final Object myInterceptor;
479
480                BaseInvoker(Object theInterceptor, int theOrder) {
481                        myInterceptor = theInterceptor;
482                        myOrder = theOrder;
483                }
484
485                public Object getInterceptor() {
486                        return myInterceptor;
487                }
488
489                abstract Object invoke(HookParams theParams);
490
491                @Override
492                public int compareTo(BaseInvoker theInvoker) {
493                        return myOrder - theInvoker.myOrder;
494                }
495        }
496
497        private static class HookInvoker extends BaseInvoker {
498
499                private final Method myMethod;
500                private final Class<?>[] myParameterTypes;
501                private final int[] myParameterIndexes;
502                private final IPointcut myPointcut;
503
504                /**
505                 * Constructor
506                 */
507                private HookInvoker(HookDescriptor theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
508                        super(theInterceptor, theOrder);
509                        myPointcut = theHook.getPointcut();
510                        myParameterTypes = theHookMethod.getParameterTypes();
511                        myMethod = theHookMethod;
512
513                        Class<?> returnType = theHookMethod.getReturnType();
514                        if (myPointcut.getReturnType().equals(boolean.class)) {
515                                Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod);
516                        } else if (myPointcut.getReturnType().equals(void.class)) {
517                                Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod);
518                        } else {
519                                Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod);
520                        }
521
522                        myParameterIndexes = new int[myParameterTypes.length];
523                        Map<Class<?>, AtomicInteger> typeToCount = new HashMap<>();
524                        for (int i = 0; i < myParameterTypes.length; i++) {
525                                AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0));
526                                myParameterIndexes[i] = counter.getAndIncrement();
527                        }
528
529                        myMethod.setAccessible(true);
530                }
531
532                @Override
533                public String toString() {
534                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
535                                .append("method", myMethod)
536                                .toString();
537                }
538
539                public IPointcut getPointcut() {
540                        return myPointcut;
541                }
542
543                /**
544                 * @return Returns true/false if the hook method returns a boolean, returns true otherwise
545                 */
546                @Override
547                Object invoke(HookParams theParams) {
548
549                        Object[] args = new Object[myParameterTypes.length];
550                        for (int i = 0; i < myParameterTypes.length; i++) {
551                                Class<?> nextParamType = myParameterTypes[i];
552                                if (nextParamType.equals(Pointcut.class)) {
553                                        args[i] = myPointcut;
554                                } else {
555                                        int nextParamIndex = myParameterIndexes[i];
556                                        Object nextParamValue = theParams.get(nextParamType, nextParamIndex);
557                                        args[i] = nextParamValue;
558                                }
559                        }
560
561                        // Invoke the method
562                        try {
563                                return myMethod.invoke(getInterceptor(), args);
564                        } catch (InvocationTargetException e) {
565                                Throwable targetException = e.getTargetException();
566                                if (myPointcut.isShouldLogAndSwallowException(targetException)) {
567                                        ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException);
568                                        return null;
569                                }
570
571                                if (targetException instanceof RuntimeException) {
572                                        throw ((RuntimeException) targetException);
573                                } else {
574                                        throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException);
575                                }
576                        } catch (Exception e) {
577                                throw new InternalErrorException(e);
578                        }
579
580                }
581
582        }
583
584        protected static class HookDescriptor {
585
586                private final IPointcut myPointcut;
587                private final int myOrder;
588
589                public HookDescriptor(IPointcut thePointcut, int theOrder) {
590                        myPointcut = thePointcut;
591                        myOrder = theOrder;
592                }
593
594                IPointcut getPointcut() {
595                        return myPointcut;
596                }
597
598                int getOrder() {
599                        return myOrder;
600                }
601
602        }
603
604        protected static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> theHookClass) {
605                T annotation;
606                if (theObject instanceof Method) {
607                        annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true);
608                } else {
609                        annotation = theObject.getAnnotation(theHookClass);
610                }
611                return Optional.ofNullable(annotation);
612        }
613
614        private static int determineOrder(Class<?> theInterceptorClass) {
615                int typeOrder = Interceptor.DEFAULT_ORDER;
616                Optional<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
617                if (typeOrderAnnotation.isPresent()) {
618                        typeOrder = typeOrderAnnotation.get().order();
619                }
620                return typeOrder;
621        }
622
623        private static String toErrorString(List<String> theParameterTypes) {
624                return theParameterTypes
625                        .stream()
626                        .sorted()
627                        .collect(Collectors.joining(","));
628        }
629
630}