blob: 146cac2dff662ecccb113bb6b3bfe0dadc28d73e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.felix.scr.impl.inject.methods;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Map;
import org.apache.felix.scr.impl.inject.BaseParameter;
import org.apache.felix.scr.impl.inject.MethodResult;
import org.apache.felix.scr.impl.inject.internal.ClassUtils;
import org.apache.felix.scr.impl.logger.ComponentLogger;
import org.apache.felix.scr.impl.metadata.DSVersion;
import org.osgi.service.log.LogService;
/**
* Component method to be invoked on service (un)binding.
*/
public abstract class BaseMethod<P extends BaseParameter, T>
{
private final DSVersion dsVersion;
private final boolean configurableServiceProperties;
private final String m_methodName;
private final Class<?> m_componentClass;
private volatile Method m_method;
private final boolean m_methodRequired;
private volatile State m_state;
protected BaseMethod( final String methodName,
final boolean methodRequired,
final Class<?> componentClass,
final DSVersion dsVersion,
final boolean configurableServiceProperties )
{
m_methodName = methodName;
m_methodRequired = methodRequired;
m_componentClass = componentClass;
this.dsVersion = dsVersion;
this.configurableServiceProperties = configurableServiceProperties;
if ( m_methodName == null )
{
m_state = NotApplicable.INSTANCE;
}
else
{
m_state = NotResolved.INSTANCE;
}
}
protected final DSVersion getDSVersion()
{
return dsVersion;
}
protected final boolean isDS12Felix()
{
return configurableServiceProperties;
}
protected final String getMethodName()
{
return m_methodName;
}
final Method getMethod()
{
return m_method;
}
protected final Class<?> getComponentClass()
{
return m_componentClass;
}
protected abstract void setTypes(T types);
synchronized void setMethod(MethodInfo<T> methodInfo, ComponentLogger logger)
{
this.m_method = methodInfo == null ? null : methodInfo.getMethod();
if (m_method != null)
{
setTypes(methodInfo.getTypes());
m_state = Resolved.INSTANCE;
logger.log( LogService.LOG_DEBUG, "Found {0} method: {1}", null,
getMethodNamePrefix(), m_method );
}
else if ( m_methodRequired )
{
m_state = NotFound.INSTANCE;
logger.log(LogService.LOG_ERROR, "{0} method [{1}] not found; Component will fail",
null,
getMethodNamePrefix(), getMethodName());
}
else
{
// optional method not found, log as DEBUG and ignore
logger.log( LogService.LOG_DEBUG, "{0} method [{1}] not found, ignoring", null,
getMethodNamePrefix(), getMethodName() );
m_state = NotApplicable.INSTANCE;
}
}
State getState()
{
return m_state;
}
/**
* Finds the method named in the {@link #m_methodName} field in the given
* <code>targetClass</code>. If the target class has no acceptable method
* the class hierarchy is traversed until a method is found or the root
* of the class hierarchy is reached without finding a method.
*
* @return The requested method or <code>null</code> if no acceptable method
* can be found in the target class or any super class.
* @throws InvocationTargetException If an unexpected Throwable is caught
* trying to find the requested method.
* @param logger
*/
private MethodInfo<T> findMethod(final ComponentLogger logger) throws InvocationTargetException
{
boolean acceptPrivate = getDSVersion().isDS11();
boolean acceptPackage = getDSVersion().isDS11();
final Class<?> targetClass = getComponentClass();
final ClassLoader targetClasslLoader = targetClass.getClassLoader();
final String targetPackage = getPackageName( targetClass );
Class<?> theClass = targetClass;
while (true)
{
if ( logger.isLogEnabled( LogService.LOG_DEBUG ) )
{
logger.log( LogService.LOG_DEBUG,
"Locating method " + getMethodName() + " in class " + theClass.getName(), null );
}
try
{
MethodInfo<T> method = doFindMethod(theClass, acceptPrivate,
acceptPackage,
logger);
if ( method != null )
{
return method;
}
}
catch ( SuitableMethodNotAccessibleException ex )
{
// log and return null
logger.log( LogService.LOG_ERROR,
"findMethod: Suitable but non-accessible method {0} found in class {1}, subclass of {2}", null,
getMethodName(), theClass.getName(), targetClass.getName() );
break;
}
// if we get here, we have no method, so check the super class
theClass = theClass.getSuperclass();
if ( theClass == null )
{
break;
}
// super class method check ignores private methods and accepts
// package methods only if in the same package and package
// methods are (still) allowed
acceptPackage &= targetClasslLoader == theClass.getClassLoader()
&& targetPackage.equals( getPackageName( theClass ) );
// private methods will not be accepted any more in super classes
acceptPrivate = false;
}
// nothing found after all these years ...
return null;
}
protected abstract MethodInfo<T> doFindMethod(Class<?> targetClass,
boolean acceptPrivate,
boolean acceptPackage,
ComponentLogger logger ) throws SuitableMethodNotAccessibleException, InvocationTargetException;
private String[] getParametersForLogging(final Object[] params) {
if (params == null) {
return null;
}
final String[] result = new String[params.length];
for (int i = 0; i < params.length; i++) {
result[i] = (params[i] == null ? null : params[i].getClass().getName());
}
return result;
}
private MethodResult invokeMethod(final Object componentInstance, final P rawParameter )
throws InvocationTargetException
{
final ComponentLogger logger = rawParameter.getComponentContext().getLogger();
try
{
if ( componentInstance != null )
{
final Object[] params = getParameters(m_method, rawParameter);
if (logger.isLogEnabled(LogService.LOG_DEBUG)) {
logger.log(LogService.LOG_DEBUG, "invoking {0}: {1}: parameters {2}", null, getMethodNamePrefix(),
getMethodName(), Arrays.asList(getParametersForLogging(params)));
}
final Object result = m_method.invoke(componentInstance, params);
logger.log(LogService.LOG_DEBUG, "invoked {0}: {1}", null,
getMethodNamePrefix(), getMethodName() );
return new MethodResult((m_method.getReturnType() != Void.TYPE), (Map<String, Object>) result);
}
else
{
rawParameter.getComponentContext().getLogger().log( LogService.LOG_WARNING, "Method {0}: {1} cannot be called on null object",
null,
getMethodNamePrefix(), getMethodName() );
}
}
catch ( IllegalStateException ise )
{
rawParameter.getComponentContext().getLogger().log( LogService.LOG_DEBUG, ise.getMessage(), null );
return null;
}
catch ( IllegalAccessException ex )
{
// 112.3.1 If the method is not is not declared protected or
// public, SCR must log an error message with the log service,
// if present, and ignore the method
rawParameter.getComponentContext().getLogger().log( LogService.LOG_DEBUG, "Method {0} cannot be called", ex,
getMethodName() );
}
catch ( InvocationTargetException ex )
{
throw ex;
}
catch ( Throwable t )
{
throw new InvocationTargetException( t );
}
// assume success (also if the method is not available or accessible)
return MethodResult.VOID;
}
protected boolean returnValue()
{
// allow returning Map if declared as DS 1.2-Felix or newer
return isDS12Felix();
}
/**
* Returns the parameter array created from the <code>rawParameter</code>
* using the actual parameter type list of the <code>method</code>.
* @param method
* @param rawParameter
* @return
* @throws IllegalStateException If the required parameters cannot be
* extracted from the <code>rawParameter</code>
*/
protected abstract Object[] getParameters( Method method, P rawParameter );
protected String getMethodNamePrefix()
{
return "";
}
//---------- Helpers
/**
* Finds the named public or protected method in the given class or any
* super class. If such a method is found, its accessibility is enfored by
* calling the <code>Method.setAccessible</code> method if required and
* the method is returned. Enforcing accessibility is required to support
* invocation of protected methods.
*
*
* @param clazz The <code>Class</code> which provides the method.
* @param name The name of the method.
* @param parameterTypes The parameters to the method. Passing
* <code>null</code> is equivalent to using an empty array.
*
* @param logger
* @return The named method with enforced accessibility or <code>null</code>
* if no such method exists in the class.
*
* @throws SuitableMethodNotAccessibleException If method with the given
* name taking the parameters is found in the class but the method
* is not accessible.
* @throws InvocationTargetException If an unexpected Throwable is caught
* trying to access the desired method.
*/
public /* static */ Method getMethod( Class<?> clazz,
String name,
Class[] parameterTypes,
boolean acceptPrivate,
boolean acceptPackage,
ComponentLogger logger ) throws SuitableMethodNotAccessibleException,
InvocationTargetException
{
try
{
// find the declared method in this class
Method method = clazz.getDeclaredMethod( name, parameterTypes );
// accept public and protected methods only and ensure accessibility
if ( accept( method, acceptPrivate, acceptPackage, returnValue() ) )
{
return method;
}
// the method would fit the requirements but is not acceptable
throw new SuitableMethodNotAccessibleException();
}
catch ( NoSuchMethodException nsme )
{
// thrown if no method is declared with the given name and
// parameters
if ( logger.isLogEnabled( LogService.LOG_DEBUG ) )
{
String argList = ( parameterTypes != null ) ? Arrays.asList( parameterTypes ).toString() : "";
logger.log( LogService.LOG_DEBUG, "Declared Method {0}.{1}({2}) not found", null,
clazz.getName(), name, argList );
}
}
catch ( NoClassDefFoundError cdfe )
{
// may be thrown if a method would be found but the signature
// contains throws declaration for an exception which cannot
// be loaded
if ( logger.isLogEnabled( LogService.LOG_WARNING ) )
{
StringBuilder buf = new StringBuilder();
buf.append( "Failure loooking up method " ).append( name ).append( '(' );
for ( int i = 0; parameterTypes != null && i < parameterTypes.length; i++ )
{
buf.append( parameterTypes[i].getName() );
if ( i > 0 )
{
buf.append( ", " );
}
}
buf.append( ") in class class " ).append( clazz.getName() ).append( ". Assuming no such method." );
logger.log( LogService.LOG_WARNING, buf.toString(), cdfe );
}
}
catch ( SuitableMethodNotAccessibleException e)
{
throw e;
}
catch ( Throwable throwable )
{
// unexpected problem accessing the method, don't let everything
// blow up in this situation, just throw a declared exception
throw new InvocationTargetException( throwable, "Unexpected problem trying to get method " + name );
}
// caught and ignored exception, assume no method and continue search
return null;
}
/**
* Returns <code>true</code> if the method is acceptable to be returned from the
* {@link #getMethod(Class, String, Class[], boolean, boolean, ComponentLogger)} and also
* makes the method accessible.
* <p>
* This method returns <code>true</code> iff:
* <ul>
* <li>The method has <code>void</code> return type</li>
* <li>Is not static</li>
* <li>Is public or protected</li>
* <li>Is private and <code>acceptPrivate</code> is <code>true</code></li>
* <li>Is package private and <code>acceptPackage</code> is <code>true</code></li>
* </ul>
* <p>
* This method is package private for unit testing purposes. It is not
* meant to be called from client code.
*
*
* @param method The method to check
* @param acceptPrivate Whether a private method is acceptable
* @param acceptPackage Whether a package private method is acceptable
* @param allowReturnValue whether the method can return a value (to update service registration properties)
* @return whether the method is acceptable
*/
protected static boolean accept( final Method method, boolean acceptPrivate, boolean acceptPackage, boolean allowReturnValue )
{
if (!(Void.TYPE == method.getReturnType() || (ClassUtils.MAP_CLASS == method.getReturnType() && allowReturnValue)))
{
return false;
}
// check modifiers now
int mod = method.getModifiers();
// no static method
if ( Modifier.isStatic( mod ) )
{
return false;
}
// accept public and protected methods
if ( Modifier.isPublic( mod ) || Modifier.isProtected( mod ) )
{
setAccessible( method );
return true;
}
// accept private if accepted
if ( Modifier.isPrivate( mod ) )
{
if ( acceptPrivate )
{
setAccessible( method );
return true;
}
return false;
}
// accept default (package)
if ( acceptPackage )
{
setAccessible( method );
return true;
}
// else don't accept
return false;
}
private static void setAccessible(final Method method)
{
AccessController.doPrivileged( new PrivilegedAction<Object>()
{
@Override
public Object run()
{
method.setAccessible( true );
return null;
}
} );
}
/**
* Returns the name of the package to which the class belongs or an
* empty string if the class is in the default package.
*/
public static String getPackageName( Class<?> clazz )
{
String name = clazz.getName();
int dot = name.lastIndexOf( '.' );
return ( dot > 0 ) ? name.substring( 0, dot ) : "";
}
//---------- State management ------------------------------------
/**
* Calls the declared method on the given component with the provided
* method call arguments.
*
*
*
*
* @param componentInstance The component instance on which to call the
* method
* @param rawParameter The parameter container providing the actual
* parameters to provide to the called method
* @param methodCallFailureResult The result to return from this method if
* calling the method resulted in an exception.
*
* @param logger
* @return <code>true</code> if the method was called successfully or the
* method was not found and was not required. <code>false</code> if
* the method was not found but required.
* <code>methodCallFailureResult</code> is returned if the method was
* found and called, but the method threw an exception.
*/
public MethodResult invoke( final Object componentInstance, final P rawParameter,
final MethodResult methodCallFailureResult )
{
try
{
return m_state.invoke( this, componentInstance, rawParameter );
}
catch ( InvocationTargetException ite )
{
rawParameter.getComponentContext().getLogger().log( LogService.LOG_ERROR, "The {0} method has thrown an exception", ite.getCause(),
getMethodName() );
if ( methodCallFailureResult != null && methodCallFailureResult.getResult() != null )
{
methodCallFailureResult.getResult().put("exception", ite.getCause());
}
}
return methodCallFailureResult;
}
public boolean methodExists( ComponentLogger logger )
{
return m_state.methodExists( this, logger );
}
protected static final class MethodInfo<T>
{
private final Method m_method;
private final T m_types;
public MethodInfo(Method m)
{
this(m, null);
}
public MethodInfo(Method m, T types)
{
m_method = m;
m_types = types;
}
public Method getMethod()
{
return m_method;
}
public T getTypes()
{
return m_types;
}
}
private static interface State
{
<P extends BaseParameter, T> MethodResult invoke( BaseMethod<P, T> baseMethod, Object componentInstance, P rawParameter )
throws InvocationTargetException;
<P extends BaseParameter, T> boolean methodExists( BaseMethod<P, T> baseMethod, ComponentLogger logger );
}
private static class NotApplicable implements State
{
private static final State INSTANCE = new NotApplicable();
@Override
public <P extends BaseParameter, T> MethodResult invoke( final BaseMethod<P, T> baseMethod, final Object componentInstance, final P rawParameter )
{
return MethodResult.VOID;
}
@Override
public <P extends BaseParameter, T> boolean methodExists( final BaseMethod<P, T> baseMethod, ComponentLogger logger )
{
return true;
}
}
private static class NotResolved implements State
{
private static final State INSTANCE = new NotResolved();
private <P extends BaseParameter, T> void resolve( final BaseMethod<P, T> baseMethod, ComponentLogger logger )
{
logger.log( LogService.LOG_DEBUG, "getting {0}: {1}", null,
baseMethod.getMethodNamePrefix(), baseMethod.getMethodName() );
// resolve the method
MethodInfo<T> method = null;
try
{
method = baseMethod.findMethod( logger );
}
catch ( InvocationTargetException ex )
{
logger.log( LogService.LOG_WARNING, "{0} cannot be found", ex.getTargetException(),
baseMethod.getMethodName() );
}
baseMethod.setMethod( method, logger );
}
@Override
public <P extends BaseParameter, T> MethodResult invoke( final BaseMethod<P, T> baseMethod, final Object componentInstance, final P rawParameter )
throws InvocationTargetException
{
resolve( baseMethod, rawParameter.getComponentContext().getLogger() );
return baseMethod.getState().invoke( baseMethod, componentInstance, rawParameter );
}
@Override
public <P extends BaseParameter, T> boolean methodExists( final BaseMethod<P, T> baseMethod, ComponentLogger logger )
{
resolve( baseMethod, logger );
return baseMethod.getState().methodExists( baseMethod, logger );
}
}
private static class NotFound implements State
{
private static final State INSTANCE = new NotFound();
@Override
public <P extends BaseParameter, T> MethodResult invoke( final BaseMethod<P, T> baseMethod, final Object componentInstance, final P rawParameter )
{
// 112.3.1 If the method is not found , SCR must log an error
// message with the log service, if present, and ignore the
// method
rawParameter.getComponentContext().getLogger().log( LogService.LOG_ERROR, "{0} method [{1}] not found", null,
baseMethod.getMethodNamePrefix(), baseMethod.getMethodName() );
return null;
}
@Override
public <P extends BaseParameter, T> boolean methodExists( final BaseMethod<P, T> baseMethod, ComponentLogger logger )
{
return false;
}
}
private static class Resolved implements State
{
private static final State INSTANCE = new Resolved();
@Override
public <P extends BaseParameter, T> MethodResult invoke( final BaseMethod<P, T> baseMethod, final Object componentInstance, final P rawParameter )
throws InvocationTargetException
{
return baseMethod.invokeMethod( componentInstance, rawParameter );
}
@Override
public <P extends BaseParameter, T> boolean methodExists( final BaseMethod<P, T> baseMethod, ComponentLogger logger )
{
return true;
}
}
}