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