Reusable interceptor service (#2318)
* Reusable interceptor service * Azure fix
This commit is contained in:
parent
749934d5f1
commit
522efc87d9
|
@ -52,7 +52,7 @@ jobs:
|
||||||
inputs:
|
inputs:
|
||||||
targetPath: '$(Build.ArtifactStagingDirectory)/'
|
targetPath: '$(Build.ArtifactStagingDirectory)/'
|
||||||
artifactName: 'full_logs.zip'
|
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'
|
displayName: 'codecov'
|
||||||
- task: PublishTestResults@2
|
- task: PublishTestResults@2
|
||||||
inputs:
|
inputs:
|
||||||
|
|
|
@ -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<POINTCUT extends IPointcut> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke registered interceptor hook methods for the given Pointcut.
|
||||||
|
*
|
||||||
|
* @return Returns <code>false</code> if any of the invoked hook methods returned
|
||||||
|
* <code>false</code>, and returns <code>true</code> 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
|
||||||
|
* <code>void</code> or <code>boolean</code>
|
||||||
|
*
|
||||||
|
* @return Returns the object returned by the first hook method that did not return <code>null</code>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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<POINTCUT extends IPointcut> extends IBaseInterceptorBroadcaster<POINTCUT> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param theInterceptor The interceptor
|
||||||
|
* @return Returns <code>true</code> 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 <code>true</code> 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 <code>true</code> if the interceptor was found and removed
|
||||||
|
*/
|
||||||
|
boolean unregisterInterceptor(Object theInterceptor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all currently registered interceptors (excluding any thread local interceptors).
|
||||||
|
*/
|
||||||
|
List<Object> 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 <code>true</code>
|
||||||
|
*/
|
||||||
|
void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction);
|
||||||
|
}
|
|
@ -20,31 +20,6 @@ package ca.uhn.fhir.interceptor.api;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public interface IInterceptorBroadcaster {
|
public interface IInterceptorBroadcaster extends IBaseInterceptorBroadcaster<Pointcut> {
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoke registered interceptor hook methods for the given Pointcut.
|
|
||||||
*
|
|
||||||
* @return Returns <code>false</code> if any of the invoked hook methods returned
|
|
||||||
* <code>false</code>, and returns <code>true</code> 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
|
|
||||||
* <code>void</code> or <code>boolean</code>
|
|
||||||
*
|
|
||||||
* @return Returns the object returned by the first hook method that did not return <code>null</code>
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,80 +20,10 @@ package ca.uhn.fhir.interceptor.api;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
public interface IInterceptorService extends IBaseInterceptorService<Pointcut>, IInterceptorBroadcaster {
|
||||||
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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param theInterceptor The interceptor
|
|
||||||
* @return Returns <code>true</code> 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 <code>true</code> 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 <code>true</code> if the interceptor was found and removed
|
|
||||||
*/
|
|
||||||
boolean unregisterInterceptor(Object theInterceptor);
|
|
||||||
|
|
||||||
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
|
void registerAnonymousInterceptor(Pointcut thePointcut, IAnonymousInterceptor theInterceptor);
|
||||||
|
|
||||||
void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor);
|
void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all currently registered interceptors (excluding any thread local interceptors).
|
|
||||||
*/
|
|
||||||
List<Object> 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 <code>true</code>
|
|
||||||
*/
|
|
||||||
void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String> getParameterTypes();
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
String name();
|
||||||
|
|
||||||
|
boolean isShouldLogAndSwallowException(Throwable theException);
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ import java.util.Set;
|
||||||
* </ul>
|
* </ul>
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public enum Pointcut {
|
public enum Pointcut implements IPointcut {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <b>Interceptor Framework Hook:</b>
|
* <b>Interceptor Framework Hook:</b>
|
||||||
|
@ -2178,6 +2178,7 @@ public enum Pointcut {
|
||||||
this(theReturnType, new ExceptionHandlingSpec(), theParameterTypes);
|
this(theReturnType, new ExceptionHandlingSpec(), theParameterTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isShouldLogAndSwallowException(@Nonnull Throwable theException) {
|
public boolean isShouldLogAndSwallowException(@Nonnull Throwable theException) {
|
||||||
for (Class<? extends Throwable> next : myExceptionHandlingSpec.myTypesToLogAndSwallow) {
|
for (Class<? extends Throwable> next : myExceptionHandlingSpec.myTypesToLogAndSwallow) {
|
||||||
if (next.isAssignableFrom(theException.getClass())) {
|
if (next.isAssignableFrom(theException.getClass())) {
|
||||||
|
@ -2187,11 +2188,13 @@ public enum Pointcut {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public Class<?> getReturnType() {
|
public Class<?> getReturnType() {
|
||||||
return myReturnType;
|
return myReturnType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public List<String> getParameterTypes() {
|
public List<String> getParameterTypes() {
|
||||||
return myParameterTypes;
|
return myParameterTypes;
|
||||||
|
|
|
@ -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<POINTCUT extends IPointcut> implements IBaseInterceptorService<POINTCUT>, IBaseInterceptorBroadcaster<POINTCUT> {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(BaseInterceptorService.class);
|
||||||
|
private final List<Object> myInterceptors = new ArrayList<>();
|
||||||
|
private final ListMultimap<POINTCUT, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
|
||||||
|
private final ListMultimap<POINTCUT, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
|
||||||
|
private final Object myRegistryMutex = new Object();
|
||||||
|
private final ThreadLocal<ListMultimap<POINTCUT, BaseInvoker>> 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<Object> 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<Object> getAllRegisteredInterceptors() {
|
||||||
|
synchronized (myRegistryMutex) {
|
||||||
|
List<Object> 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<Object> theShouldUnregisterFunction) {
|
||||||
|
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
|
||||||
|
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<POINTCUT, BaseInvoker> theGlobalInvokers) {
|
||||||
|
theGlobalInvokers.entries().removeIf(t -> theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
|
||||||
|
if (!myThreadlocalInvokersEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
|
||||||
|
scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers);
|
||||||
|
return !invokers.isEmpty();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterThreadLocalInterceptor(Object theInterceptor) {
|
||||||
|
if (myThreadlocalInvokersEnabled) {
|
||||||
|
ListMultimap<POINTCUT, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
|
||||||
|
invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
|
||||||
|
if (invokers.isEmpty()) {
|
||||||
|
myThreadlocalInvokers.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListMultimap<POINTCUT, BaseInvoker> getThreadLocalInvokerMultimap() {
|
||||||
|
ListMultimap<POINTCUT, BaseInvoker> 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<HookInvoker> 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<Object> theObjects) {
|
||||||
|
IdentityHashMap<Object, Integer> 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<POINTCUT, BaseInvoker> 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<BaseInvoker> 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<Object> 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<BaseInvoker> getInvokersForPointcut(POINTCUT thePointcut) {
|
||||||
|
List<BaseInvoker> invokers;
|
||||||
|
|
||||||
|
synchronized (myRegistryMutex) {
|
||||||
|
List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut);
|
||||||
|
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
|
||||||
|
List<BaseInvoker> threadLocalInvokers = null;
|
||||||
|
if (myThreadlocalInvokersEnabled) {
|
||||||
|
ListMultimap<POINTCUT, BaseInvoker> 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<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) {
|
||||||
|
List<BaseInvoker> haveOne = null;
|
||||||
|
boolean haveMultiple = false;
|
||||||
|
for (List<BaseInvoker> nextInvokerList : theInvokersLists) {
|
||||||
|
if (nextInvokerList == null || nextInvokerList.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haveOne == null) {
|
||||||
|
haveOne = nextInvokerList;
|
||||||
|
} else {
|
||||||
|
haveMultiple = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haveOne == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BaseInvoker> 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<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
|
||||||
|
|
||||||
|
ListMultimap<Class<?>, 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<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<POINTCUT, BaseInvoker> theInvokers) {
|
||||||
|
Class<?> interceptorClass = theInterceptor.getClass();
|
||||||
|
int typeOrder = determineOrder(interceptorClass);
|
||||||
|
|
||||||
|
List<HookInvoker> 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<BaseInvoker> nextInvokerList = theInvokers.get((POINTCUT) nextPointcut);
|
||||||
|
nextInvokerList.sort(Comparator.naturalOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedInvokers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract static class BaseInvoker implements Comparable<BaseInvoker> {
|
||||||
|
|
||||||
|
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<Class<?>, 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<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
|
||||||
|
ArrayList<HookInvoker> retVal = new ArrayList<>();
|
||||||
|
for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
|
||||||
|
Optional<HookDescriptor> 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<HookDescriptor> scanForHook(Method nextMethod);
|
||||||
|
|
||||||
|
protected static <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> 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<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
|
||||||
|
if (typeOrderAnnotation.isPresent()) {
|
||||||
|
typeOrder = typeOrderAnnotation.get().order();
|
||||||
|
}
|
||||||
|
return typeOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toErrorString(List<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,39 +27,13 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
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.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.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.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class InterceptorService implements IInterceptorService, IInterceptorBroadcaster {
|
public class InterceptorService extends BaseInterceptorService<Pointcut> implements IInterceptorService, IInterceptorBroadcaster {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(InterceptorService.class);
|
|
||||||
private final List<Object> myInterceptors = new ArrayList<>();
|
|
||||||
private final ListMultimap<Pointcut, BaseInvoker> myGlobalInvokers = ArrayListMultimap.create();
|
|
||||||
private final ListMultimap<Pointcut, BaseInvoker> myAnonymousInvokers = ArrayListMultimap.create();
|
|
||||||
private final Object myRegistryMutex = new Object();
|
|
||||||
private final ThreadLocal<ListMultimap<Pointcut, BaseInvoker>> myThreadlocalInvokers = new ThreadLocal<>();
|
|
||||||
private String myName;
|
|
||||||
private boolean myThreadlocalInvokersEnabled = true;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor which uses a default name of "default"
|
* 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)
|
* @param theName The name for this registry (useful for troubleshooting)
|
||||||
*/
|
*/
|
||||||
public InterceptorService(String theName) {
|
public InterceptorService(String theName) {
|
||||||
super();
|
super(theName);
|
||||||
myName = theName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Are threadlocal interceptors enabled on this registry (defaults to true)
|
protected Optional<HookDescriptor> scanForHook(Method nextMethod) {
|
||||||
*/
|
return findAnnotation(nextMethod, Hook.class).map(t -> new HookDescriptor(t.value(), t.order()));
|
||||||
public boolean isThreadlocalInvokersEnabled() {
|
|
||||||
return myThreadlocalInvokersEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Are threadlocal interceptors enabled on this registry (defaults to true)
|
|
||||||
*/
|
|
||||||
public void setThreadlocalInvokersEnabled(boolean theThreadlocalInvokersEnabled) {
|
|
||||||
myThreadlocalInvokersEnabled = theThreadlocalInvokersEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
List<Object> getGlobalInterceptorsForUnitTest() {
|
|
||||||
return myInterceptors;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -103,309 +63,14 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
|
||||||
registerAnonymousInterceptor(thePointcut, Interceptor.DEFAULT_ORDER, theInterceptor);
|
registerAnonymousInterceptor(thePointcut, Interceptor.DEFAULT_ORDER, theInterceptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String theName) {
|
|
||||||
myName = theName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) {
|
public void registerAnonymousInterceptor(Pointcut thePointcut, int theOrder, IAnonymousInterceptor theInterceptor) {
|
||||||
Validate.notNull(thePointcut);
|
Validate.notNull(thePointcut);
|
||||||
Validate.notNull(theInterceptor);
|
Validate.notNull(theInterceptor);
|
||||||
synchronized (myRegistryMutex) {
|
BaseInvoker invoker = new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder);
|
||||||
|
registerAnonymousInterceptor(thePointcut, theInterceptor, invoker);
|
||||||
myAnonymousInvokers.put(thePointcut, new AnonymousLambdaInvoker(thePointcut, theInterceptor, theOrder));
|
|
||||||
if (!isInterceptorAlreadyRegistered(theInterceptor)) {
|
|
||||||
myInterceptors.add(theInterceptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Object> getAllRegisteredInterceptors() {
|
|
||||||
synchronized (myRegistryMutex) {
|
|
||||||
List<Object> 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<Object> theShouldUnregisterFunction) {
|
|
||||||
unregisterInterceptorsIf(theShouldUnregisterFunction, myGlobalInvokers);
|
|
||||||
unregisterInterceptorsIf(theShouldUnregisterFunction, myAnonymousInvokers);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unregisterInterceptorsIf(Predicate<Object> theShouldUnregisterFunction, ListMultimap<Pointcut, BaseInvoker> theGlobalInvokers) {
|
|
||||||
theGlobalInvokers.entries().removeIf(t->theShouldUnregisterFunction.test(t.getValue().getInterceptor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean registerThreadLocalInterceptor(Object theInterceptor) {
|
|
||||||
if (!myThreadlocalInvokersEnabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
|
|
||||||
scanInterceptorAndAddToInvokerMultimap(theInterceptor, invokers);
|
|
||||||
return !invokers.isEmpty();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unregisterThreadLocalInterceptor(Object theInterceptor) {
|
|
||||||
if (myThreadlocalInvokersEnabled) {
|
|
||||||
ListMultimap<Pointcut, BaseInvoker> invokers = getThreadLocalInvokerMultimap();
|
|
||||||
invokers.entries().removeIf(t -> t.getValue().getInterceptor() == theInterceptor);
|
|
||||||
if (invokers.isEmpty()) {
|
|
||||||
myThreadlocalInvokers.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListMultimap<Pointcut, BaseInvoker> getThreadLocalInvokerMultimap() {
|
|
||||||
ListMultimap<Pointcut, BaseInvoker> 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<HookInvoker> 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<Object> theObjects) {
|
|
||||||
IdentityHashMap<Object, Integer> 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<Pointcut, BaseInvoker> 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<BaseInvoker> 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<Object> 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<BaseInvoker> getInvokersForPointcut(Pointcut thePointcut) {
|
|
||||||
List<BaseInvoker> invokers;
|
|
||||||
|
|
||||||
synchronized (myRegistryMutex) {
|
|
||||||
List<BaseInvoker> globalInvokers = myGlobalInvokers.get(thePointcut);
|
|
||||||
List<BaseInvoker> anonymousInvokers = myAnonymousInvokers.get(thePointcut);
|
|
||||||
List<BaseInvoker> threadLocalInvokers = null;
|
|
||||||
if (myThreadlocalInvokersEnabled) {
|
|
||||||
ListMultimap<Pointcut, BaseInvoker> 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<BaseInvoker> union(List<BaseInvoker>... theInvokersLists) {
|
|
||||||
List<BaseInvoker> haveOne = null;
|
|
||||||
boolean haveMultiple = false;
|
|
||||||
for (List<BaseInvoker> nextInvokerList : theInvokersLists) {
|
|
||||||
if (nextInvokerList == null || nextInvokerList.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (haveOne == null) {
|
|
||||||
haveOne = nextInvokerList;
|
|
||||||
} else {
|
|
||||||
haveMultiple = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (haveOne == null) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<BaseInvoker> 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<String> wantedTypes = new ArrayList<>(thePointcut.getParameterTypes());
|
|
||||||
|
|
||||||
ListMultimap<Class<?>, 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 class AnonymousLambdaInvoker extends BaseInvoker {
|
||||||
private final IAnonymousInterceptor myHook;
|
private final IAnonymousInterceptor myHook;
|
||||||
|
@ -424,191 +89,5 @@ public class InterceptorService implements IInterceptorService, IInterceptorBroa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract static class BaseInvoker implements Comparable<BaseInvoker> {
|
|
||||||
|
|
||||||
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<Class<?>, 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<HookInvoker> scanInterceptorAndAddToInvokerMultimap(Object theInterceptor, ListMultimap<Pointcut, BaseInvoker> theInvokers) {
|
|
||||||
Class<?> interceptorClass = theInterceptor.getClass();
|
|
||||||
int typeOrder = determineOrder(interceptorClass);
|
|
||||||
|
|
||||||
List<HookInvoker> 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<BaseInvoker> nextInvokerList = theInvokers.get(nextPointcut);
|
|
||||||
nextInvokerList.sort(Comparator.naturalOrder());
|
|
||||||
}
|
|
||||||
|
|
||||||
return addedInvokers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Returns a list of any added invokers
|
|
||||||
*/
|
|
||||||
private static List<HookInvoker> scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
|
|
||||||
ArrayList<HookInvoker> retVal = new ArrayList<>();
|
|
||||||
for (Method nextMethod : ReflectionUtil.getDeclaredMethods(theInterceptor.getClass(), true)) {
|
|
||||||
Optional<Hook> 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 <T extends Annotation> Optional<T> findAnnotation(AnnotatedElement theObject, Class<T> 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<Interceptor> typeOrderAnnotation = findAnnotation(theInterceptorClass, Interceptor.class);
|
|
||||||
if (typeOrderAnnotation.isPresent()) {
|
|
||||||
typeOrder = typeOrderAnnotation.get().order();
|
|
||||||
}
|
|
||||||
return typeOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toErrorString(List<String> theParameterTypes) {
|
|
||||||
return theParameterTypes
|
|
||||||
.stream()
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.joining(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue