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