Refactoring to support "after invocation" processing.
This commit is contained in:
parent
03a530b36b
commit
5f6aa9c49e
|
@ -0,0 +1,103 @@
|
|||
/* Copyright 2004 Acegi Technology Pty Limited
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package net.sf.acegisecurity;
|
||||
|
||||
/**
|
||||
* Reviews the <code>Object</code> returned from a secure object invocation,
|
||||
* being able to modify the <code>Object</code> or throw an {@link
|
||||
* AccessDeniedException}.
|
||||
*
|
||||
* <p>
|
||||
* Typically used to ensure the principal is permitted to access the domain
|
||||
* object instance returned by a service layer bean. Can also be used to
|
||||
* mutate the domain object instance so the principal is only able to access
|
||||
* authorised bean properties or <code>Collection</code> elements. Often used
|
||||
* in conjunction with an {@link net.sf.acegisecurity.acl.AclManager} to
|
||||
* obtain the access control list applicable for the domain object instance.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Special consideration should be given to using an
|
||||
* <code>AfterInvocationManager</code> on bean methods that modify a database.
|
||||
* Typically an <code>AfterInvocationManager</code> is used with read-only
|
||||
* methods, such as <code>public DomainObject getById(id)</code>. If used with
|
||||
* methods that modify a database, a transaction manager should be used to
|
||||
* ensure any <code>AccessDeniedException</code> will cause a rollback of the
|
||||
* changes made by the transaction.
|
||||
* </p>
|
||||
*
|
||||
* @author Ben Alex
|
||||
* @version $Id$
|
||||
*/
|
||||
public interface AfterInvocationManager {
|
||||
//~ Methods ================================================================
|
||||
|
||||
/**
|
||||
* Given the details of a secure object invocation including its returned
|
||||
* <code>Object</code>, make an access control decision or optionally
|
||||
* modify the returned <code>Object</code>.
|
||||
*
|
||||
* @param authentication the caller that invoked the method
|
||||
* @param object the secured object that was called
|
||||
* @param config the configuration attributes associated with the secured
|
||||
* object that was invoked
|
||||
* @param returnedObject the <code>Object</code> that was returned from the
|
||||
* secure object invocation
|
||||
*
|
||||
* @return the <code>Object</code> that will ultimately be returned to the
|
||||
* caller (if an implementation does not wish to modify the object
|
||||
* to be returned to the caller, the implementation should simply
|
||||
* return the same object it was passed by the
|
||||
* <code>returnedObject</code> method argument)
|
||||
*
|
||||
* @throws AccessDeniedException if access is denied
|
||||
*/
|
||||
public Object decide(Authentication authentication, Object object,
|
||||
ConfigAttributeDefinition config, Object returnedObject)
|
||||
throws AccessDeniedException;
|
||||
|
||||
/**
|
||||
* Indicates whether this <code>AfterInvocationManager</code> is able to
|
||||
* process "after invocation" requests presented with the passed
|
||||
* <code>ConfigAttribute</code>.
|
||||
*
|
||||
* <p>
|
||||
* This allows the <code>AbstractSecurityInterceptor</code> to check every
|
||||
* configuration attribute can be consumed by the configured
|
||||
* <code>AccessDecisionManager</code> and/or <code>RunAsManager</code>
|
||||
* and/or <code>AfterInvocationManager</code>.
|
||||
* </p>
|
||||
*
|
||||
* @param attribute a configuration attribute that has been configured
|
||||
* against the <code>AbstractSecurityInterceptor</code>
|
||||
*
|
||||
* @return true if this <code>AfterInvocationManager</code> can support the
|
||||
* passed configuration attribute
|
||||
*/
|
||||
public boolean supports(ConfigAttribute attribute);
|
||||
|
||||
/**
|
||||
* Indicates whether the <code>AfterInvocationManager</code> implementation
|
||||
* is able to provide access control decisions for the indicated secured
|
||||
* object type.
|
||||
*
|
||||
* @param clazz the class that is being queried
|
||||
*
|
||||
* @return <code>true</code> if the implementation can process the
|
||||
* indicated class
|
||||
*/
|
||||
public boolean supports(Class clazz);
|
||||
}
|
|
@ -17,6 +17,7 @@ package net.sf.acegisecurity.intercept;
|
|||
|
||||
import net.sf.acegisecurity.AccessDecisionManager;
|
||||
import net.sf.acegisecurity.AccessDeniedException;
|
||||
import net.sf.acegisecurity.AfterInvocationManager;
|
||||
import net.sf.acegisecurity.Authentication;
|
||||
import net.sf.acegisecurity.AuthenticationCredentialsNotFoundException;
|
||||
import net.sf.acegisecurity.AuthenticationException;
|
||||
|
@ -74,7 +75,7 @@ import java.util.Set;
|
|||
* For an invocation that is secured (there is a
|
||||
* <code>ConfigAttributeDefinition</code> for the secure object invocation):
|
||||
*
|
||||
* <ol>
|
||||
* <ol type="a">
|
||||
* <li>
|
||||
* Authenticate the request against the configured {@link
|
||||
* AuthenticationManager}, replacing the <code>Authentication</code> object on
|
||||
|
@ -103,6 +104,11 @@ import java.util.Set;
|
|||
* object, return the <code>ContextHolder</code> to the object that existed
|
||||
* after the call to <code>AuthenticationManager</code>.
|
||||
* </li>
|
||||
* <li>
|
||||
* If an <code>AfterInvocationManager</code> is defined, invoke the invocation
|
||||
* manager and allow it to replace the object due to be returned to the
|
||||
* caller.
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* </li>
|
||||
|
@ -110,7 +116,7 @@ import java.util.Set;
|
|||
* For an invocation that is public (there is no
|
||||
* <code>ConfigAttributeDefinition</code> for the secure object invocation):
|
||||
*
|
||||
* <ol>
|
||||
* <ol type="a">
|
||||
* <li>
|
||||
* If the <code>ContextHolder</code> contains a <code>SecureContext</code>, set
|
||||
* the <code>isAuthenticated</code> flag on the <code>Authentication</code>
|
||||
|
@ -128,9 +134,9 @@ import java.util.Set;
|
|||
*
|
||||
* </li>
|
||||
* <li>
|
||||
* Control again returns to the concrete subclass, which will return to the
|
||||
* caller any result or exception that occurred when it proceeded with the
|
||||
* execution of the secure object.
|
||||
* Control again returns to the concrete subclass, along with the
|
||||
* <code>Object</code> that should be returned to the caller. The subclass
|
||||
* will then return that result or exception to the original caller.
|
||||
* </li>
|
||||
* </ol>
|
||||
* </p>
|
||||
|
@ -147,6 +153,7 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
//~ Instance fields ========================================================
|
||||
|
||||
private AccessDecisionManager accessDecisionManager;
|
||||
private AfterInvocationManager afterInvocationManager;
|
||||
private ApplicationContext context;
|
||||
private AuthenticationManager authenticationManager;
|
||||
private RunAsManager runAsManager = new NullRunAsManager();
|
||||
|
@ -154,11 +161,30 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
|
||||
//~ Methods ================================================================
|
||||
|
||||
public void setAfterInvocationManager(
|
||||
AfterInvocationManager afterInvocationManager) {
|
||||
this.afterInvocationManager = afterInvocationManager;
|
||||
}
|
||||
|
||||
public AfterInvocationManager getAfterInvocationManager() {
|
||||
return afterInvocationManager;
|
||||
}
|
||||
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
this.context = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the type of secure objects the subclass will be presenting to
|
||||
* the abstract parent for processing. This is used to ensure
|
||||
* collaborators wired to the <code>AbstractSecurityInterceptor</code> all
|
||||
* support the indicated secure object class.
|
||||
*
|
||||
* @return the type of secure object the subclass provides services for
|
||||
*/
|
||||
public abstract Class getSecureObjectClass();
|
||||
|
||||
public abstract ObjectDefinitionSource obtainObjectDefinitionSource();
|
||||
|
||||
public void setAccessDecisionManager(
|
||||
|
@ -223,51 +249,116 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
logger.warn(
|
||||
"Could not validate configuration attributes as the MethodDefinitionSource did not return a ConfigAttributeDefinition Iterator");
|
||||
}
|
||||
} else {
|
||||
Set set = new HashSet();
|
||||
|
||||
return;
|
||||
}
|
||||
while (iter.hasNext()) {
|
||||
ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter
|
||||
.next();
|
||||
Iterator attributes = def.getConfigAttributes();
|
||||
|
||||
Set set = new HashSet();
|
||||
while (attributes.hasNext()) {
|
||||
ConfigAttribute attr = (ConfigAttribute) attributes
|
||||
.next();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
ConfigAttributeDefinition def = (ConfigAttributeDefinition) iter
|
||||
.next();
|
||||
Iterator attributes = def.getConfigAttributes();
|
||||
|
||||
while (attributes.hasNext()) {
|
||||
ConfigAttribute attr = (ConfigAttribute) attributes.next();
|
||||
|
||||
if (!this.runAsManager.supports(attr)
|
||||
&& !this.accessDecisionManager.supports(attr)) {
|
||||
set.add(attr);
|
||||
if (!this.runAsManager.supports(attr)
|
||||
&& !this.accessDecisionManager.supports(attr)
|
||||
&& ((this.afterInvocationManager == null)
|
||||
|| !this.afterInvocationManager.supports(attr))) {
|
||||
set.add(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (set.size() == 0) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Validated configuration attributes");
|
||||
if (set.size() == 0) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Validated configuration attributes");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported configuration attributes: "
|
||||
+ set.toString());
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported configuration attributes: " + set.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (getSecureObjectClass() == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Subclass must provide a non-null response to getSecureObjectClass()");
|
||||
}
|
||||
|
||||
if (!this.accessDecisionManager.supports(getSecureObjectClass())) {
|
||||
throw new IllegalArgumentException(
|
||||
"AccessDecisionManager does not support secure object class: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
|
||||
boolean result = this.obtainObjectDefinitionSource().supports(getSecureObjectClass());
|
||||
|
||||
if (!result) {
|
||||
throw new IllegalArgumentException(
|
||||
"ObjectDefinitionSource does not support secure object class: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
|
||||
if (!this.runAsManager.supports(getSecureObjectClass())) {
|
||||
throw new IllegalArgumentException(
|
||||
"RunAsManager does not support secure object class: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
|
||||
if ((this.afterInvocationManager != null)
|
||||
&& !this.afterInvocationManager.supports(getSecureObjectClass())) {
|
||||
throw new IllegalArgumentException(
|
||||
"AfterInvocationManager does not support secure object class: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
|
||||
if (!this.obtainObjectDefinitionSource().supports(getSecureObjectClass())) {
|
||||
throw new IllegalArgumentException(
|
||||
"ObjectDefinitionSource does not support secure object class: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
}
|
||||
|
||||
protected void afterInvocation(InterceptorStatusToken token) {
|
||||
/**
|
||||
* Completes the work of the <code>AbstractSecurityInterceptor</code> after
|
||||
* the secure object invocation has been complete
|
||||
*
|
||||
* @param token as returned by the {@link #beforeInvocation(Object)}}
|
||||
* method
|
||||
* @param returnedObject any object returned from the secure object
|
||||
* invocation (may be<code>null</code>)
|
||||
*
|
||||
* @return the object the secure object invocation should ultimately return
|
||||
* to its caller (may be <code>null</code>)
|
||||
*/
|
||||
protected Object afterInvocation(InterceptorStatusToken token,
|
||||
Object returnedObject) {
|
||||
if (token == null) {
|
||||
return;
|
||||
// public object
|
||||
return returnedObject;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reverting to original Authentication: "
|
||||
+ token.getAuthenticated().toString());
|
||||
if (token.isContextHolderRefreshRequired()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reverting to original Authentication: "
|
||||
+ token.getAuthentication().toString());
|
||||
}
|
||||
|
||||
SecureContext secureContext = (SecureContext) ContextHolder
|
||||
.getContext();
|
||||
secureContext.setAuthentication(token.getAuthentication());
|
||||
ContextHolder.setContext(secureContext);
|
||||
}
|
||||
|
||||
SecureContext secureContext = (SecureContext) ContextHolder.getContext();
|
||||
secureContext.setAuthentication(token.getAuthenticated());
|
||||
ContextHolder.setContext(secureContext);
|
||||
if (afterInvocationManager != null) {
|
||||
returnedObject = afterInvocationManager.decide(token
|
||||
.getAuthentication(), token.getSecureObject(),
|
||||
token.getAttr(), returnedObject);
|
||||
}
|
||||
|
||||
return returnedObject;
|
||||
}
|
||||
|
||||
protected InterceptorStatusToken beforeInvocation(Object object) {
|
||||
|
@ -275,10 +366,11 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
throw new IllegalArgumentException("Object was null");
|
||||
}
|
||||
|
||||
if (!this.obtainObjectDefinitionSource().supports(object.getClass())) {
|
||||
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
|
||||
throw new IllegalArgumentException(
|
||||
"ObjectDefinitionSource does not support objects of type "
|
||||
+ object.getClass());
|
||||
"Security invocation attempted for object " + object
|
||||
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
|
||||
+ getSecureObjectClass());
|
||||
}
|
||||
|
||||
ConfigAttributeDefinition attr = this.obtainObjectDefinitionSource()
|
||||
|
@ -365,7 +457,8 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
"RunAsManager did not change Authentication object");
|
||||
}
|
||||
|
||||
return null; // no further work post-invocation
|
||||
return new InterceptorStatusToken(authenticated, false, attr,
|
||||
object); // no further work post-invocation
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Switching to RunAs Authentication: "
|
||||
|
@ -375,10 +468,8 @@ public abstract class AbstractSecurityInterceptor implements InitializingBean,
|
|||
context.setAuthentication(runAs);
|
||||
ContextHolder.setContext((Context) context);
|
||||
|
||||
InterceptorStatusToken token = new InterceptorStatusToken();
|
||||
token.setAuthenticated(authenticated);
|
||||
|
||||
return token; // revert to token.Authenticated post-invocation
|
||||
return new InterceptorStatusToken(authenticated, true, attr,
|
||||
object); // revert to token.Authenticated post-invocation
|
||||
}
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package net.sf.acegisecurity.intercept;
|
||||
|
||||
import net.sf.acegisecurity.Authentication;
|
||||
import net.sf.acegisecurity.ConfigAttributeDefinition;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,14 +24,9 @@ import net.sf.acegisecurity.Authentication;
|
|||
*
|
||||
* <P>
|
||||
* This class reflects the status of the security interception, so that the
|
||||
* final call to <code>AbstractSecurityInterceptor</code> can tidy up
|
||||
* correctly.
|
||||
* </p>
|
||||
*
|
||||
* <P>
|
||||
* Whilst this class currently only wraps a single object, it has been modelled
|
||||
* as a class so that future changes to the operation of
|
||||
* <code>AbstractSecurityInterceptor</code> are abstracted from subclasses.
|
||||
* final call to {@link
|
||||
* net.sf.acegisecurity.intercept.AbstractSecurityInterceptor#afterInvocation(InterceptorStatusToken,
|
||||
* Object)} can tidy up correctly.
|
||||
* </p>
|
||||
*
|
||||
* @author Ben Alex
|
||||
|
@ -39,15 +35,41 @@ import net.sf.acegisecurity.Authentication;
|
|||
public class InterceptorStatusToken {
|
||||
//~ Instance fields ========================================================
|
||||
|
||||
private Authentication authenticated;
|
||||
private Authentication authentication;
|
||||
private ConfigAttributeDefinition attr;
|
||||
private Object secureObject;
|
||||
private boolean contextHolderRefreshRequired;
|
||||
|
||||
//~ Constructors ===========================================================
|
||||
|
||||
public InterceptorStatusToken(Authentication authentication,
|
||||
boolean contextHolderRefreshRequired, ConfigAttributeDefinition attr,
|
||||
Object secureObject) {
|
||||
this.authentication = authentication;
|
||||
this.contextHolderRefreshRequired = contextHolderRefreshRequired;
|
||||
this.attr = attr;
|
||||
this.secureObject = secureObject;
|
||||
}
|
||||
|
||||
protected InterceptorStatusToken() {
|
||||
throw new IllegalArgumentException("Cannot use default constructor");
|
||||
}
|
||||
|
||||
//~ Methods ================================================================
|
||||
|
||||
public void setAuthenticated(Authentication authenticated) {
|
||||
this.authenticated = authenticated;
|
||||
public ConfigAttributeDefinition getAttr() {
|
||||
return attr;
|
||||
}
|
||||
|
||||
public Authentication getAuthenticated() {
|
||||
return authenticated;
|
||||
public Authentication getAuthentication() {
|
||||
return authentication;
|
||||
}
|
||||
|
||||
public boolean isContextHolderRefreshRequired() {
|
||||
return contextHolderRefreshRequired;
|
||||
}
|
||||
|
||||
public Object getSecureObject() {
|
||||
return secureObject;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,18 +58,8 @@ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor
|
|||
return this.objectDefinitionSource;
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
if (!this.getAccessDecisionManager().supports(MethodInvocation.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"AccessDecisionManager does not support MethodInvocation");
|
||||
}
|
||||
|
||||
if (!this.getRunAsManager().supports(MethodInvocation.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"RunAsManager does not support MethodInvocation");
|
||||
}
|
||||
public Class getSecureObjectClass() {
|
||||
return MethodInvocation.class;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,13 +73,13 @@ public class MethodSecurityInterceptor extends AbstractSecurityInterceptor
|
|||
* @throws Throwable if any error occurs
|
||||
*/
|
||||
public Object invoke(MethodInvocation mi) throws Throwable {
|
||||
Object result;
|
||||
Object result = null;
|
||||
InterceptorStatusToken token = super.beforeInvocation(mi);
|
||||
|
||||
try {
|
||||
result = mi.proceed();
|
||||
} finally {
|
||||
super.afterInvocation(token);
|
||||
result = super.afterInvocation(token, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.aspectj.lang.JoinPoint;
|
|||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The secure object type is <code>org.aspectj.lang.JointPoint</code>, which is
|
||||
* The secure object type is <code>org.aspectj.lang.JoinPoint</code>, which is
|
||||
* passed from the relevant <code>around()</code> advice. The
|
||||
* <code>around()</code> advice also passes an anonymous implementation of
|
||||
* {@link AspectJCallback} which contains the call for AspectJ to continue
|
||||
|
@ -64,18 +64,8 @@ public class AspectJSecurityInterceptor extends AbstractSecurityInterceptor {
|
|||
return this.objectDefinitionSource;
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
if (!this.getAccessDecisionManager().supports(JoinPoint.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"AccessDecisionManager does not support JointPoint");
|
||||
}
|
||||
|
||||
if (!this.getRunAsManager().supports(JoinPoint.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"RunAsManager does not support JointPoint");
|
||||
}
|
||||
public Class getSecureObjectClass() {
|
||||
return JoinPoint.class;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,13 +81,13 @@ public class AspectJSecurityInterceptor extends AbstractSecurityInterceptor {
|
|||
* @return The returned value from the method invocation
|
||||
*/
|
||||
public Object invoke(JoinPoint jp, AspectJCallback advisorProceed) {
|
||||
Object result;
|
||||
Object result = null;
|
||||
InterceptorStatusToken token = super.beforeInvocation(jp);
|
||||
|
||||
try {
|
||||
result = advisorProceed.proceedWithObject();
|
||||
} finally {
|
||||
super.afterInvocation(token);
|
||||
result = super.afterInvocation(token, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -59,18 +59,8 @@ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor {
|
|||
return this.objectDefinitionSource;
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
if (!this.getAccessDecisionManager().supports(FilterInvocation.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"AccessDecisionManager does not support FilterInvocation");
|
||||
}
|
||||
|
||||
if (!this.getRunAsManager().supports(FilterInvocation.class)) {
|
||||
throw new IllegalArgumentException(
|
||||
"RunAsManager does not support FilterInvocation");
|
||||
}
|
||||
public Class getSecureObjectClass() {
|
||||
return FilterInvocation.class;
|
||||
}
|
||||
|
||||
public void invoke(FilterInvocation fi) throws Throwable {
|
||||
|
@ -79,7 +69,7 @@ public class FilterSecurityInterceptor extends AbstractSecurityInterceptor {
|
|||
try {
|
||||
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
|
||||
} finally {
|
||||
super.afterInvocation(token);
|
||||
super.afterInvocation(token, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue