From 522efc87d91001e7c03bcd6b66ce7996686f7a63 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 22 Jan 2021 19:17:08 -0500 Subject: [PATCH] Reusable interceptor service (#2318) * Reusable interceptor service * Azure fix --- azure-pipelines.yml | 2 +- .../api/IBaseInterceptorBroadcaster.java | 50 ++ .../api/IBaseInterceptorService.java | 94 +++ .../api/IInterceptorBroadcaster.java | 27 +- .../interceptor/api/IInterceptorService.java | 72 +- .../uhn/fhir/interceptor/api/IPointcut.java | 17 + .../ca/uhn/fhir/interceptor/api/Pointcut.java | 5 +- .../executor/BaseInterceptorService.java | 621 ++++++++++++++++++ .../executor/InterceptorService.java | 537 +-------------- 9 files changed, 797 insertions(+), 628 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorService.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b026fee6b54..04c4ea0b33d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,7 +52,7 @@ jobs: inputs: targetPath: '$(Build.ArtifactStagingDirectory)/' artifactName: 'full_logs.zip' - - script: bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) + - script: bash <(curl https://codecov.io/bash) -C $(Build.SourceVersion) displayName: 'codecov' - task: PublishTestResults@2 inputs: diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java new file mode 100644 index 00000000000..404112fa2e1 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorBroadcaster.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.interceptor.api; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IBaseInterceptorBroadcaster { + + /** + * Invoke registered interceptor hook methods for the given Pointcut. + * + * @return Returns false if any of the invoked hook methods returned + * false, and returns true otherwise. + */ + boolean callHooks(POINTCUT thePointcut, HookParams theParams); + + /** + * Invoke registered interceptor hook methods for the given Pointcut. This method + * should only be called for pointcuts that return a type other than + * void or boolean + * + * @return Returns the object returned by the first hook method that did not return null + */ + Object callHooksAndReturnObject(POINTCUT thePointcut, HookParams theParams); + + /** + * Does this broadcaster have any hooks for the given pointcut? + * + * @param thePointcut The poointcut + * @return Does this broadcaster have any hooks for the given pointcut? + * @since 4.0.0 + */ + boolean hasHooks(POINTCUT thePointcut); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorService.java new file mode 100644 index 00000000000..b6efd6b7132 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IBaseInterceptorService.java @@ -0,0 +1,94 @@ +package ca.uhn.fhir.interceptor.api; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +public interface IBaseInterceptorService extends IBaseInterceptorBroadcaster { + + /** + * Register an interceptor that will be used in a {@link ThreadLocal} context. + * This means that events will only be broadcast to the given interceptor if + * they were fired from the current thread. + *

+ * Note that it is almost always desirable to call this method with a + * try-finally statement that removes the interceptor afterwards, since + * this can lead to memory leakage, poor performance due to ever-increasing + * numbers of interceptors, etc. + *

+ *

+ * Note that most methods such as {@link #getAllRegisteredInterceptors()} and + * {@link #unregisterAllInterceptors()} do not affect thread local interceptors + * as they are kept in a separate list. + *

+ * + * @param theInterceptor The interceptor + * @return Returns true if at least one valid hook method was found on this interceptor + */ + boolean registerThreadLocalInterceptor(Object theInterceptor); + + /** + * Unregisters a ThreadLocal interceptor + * + * @param theInterceptor The interceptor + * @see #registerThreadLocalInterceptor(Object) + */ + void unregisterThreadLocalInterceptor(Object theInterceptor); + + /** + * Register an interceptor. This method has no effect if the given interceptor is already registered. + * + * @param theInterceptor The interceptor to register + * @return Returns true if at least one valid hook method was found on this interceptor + */ + boolean registerInterceptor(Object theInterceptor); + + /** + * Unregister an interceptor. This method has no effect if the given interceptor is not already registered. + * + * @param theInterceptor The interceptor to unregister + * @return Returns true if the interceptor was found and removed + */ + boolean unregisterInterceptor(Object theInterceptor); + + /** + * Returns all currently registered interceptors (excluding any thread local interceptors). + */ + List getAllRegisteredInterceptors(); + + /** + * Unregisters all registered interceptors. Note that this method does not unregister + * any {@link #registerThreadLocalInterceptor(Object) thread local interceptors}. + */ + void unregisterAllInterceptors(); + + void unregisterInterceptors(@Nullable Collection theInterceptors); + + void registerInterceptors(@Nullable Collection theInterceptors); + + /** + * Unregisters all interceptors that are indicated by the given callback function returning true + */ + void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorBroadcaster.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorBroadcaster.java index fd0ab5442fc..73e8add78a4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorBroadcaster.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorBroadcaster.java @@ -20,31 +20,6 @@ package ca.uhn.fhir.interceptor.api; * #L% */ -public interface IInterceptorBroadcaster { +public interface IInterceptorBroadcaster extends IBaseInterceptorBroadcaster { - /** - * Invoke registered interceptor hook methods for the given Pointcut. - * - * @return Returns false if any of the invoked hook methods returned - * false, and returns true otherwise. - */ - boolean callHooks(Pointcut thePointcut, HookParams theParams); - - /** - * Invoke registered interceptor hook methods for the given Pointcut. This method - * should only be called for pointcuts that return a type other than - * void or boolean - * - * @return Returns the object returned by the first hook method that did not return null - */ - Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams); - - /** - * Does this broadcaster have any hooks for the given pointcut? - * - * @param thePointcut The poointcut - * @return Does this broadcaster have any hooks for the given pointcut? - * @since 4.0.0 - */ - boolean hasHooks(Pointcut thePointcut); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java index 6cc72d4de7e..876b515bc2f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IInterceptorService.java @@ -20,80 +20,10 @@ package ca.uhn.fhir.interceptor.api; * #L% */ -import javax.annotation.Nullable; -import java.util.Collection; -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; - -public interface IInterceptorService extends IInterceptorBroadcaster { - - /** - * Register an interceptor that will be used in a {@link ThreadLocal} context. - * This means that events will only be broadcast to the given interceptor if - * they were fired from the current thread. - *

- * Note that it is almost always desirable to call this method with a - * try-finally statement that removes the interceptor afterwards, since - * this can lead to memory leakage, poor performance due to ever-increasing - * numbers of interceptors, etc. - *

- *

- * Note that most methods such as {@link #getAllRegisteredInterceptors()} and - * {@link #unregisterAllInterceptors()} do not affect thread local interceptors - * as they are kept in a separate list. - *

- * - * @param theInterceptor The interceptor - * @return Returns true if at least one valid hook method was found on this interceptor - */ - boolean registerThreadLocalInterceptor(Object theInterceptor); - - /** - * Unregisters a ThreadLocal interceptor - * - * @param theInterceptor The interceptor - * @see #registerThreadLocalInterceptor(Object) - */ - void unregisterThreadLocalInterceptor(Object theInterceptor); - - /** - * Register an interceptor. This method has no effect if the given interceptor is already registered. - * - * @param theInterceptor The interceptor to register - * @return Returns true if at least one valid hook method was found on this interceptor - */ - boolean registerInterceptor(Object theInterceptor); - - /** - * Unregister an interceptor. This method has no effect if the given interceptor is not already registered. - * - * @param theInterceptor The interceptor to unregister - * @return Returns true if the interceptor was found and removed - */ - boolean unregisterInterceptor(Object theInterceptor); +public interface IInterceptorService extends IBaseInterceptorService, IInterceptorBroadcaster { void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor); void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor); - /** - * Returns all currently registered interceptors (excluding any thread local interceptors). - */ - List getAllRegisteredInterceptors(); - - /** - * Unregisters all registered interceptors. Note that this method does not unregister - * any {@link #registerThreadLocalInterceptor(Object) thread local interceptors}. - */ - void unregisterAllInterceptors(); - - void unregisterInterceptors(@Nullable Collection theInterceptors); - - void registerInterceptors(@Nullable Collection theInterceptors); - - /** - * Unregisters all interceptors that are indicated by the given callback function returning true - */ - void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java new file mode 100644 index 00000000000..d9abf1de3f9 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/IPointcut.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.interceptor.api; + +import javax.annotation.Nonnull; +import java.util.List; + +public interface IPointcut { + @Nonnull + Class getReturnType(); + + @Nonnull + List getParameterTypes(); + + @Nonnull + String name(); + + boolean isShouldLogAndSwallowException(Throwable theException); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index 51562475365..0498e9585f8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -48,7 +48,7 @@ import java.util.Set; * *

*/ -public enum Pointcut { +public enum Pointcut implements IPointcut { /** * Interceptor Framework Hook: @@ -2178,6 +2178,7 @@ public enum Pointcut { this(theReturnType, new ExceptionHandlingSpec(), theParameterTypes); } + @Override public boolean isShouldLogAndSwallowException(@Nonnull Throwable theException) { for (Class next : myExceptionHandlingSpec.myTypesToLogAndSwallow) { if (next.isAssignableFrom(theException.getClass())) { @@ -2187,11 +2188,13 @@ public enum Pointcut { return false; } + @Override @Nonnull public Class getReturnType() { return myReturnType; } + @Override @Nonnull public List getParameterTypes() { return myParameterTypes; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java new file mode 100644 index 00000000000..4458c7d6a91 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java @@ -0,0 +1,621 @@ +package ca.uhn.fhir.interceptor.executor; + +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2021 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IBaseInterceptorBroadcaster; +import ca.uhn.fhir.interceptor.api.IBaseInterceptorService; +import ca.uhn.fhir.interceptor.api.IPointcut; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.ReflectionUtil; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public abstract class BaseInterceptorService implements IBaseInterceptorService, IBaseInterceptorBroadcaster { + private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.class); + private final List myInterceptors = new ArrayList<>(); + private final ListMultimap myGlobalInvokers = ArrayListMultimap.create(); + private final ListMultimap myAnonymousInvokers = ArrayListMultimap.create(); + private final Object myRegistryMutex = new Object(); + private final ThreadLocal> myThreadlocalInvokers = new ThreadLocal<>(); + private String myName; + private boolean myThreadlocalInvokersEnabled = true; + + /** + * Constructor which uses a default name of "default" + */ + public BaseInterceptorService() { + this("default"); + } + + /** + * Constructor + * + * @param theName The name for this registry (useful for troubleshooting) + */ + public BaseInterceptorService(String theName) { + super(); + myName = theName; + } + + /** + * Are threadlocal interceptors enabled on this registry (defaults to true) + */ + public boolean isThreadlocalInvokersEnabled() { + return myThreadlocalInvokersEnabled; + } + + /** + * Are threadlocal interceptors enabled on this registry (defaults to true) + */ + public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) { + myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled; + } + + @VisibleForTesting + List getGlobalInterceptorsForUnitTest() { + return myInterceptors; + } + + public void setName(String theName) { + myName = theName; + } + + protected void registerAnonymousInterceptor(POINTCUT thePointcut, Object theInterceptor, BaseInvoker theInvoker) { + Validate.notNull(thePointcut); + Validate.notNull(theInterceptor); + synchronized (myRegistryMutex) { + + myAnonymousInvokers.put(thePointcut, theInvoker); + if (!isInterceptorAlreadyRegistered(theInterceptor)) { + myInterceptors.add(theInterceptor); + } + } + } + + @Override + public List getAllRegisteredInterceptors() { + synchronized (myRegistryMutex) { + List retVal = new ArrayList<>(); + retVal.addAll(myInterceptors); + return Collections.unmodifiableList(retVal); + } + } + + @Override + @VisibleForTesting + public void unregisterAllInterceptors() { + synchronized (myRegistryMutex) { + myAnonymousInvokers.clear(); + myGlobalInvokers.clear(); + myInterceptors.clear(); + } + } + + @Override + public void unregisterInterceptors(@Nullable Collection theInterceptors) { + if (theInterceptors != null) { + theInterceptors.forEach(t -> unregisterInterceptor(t)); + } + } + + @Override + public void registerInterceptors(@Nullable Collection theInterceptors) { + if (theInterceptors != null) { + theInterceptors.forEach(t -> registerInterceptor(t)); + } + } + + @Override + public void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction) { + unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers); + unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers); + } + + private void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction, ListMultimap theGlobalInvokers) { + theGlobalInvokers.entries().removeIf(t -> theShouldUnregisterFunction.test(t.getValue().getInterceptor())); + } + + @Override + public boolean registerThreadLocalInterceptor(Object theInterceptor) { + if (!myThreadlocalInvokersEnabled) { + return false; + } + ListMultimap invokers = getThreadLocalInvokerMultimap(); + scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers); + return !invokers.isEmpty(); + + } + + @Override + public void unregisterThreadLocalInterceptor(Object theInterceptor) { + if (myThreadlocalInvokersEnabled) { + ListMultimap invokers = getThreadLocalInvokerMultimap(); + invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); + if (invokers.isEmpty()) { + myThreadlocalInvokers.remove(); + } + } + } + + private ListMultimap getThreadLocalInvokerMultimap() { + ListMultimap invokers = myThreadlocalInvokers.get(); + if (invokers == null) { + invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + myThreadlocalInvokers.set(invokers); + } + return invokers; + } + + @Override + public boolean registerInterceptor(Object theInterceptor) { + synchronized (myRegistryMutex) { + + if (isInterceptorAlreadyRegistered(theInterceptor)) { + return false; + } + + List addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers); + if (addedInvokers.isEmpty()) { + ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName()); + return false; + } + + // Add to the global list + myInterceptors.add(theInterceptor); + sortByOrderAnnotation(myInterceptors); + + return true; + } + } + + private boolean isInterceptorAlreadyRegistered(Object theInterceptor) { + for (Object next : myInterceptors) { + if (next == theInterceptor) { + return true; + } + } + return false; + } + + @Override + public boolean unregisterInterceptor(Object theInterceptor) { + synchronized (myRegistryMutex) { + boolean removed = myInterceptors.removeIf(t -> t == theInterceptor); + removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); + removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); + return removed; + } + } + + private void sortByOrderAnnotation(List theObjects) { + IdentityHashMap interceptorToOrder = new IdentityHashMap<>(); + for (Object next : theObjects) { + Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class); + int order = orderAnnotation != null ? orderAnnotation.order() : 0; + interceptorToOrder.put(next, order); + } + + theObjects.sort((a, b) -> { + Integer orderA = interceptorToOrder.get(a); + Integer orderB = interceptorToOrder.get(b); + return orderA - orderB; + }); + } + + @Override + public Object callHooksAndReturnObject(POINTCUT thePointcut, HookParams theParams) { + assert haveAppropriateParams(thePointcut, theParams); + assert thePointcut.getReturnType() != void.class; + + return doCallHooks(thePointcut, theParams, null); + } + + @Override + public boolean hasHooks(POINTCUT thePointcut) { + return myGlobalInvokers.containsKey(thePointcut) + || myAnonymousInvokers.containsKey(thePointcut) + || hasThreadLocalHooks(thePointcut); + } + + private boolean hasThreadLocalHooks(POINTCUT thePointcut) { + ListMultimap hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null; + return hooks != null && hooks.containsKey(thePointcut); + } + + @Override + public boolean callHooks(POINTCUT thePointcut, HookParams theParams) { + assert haveAppropriateParams(thePointcut, theParams); + assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class; + + Object retValObj = doCallHooks(thePointcut, theParams, true); + return (Boolean) retValObj; + } + + private Object doCallHooks(POINTCUT thePointcut, HookParams theParams, Object theRetVal) { + List invokers = getInvokersForPointcut(thePointcut); + + /* + * Call each hook in order + */ + for (BaseInvoker nextInvoker : invokers) { + Object nextOutcome = nextInvoker.invoke(theParams); + Class pointcutReturnType = thePointcut.getReturnType(); + if (pointcutReturnType.equals(boolean.class)) { + Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome; + if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) { + ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker); + theRetVal = false; + break; + } + } else if (pointcutReturnType.equals(void.class) == false) { + if (nextOutcome != null) { + theRetVal = nextOutcome; + break; + } + } + } + + return theRetVal; + } + + @VisibleForTesting + List getInterceptorsWithInvokersForPointcut(POINTCUT thePointcut) { + return getInvokersForPointcut(thePointcut) + .stream() + .map(BaseInvoker::getInterceptor) + .collect(Collectors.toList()); + } + + /** + * Returns an ordered list of invokers for the given pointcut. Note that + * a new and stable list is returned to.. do whatever you want with it. + */ + private List getInvokersForPointcut(POINTCUT thePointcut) { + List invokers; + + synchronized (myRegistryMutex) { + List globalInvokers = myGlobalInvokers.get(thePointcut); + List anonymousInvokers = myAnonymousInvokers.get(thePointcut); + List threadLocalInvokers = null; + if (myThreadlocalInvokersEnabled) { + ListMultimap pointcutToInvokers = myThreadlocalInvokers.get(); + if (pointcutToInvokers != null) { + threadLocalInvokers = pointcutToInvokers.get(thePointcut); + } + } + invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers); + } + + return invokers; + } + + /** + * First argument must be the global invoker list!! + */ + @SafeVarargs + private final List union(List... theInvokersLists) { + List haveOne = null; + boolean haveMultiple = false; + for (List nextInvokerList : theInvokersLists) { + if (nextInvokerList == null || nextInvokerList.isEmpty()) { + continue; + } + + if (haveOne == null) { + haveOne = nextInvokerList; + } else { + haveMultiple = true; + } + } + + if (haveOne == null) { + return Collections.emptyList(); + } + + List retVal; + + if (haveMultiple == false) { + + // The global list doesn't need to be sorted every time since it's sorted on + // insertion each time. Doing so is a waste of cycles.. + if (haveOne == theInvokersLists[0]) { + retVal = haveOne; + } else { + retVal = new ArrayList<>(haveOne); + retVal.sort(Comparator.naturalOrder()); + } + + } else { + + retVal = Arrays + .stream(theInvokersLists) + .filter(t -> t != null) + .flatMap(t -> t.stream()) + .sorted() + .collect(Collectors.toList()); + + } + + return retVal; + } + + /** + * Only call this when assertions are enabled, it's expensive + */ + boolean haveAppropriateParams(POINTCUT thePointcut, HookParams theParams) { + 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())); + + List wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); + + ListMultimap, Object> givenTypes = theParams.getParamsForType(); + for (Class nextTypeClass : givenTypes.keySet()) { + String nextTypeName = nextTypeClass.getName(); + for (Object nextParamValue : givenTypes.get(nextTypeClass)) { + 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); + Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName); + } + } + + return true; + } + + private List scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap theInvokers) { + Class interceptorClass = theInterceptor.getClass(); + int typeOrder = determineOrder(interceptorClass); + + List addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder); + + // Invoke the REGISTERED pointcut for any added hooks + addedInvokers.stream() + .filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut())) + .forEach(t -> t.invoke(new HookParams())); + + // Register the interceptor and its various hooks + for (HookInvoker nextAddedHook : addedInvokers) { + IPointcut nextPointcut = nextAddedHook.getPointcut(); + if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) { + continue; + } + theInvokers.put((POINTCUT) nextPointcut, nextAddedHook); + } + + // Make sure we're always sorted according to the order declared in + // @Order + for (IPointcut nextPointcut : theInvokers.keys()) { + List nextInvokerList = theInvokers.get((POINTCUT) nextPointcut); + nextInvokerList.sort(Comparator.naturalOrder()); + } + + return addedInvokers; + } + + protected abstract static class BaseInvoker implements Comparable { + + private final int myOrder; + private final Object myInterceptor; + + BaseInvoker(Object theInterceptor, int theOrder) { + myInterceptor = theInterceptor; + myOrder = theOrder; + } + + public Object getInterceptor() { + return myInterceptor; + } + + abstract Object invoke(HookParams theParams); + + @Override + public int compareTo(BaseInvoker theInvoker) { + return myOrder - theInvoker.myOrder; + } + } + + private static class HookInvoker extends BaseInvoker { + + private final Method myMethod; + private final Class[] myParameterTypes; + private final int[] myParameterIndexes; + private final IPointcut myPointcut; + + /** + * Constructor + */ + private HookInvoker(HookDescriptor theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { + super(theInterceptor, theOrder); + myPointcut = theHook.getPointcut(); + myParameterTypes = theHookMethod.getParameterTypes(); + myMethod = theHookMethod; + + Class returnType = theHookMethod.getReturnType(); + if (myPointcut.getReturnType().equals(boolean.class)) { + Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); + } else if (myPointcut.getReturnType().equals(void.class)) { + Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod); + } else { + Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod); + } + + myParameterIndexes = new int[myParameterTypes.length]; + Map, AtomicInteger> typeToCount = new HashMap<>(); + for (int i = 0; i < myParameterTypes.length; i++) { + AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0)); + myParameterIndexes[i] = counter.getAndIncrement(); + } + + myMethod.setAccessible(true); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("method", myMethod) + .toString(); + } + + public IPointcut getPointcut() { + return myPointcut; + } + + /** + * @return Returns true/false if the hook method returns a boolean, returns true otherwise + */ + @Override + Object invoke(HookParams theParams) { + + Object[] args = new Object[myParameterTypes.length]; + for (int i = 0; i < myParameterTypes.length; i++) { + Class nextParamType = myParameterTypes[i]; + if (nextParamType.equals(Pointcut.class)) { + args[i] = myPointcut; + } else { + int nextParamIndex = myParameterIndexes[i]; + Object nextParamValue = theParams.get(nextParamType, nextParamIndex); + args[i] = nextParamValue; + } + } + + // Invoke the method + try { + return myMethod.invoke(getInterceptor(), args); + } catch (InvocationTargetException e) { + Throwable targetException = e.getTargetException(); + if (myPointcut.isShouldLogAndSwallowException(targetException)) { + ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException); + return null; + } + + if (targetException instanceof RuntimeException) { + throw ((RuntimeException) targetException); + } else { + throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException); + } + } catch (Exception e) { + throw new InternalErrorException(e); + } + + } + + } + + /** + * @return Returns a list of any added invokers + */ + private List scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) { + ArrayList retVal = new ArrayList<>(); + for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) { + Optional hook = scanForHook(nextMethod); + + if (hook.isPresent()) { + int methodOrder = theTypeOrder; + int methodOrderAnnotation = hook.get().getOrder(); + if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) { + methodOrder = methodOrderAnnotation; + } + + retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder)); + } + } + + return retVal; + } + + protected abstract Optional scanForHook(Method nextMethod); + + protected static Optional findAnnotation(AnnotatedElement theObject, Class theHookClass) { + T annotation; + if (theObject instanceof Method) { + annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true); + } else { + annotation = theObject.getAnnotation(theHookClass); + } + return Optional.ofNullable(annotation); + } + + private static int determineOrder(Class theInterceptorClass) { + int typeOrder = Interceptor.DEFAULT_ORDER; + Optional typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class); + if (typeOrderAnnotation.isPresent()) { + typeOrder = typeOrderAnnotation.get().order(); + } + return typeOrder; + } + + private static String toErrorString(List theParameterTypes) { + return theParameterTypes + .stream() + .sorted() + .collect(Collectors.joining(",")); + } + + protected static class HookDescriptor { + + private final IPointcut myPointcut; + private final int myOrder; + + HookDescriptor(IPointcut thePointcut, int theOrder) { + myPointcut = thePointcut; + myOrder = theOrder; + } + + IPointcut getPointcut() { + return myPointcut; + } + + int getOrder() { + return myOrder; + } + + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java index 6b3b5e082fa..14a0d7fa459 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/InterceptorService.java @@ -27,39 +27,13 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.util.ReflectionUtil; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimaps; import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.commons.lang3.reflect.MethodUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import java.util.Optional; -public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster { - private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class); - private final List myInterceptors = new ArrayList<>(); - private final ListMultimap myGlobalInvokers = ArrayListMultimap.create(); - private final ListMultimap myAnonymousInvokers = ArrayListMultimap.create(); - private final Object myRegistryMutex = new Object(); - private final ThreadLocal> myThreadlocalInvokers = new ThreadLocal<>(); - private String myName; - private boolean myThreadlocalInvokersEnabled = true; +public class InterceptorService extends BaseInterceptorService implements IInterceptorService, IInterceptorBroadcaster { /** * Constructor which uses a default name of "default" @@ -74,28 +48,14 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa * @param theName The name for this registry (useful for troubleshooting) */ public InterceptorService(String theName) { - super(); - myName = theName; + super(theName); } - /** - * Are threadlocal interceptors enabled on this registry (defaults to true) - */ - public boolean isThreadlocalInvokersEnabled() { - return myThreadlocalInvokersEnabled; + @Override + protected Optional scanForHook(Method nextMethod) { + return findAnnotation(nextMethod, Hook.class).map(t -> new HookDescriptor(t.value(), t.order())); } - /** - * Are threadlocal interceptors enabled on this registry (defaults to true) - */ - public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) { - myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled; - } - - @VisibleForTesting - List getGlobalInterceptorsForUnitTest() { - return myInterceptors; - } @Override @VisibleForTesting @@ -103,309 +63,14 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa registerAnonymousInterceptor(thePointcut, Interceptor.DEFAULT_ORDER, theInterceptor); } - public void setName(String theName) { - myName = theName; - } - @Override public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) { Validate.notNull(thePointcut); Validate.notNull(theInterceptor); - synchronized (myRegistryMutex) { - - myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder)); - if (!isInterceptorAlreadyRegistered(theInterceptor)) { - myInterceptors.add(theInterceptor); - } - } + BaseInvoker invoker = new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder); + registerAnonymousInterceptor(thePointcut, theInterceptor, invoker); } - @Override - public List getAllRegisteredInterceptors() { - synchronized (myRegistryMutex) { - List retVal = new ArrayList<>(); - retVal.addAll(myInterceptors); - return Collections.unmodifiableList(retVal); - } - } - - @Override - @VisibleForTesting - public void unregisterAllInterceptors() { - synchronized (myRegistryMutex) { - myAnonymousInvokers.clear(); - myGlobalInvokers.clear(); - myInterceptors.clear(); - } - } - - @Override - public void unregisterInterceptors(@Nullable Collection theInterceptors) { - if (theInterceptors != null) { - theInterceptors.forEach(t -> unregisterInterceptor(t)); - } - } - - @Override - public void registerInterceptors(@Nullable Collection theInterceptors) { - if (theInterceptors != null) { - theInterceptors.forEach(t -> registerInterceptor(t)); - } - } - - @Override - public void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction) { - unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers); - unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers); - } - - private void unregisterInterceptorsIf(Predicate theShouldUnregisterFunction, ListMultimap theGlobalInvokers) { - theGlobalInvokers.entries().removeIf(t->theShouldUnregisterFunction.test(t.getValue().getInterceptor())); - } - - @Override - public boolean registerThreadLocalInterceptor(Object theInterceptor) { - if (!myThreadlocalInvokersEnabled) { - return false; - } - ListMultimap invokers = getThreadLocalInvokerMultimap(); - scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers); - return !invokers.isEmpty(); - - } - - @Override - public void unregisterThreadLocalInterceptor(Object theInterceptor) { - if (myThreadlocalInvokersEnabled) { - ListMultimap invokers = getThreadLocalInvokerMultimap(); - invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); - if (invokers.isEmpty()) { - myThreadlocalInvokers.remove(); - } - } - } - - private ListMultimap getThreadLocalInvokerMultimap() { - ListMultimap invokers = myThreadlocalInvokers.get(); - if (invokers == null) { - invokers = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); - myThreadlocalInvokers.set(invokers); - } - return invokers; - } - - @Override - public boolean registerInterceptor(Object theInterceptor) { - synchronized (myRegistryMutex) { - - if (isInterceptorAlreadyRegistered(theInterceptor)) { - return false; - } - - List addedInvokers = scanInterceptorAndAddToInvokerMultimap(theInterceptor, myGlobalInvokers); - if (addedInvokers.isEmpty()) { - ourLog.warn("Interceptor registered with no valid hooks - Type was: {}", theInterceptor.getClass().getName()); - return false; - } - - // Add to the global list - myInterceptors.add(theInterceptor); - sortByOrderAnnotation(myInterceptors); - - return true; - } - } - - private boolean isInterceptorAlreadyRegistered(Object theInterceptor) { - for (Object next : myInterceptors) { - if (next == theInterceptor) { - return true; - } - } - return false; - } - - @Override - public boolean unregisterInterceptor(Object theInterceptor) { - synchronized (myRegistryMutex) { - boolean removed = myInterceptors.removeIf(t -> t == theInterceptor); - removed |= myGlobalInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); - removed |= myAnonymousInvokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor); - return removed; - } - } - - private void sortByOrderAnnotation(List theObjects) { - IdentityHashMap interceptorToOrder = new IdentityHashMap<>(); - for (Object next : theObjects) { - Interceptor orderAnnotation = next.getClass().getAnnotation(Interceptor.class); - int order = orderAnnotation != null ? orderAnnotation.order() : 0; - interceptorToOrder.put(next, order); - } - - theObjects.sort((a, b) -> { - Integer orderA = interceptorToOrder.get(a); - Integer orderB = interceptorToOrder.get(b); - return orderA - orderB; - }); - } - - @Override - public Object callHooksAndReturnObject(Pointcut thePointcut, HookParams theParams) { - assert haveAppropriateParams(thePointcut, theParams); - assert thePointcut.getReturnType() != void.class; - - return doCallHooks(thePointcut, theParams, null); - } - - @Override - public boolean hasHooks(Pointcut thePointcut) { - return myGlobalInvokers.containsKey(thePointcut) - || myAnonymousInvokers.containsKey(thePointcut) - || hasThreadLocalHooks(thePointcut); - } - - private boolean hasThreadLocalHooks(Pointcut thePointcut) { - ListMultimap hooks = myThreadlocalInvokersEnabled ? myThreadlocalInvokers.get() : null; - return hooks != null && hooks.containsKey(thePointcut); - } - - @Override - public boolean callHooks(Pointcut thePointcut, HookParams theParams) { - assert haveAppropriateParams(thePointcut, theParams); - assert thePointcut.getReturnType() == void.class || thePointcut.getReturnType() == boolean.class; - - Object retValObj = doCallHooks(thePointcut, theParams, true); - return (Boolean) retValObj; - } - - private Object doCallHooks(Pointcut thePointcut, HookParams theParams, Object theRetVal) { - List invokers = getInvokersForPointcut(thePointcut); - - /* - * Call each hook in order - */ - for (BaseInvoker nextInvoker : invokers) { - Object nextOutcome = nextInvoker.invoke(theParams); - Class pointcutReturnType = thePointcut.getReturnType(); - if (pointcutReturnType.equals(boolean.class)) { - Boolean nextOutcomeAsBoolean = (Boolean) nextOutcome; - if (Boolean.FALSE.equals(nextOutcomeAsBoolean)) { - ourLog.trace("callHooks({}) for invoker({}) returned false", thePointcut, nextInvoker); - theRetVal = false; - break; - } - } else if (pointcutReturnType.equals(void.class) == false) { - if (nextOutcome != null) { - theRetVal = nextOutcome; - break; - } - } - } - - return theRetVal; - } - - @VisibleForTesting - List getInterceptorsWithInvokersForPointcut(Pointcut thePointcut) { - return getInvokersForPointcut(thePointcut) - .stream() - .map(BaseInvoker::getInterceptor) - .collect(Collectors.toList()); - } - - /** - * Returns an ordered list of invokers for the given pointcut. Note that - * a new and stable list is returned to.. do whatever you want with it. - */ - private List getInvokersForPointcut(Pointcut thePointcut) { - List invokers; - - synchronized (myRegistryMutex) { - List globalInvokers = myGlobalInvokers.get(thePointcut); - List anonymousInvokers = myAnonymousInvokers.get(thePointcut); - List threadLocalInvokers = null; - if (myThreadlocalInvokersEnabled) { - ListMultimap pointcutToInvokers = myThreadlocalInvokers.get(); - if (pointcutToInvokers != null) { - threadLocalInvokers = pointcutToInvokers.get(thePointcut); - } - } - invokers = union(globalInvokers, anonymousInvokers, threadLocalInvokers); - } - - return invokers; - } - - /** - * First argument must be the global invoker list!! - */ - @SafeVarargs - private final List union(List... theInvokersLists) { - List haveOne = null; - boolean haveMultiple = false; - for (List nextInvokerList : theInvokersLists) { - if (nextInvokerList == null || nextInvokerList.isEmpty()) { - continue; - } - - if (haveOne == null) { - haveOne = nextInvokerList; - } else { - haveMultiple = true; - } - } - - if (haveOne == null) { - return Collections.emptyList(); - } - - List retVal; - - if (haveMultiple == false) { - - // The global list doesn't need to be sorted every time since it's sorted on - // insertion each time. Doing so is a waste of cycles.. - if (haveOne == theInvokersLists[0]) { - retVal = haveOne; - } else { - retVal = new ArrayList<>(haveOne); - retVal.sort(Comparator.naturalOrder()); - } - - } else { - - retVal = Arrays - .stream(theInvokersLists) - .filter(t -> t != null) - .flatMap(t -> t.stream()) - .sorted() - .collect(Collectors.toList()); - - } - - return retVal; - } - - /** - * Only call this when assertions are enabled, it's expensive - */ - boolean haveAppropriateParams(Pointcut thePointcut, HookParams theParams) { - 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())); - - List wantedTypes = new ArrayList<>(thePointcut.getParameterTypes()); - - ListMultimap, Object> givenTypes = theParams.getParamsForType(); - for (Class nextTypeClass : givenTypes.keySet()) { - String nextTypeName = nextTypeClass.getName(); - for (Object nextParamValue : givenTypes.get(nextTypeClass)) { - 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); - Validate.isTrue(wantedTypes.remove(nextTypeName), "Invalid params for pointcut %s - Wanted %s but found %s", thePointcut.name(), toErrorString(thePointcut.getParameterTypes()), nextTypeName); - } - } - - return true; - } private class AnonymousLambdaInvoker extends BaseInvoker { private final IAnonymousInterceptor myHook; @@ -424,191 +89,5 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa } } - private abstract static class BaseInvoker implements Comparable { - - private final int myOrder; - private final Object myInterceptor; - - BaseInvoker(Object theInterceptor, int theOrder) { - myInterceptor = theInterceptor; - myOrder = theOrder; - } - - public Object getInterceptor() { - return myInterceptor; - } - - abstract Object invoke(HookParams theParams); - - @Override - public int compareTo(BaseInvoker theInvoker) { - return myOrder - theInvoker.myOrder; - } - } - - private static class HookInvoker extends BaseInvoker { - - private final Method myMethod; - private final Class[] myParameterTypes; - private final int[] myParameterIndexes; - private final Pointcut myPointcut; - - /** - * Constructor - */ - private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) { - super(theInterceptor, theOrder); - myPointcut = theHook.value(); - myParameterTypes = theHookMethod.getParameterTypes(); - myMethod = theHookMethod; - - Class returnType = theHookMethod.getReturnType(); - if (myPointcut.getReturnType().equals(boolean.class)) { - Validate.isTrue(boolean.class.equals(returnType) || void.class.equals(returnType), "Method does not return boolean or void: %s", theHookMethod); - } else if (myPointcut.getReturnType().equals(void.class)) { - Validate.isTrue(void.class.equals(returnType), "Method does not return void: %s", theHookMethod); - } else { - Validate.isTrue(myPointcut.getReturnType().isAssignableFrom(returnType) || void.class.equals(returnType), "Method does not return %s or void: %s", myPointcut.getReturnType(), theHookMethod); - } - - myParameterIndexes = new int[myParameterTypes.length]; - Map, AtomicInteger> typeToCount = new HashMap<>(); - for (int i = 0; i < myParameterTypes.length; i++) { - AtomicInteger counter = typeToCount.computeIfAbsent(myParameterTypes[i], t -> new AtomicInteger(0)); - myParameterIndexes[i] = counter.getAndIncrement(); - } - - myMethod.setAccessible(true); - } - - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("method", myMethod) - .toString(); - } - - public Pointcut getPointcut() { - return myPointcut; - } - - /** - * @return Returns true/false if the hook method returns a boolean, returns true otherwise - */ - @Override - Object invoke(HookParams theParams) { - - Object[] args = new Object[myParameterTypes.length]; - for (int i = 0; i < myParameterTypes.length; i++) { - Class nextParamType = myParameterTypes[i]; - if (nextParamType.equals(Pointcut.class)) { - args[i] = myPointcut; - } else { - int nextParamIndex = myParameterIndexes[i]; - Object nextParamValue = theParams.get(nextParamType, nextParamIndex); - args[i] = nextParamValue; - } - } - - // Invoke the method - try { - return myMethod.invoke(getInterceptor(), args); - } catch (InvocationTargetException e) { - Throwable targetException = e.getTargetException(); - if (myPointcut.isShouldLogAndSwallowException(targetException)) { - ourLog.error("Exception thrown by interceptor: " + targetException.toString(), targetException); - return null; - } - - if (targetException instanceof RuntimeException) { - throw ((RuntimeException) targetException); - } else { - throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcut(), targetException); - } - } catch (Exception e) { - throw new InternalErrorException(e); - } - - } - - } - - private static List scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap theInvokers) { - Class interceptorClass = theInterceptor.getClass(); - int typeOrder = determineOrder(interceptorClass); - - List addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder); - - // Invoke the REGISTERED pointcut for any added hooks - addedInvokers.stream() - .filter(t -> Pointcut.INTERCEPTOR_REGISTERED.equals(t.getPointcut())) - .forEach(t -> t.invoke(new HookParams())); - - // Register the interceptor and its various hooks - for (HookInvoker nextAddedHook : addedInvokers) { - Pointcut nextPointcut = nextAddedHook.getPointcut(); - if (nextPointcut.equals(Pointcut.INTERCEPTOR_REGISTERED)) { - continue; - } - theInvokers.put(nextPointcut, nextAddedHook); - } - - // Make sure we're always sorted according to the order declared in - // @Order - for (Pointcut nextPointcut : theInvokers.keys()) { - List nextInvokerList = theInvokers.get(nextPointcut); - nextInvokerList.sort(Comparator.naturalOrder()); - } - - return addedInvokers; - } - - /** - * @return Returns a list of any added invokers - */ - private static List scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) { - ArrayList retVal = new ArrayList<>(); - for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) { - Optional hook = findAnnotation(nextMethod, Hook.class); - - if (hook.isPresent()) { - int methodOrder = theTypeOrder; - int methodOrderAnnotation = hook.get().order(); - if (methodOrderAnnotation != Interceptor.DEFAULT_ORDER) { - methodOrder = methodOrderAnnotation; - } - - retVal.add(new HookInvoker(hook.get(), theInterceptor, nextMethod, methodOrder)); - } - } - - return retVal; - } - - private static Optional findAnnotation(AnnotatedElement theObject, Class theHookClass) { - T annotation; - if (theObject instanceof Method) { - annotation = MethodUtils.getAnnotation((Method) theObject, theHookClass, true, true); - } else { - annotation = theObject.getAnnotation(theHookClass); - } - return Optional.ofNullable(annotation); - } - - private static int determineOrder(Class theInterceptorClass) { - int typeOrder = Interceptor.DEFAULT_ORDER; - Optional typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class); - if (typeOrderAnnotation.isPresent()) { - typeOrder = typeOrderAnnotation.get().order(); - } - return typeOrder; - } - - private static String toErrorString(List theParameterTypes) { - return theParameterTypes - .stream() - .sorted() - .collect(Collectors.joining(",")); - } }