blob: f4d7f48daea305170f9e8e8c12deb0c8ac476732 [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.tuscany.sca.stripes;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletContext;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.controller.StripesFilter;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.ReflectUtil;
import org.apache.tuscany.sca.implementation.web.runtime.utils.ContextHelper;
import org.oasisopen.sca.annotation.Reference;
/**
* <p>Static helper class that is used to lookup SCA references and inject them into objects
* (often ActionBeans). Is capable of injecting references through setter methods (property access)
* and also through direct field access if the security policy allows it. Methods and fields
* must be annotated using the SCA {@code @Reference} annotation.</p>
*
* <p>Methods and fields may be public, protected, package-access or private. If they are not
* public an attempt is made to call {@link Method#setAccessible(boolean)} in order to make
* them accessible from this class. If the attempt fails, an exception will be thrown.</p>
*
* <p>Method names can take any form. For example {@code setSomeBean(Bean b)} or
* {@code someBean(bean b)}. In both cases, if a specific Reference name is not supplied,
* the default name of {@code someBean} will be used.</p>
*
* <p>The value of the {@code @Reference} annotation should be the reference on the
* SCA component with an {@code <implementation.web>} componentType.
*
* <p>The first time that any of the injection methods in this class is called with a specific type
* of object, the object's class is examined for annotated fields and methods. The discovered
* fields and methods are then cached for future usage.</p>
*
* Created for Tuscany from the Stripes SpringHelper written by Dan Hayes and Tim Fennell
*/
public class TuscanyHelper {
private static final Log log = Log.getInstance(TuscanyHelper.class);
/** Lazily filled in map of Class to methods annotated with Reference. */
private static Map<Class<?>, Collection<Method>> methodMap =
new ConcurrentHashMap<Class<?>, Collection<Method>>();
/** Lazily filled in map of Class to fields annotated with Reference. */
private static Map<Class<?>, Collection<Field>> fieldMap =
new ConcurrentHashMap<Class<?>, Collection<Field>>();
/**
* Injects SCA References using the ComponentContext that is
* derived from the ServletContext, which is in turn looked up using the
* ActionBeanContext.
*
* @param bean the object into which to inject SCA reference
* @param context the ActionBeanContext represented by the current request
*/
public static void injectBeans(Object bean, ActionBeanContext context) {
injectBeans(bean, StripesFilter.getConfiguration().getServletContext());
}
/**
* Looks for all methods and fields annotated with {@code @Reference} and attempts
* to lookup and inject a managed bean into the field/property. If any annotated
* element cannot be injected an exception is thrown.
*
* @param bean the bean into which to inject SCA reference
* @param ctx the SCA ComponentContext
*/
public static void injectBeans(Object bean, ServletContext ctx) {
// First inject any values using annotated methods
for (Method m : getMethods(bean.getClass())) {
try {
Reference scaReference = m.getAnnotation(Reference.class);
boolean nameSupplied = !"".equals(scaReference.name());
String name = nameSupplied ? scaReference.name() : methodToPropertyName(m);
Class<?> beanType = m.getParameterTypes()[0];
Object managedBean = findReference(ctx, name, beanType, !nameSupplied);
m.invoke(bean, managedBean);
}
catch (Exception e) {
throw new StripesRuntimeException("Exception while trying to lookup and inject " +
"an SCA Reference into a bean of type " + bean.getClass().getSimpleName() +
" using method " + m.toString(), e);
}
}
// And then inject any properties that are annotated
for (Field f : getFields(bean.getClass())) {
try {
Reference scaReference = f.getAnnotation(Reference.class);
boolean nameSupplied = !"".equals(scaReference.name());
String name = nameSupplied ? scaReference.name() : f.getName();
Object managedBean = findReference(ctx, name, f.getType(), !nameSupplied);
f.set(bean, managedBean);
}
catch (Exception e) {
throw new StripesRuntimeException("Exception while trying to lookup and inject " +
"an SCA Referenceinto a bean of type " + bean.getClass().getSimpleName() +
" using field access on field " + f.toString(), e);
}
}
}
/**
* Fetches the methods on a class that are annotated with Reference. The first time it
* is called for a particular class it will introspect the class and cache the results.
* All non-overridden methods are examined, including protected and private methods.
* If a method is not public an attempt it made to make it accessible - if it fails
* it is removed from the collection and an error is logged.
*
* @param clazz the class on which to look for Reference annotated methods
* @return the collection of methods with the annotation
*/
protected static Collection<Method> getMethods(Class<?> clazz) {
Collection<Method> methods = methodMap.get(clazz);
if (methods == null) {
methods = ReflectUtil.getMethods(clazz);
Iterator<Method> iterator = methods.iterator();
while (iterator.hasNext()) {
Method method = iterator.next();
if (!method.isAnnotationPresent(Reference.class)) {
iterator.remove();
}
else {
// If the method isn't public, try to make it accessible
if (!method.isAccessible()) {
try {
method.setAccessible(true);
}
catch (SecurityException se) {
throw new StripesRuntimeException(
"Method " + clazz.getName() + "." + method.getName() + "is marked " +
"with @Reference and is not public. An attempt to call " +
"setAccessible(true) resulted in a SecurityException. Please " +
"either make the method public or modify your JVM security " +
"policy to allow Stripes to setAccessible(true).", se);
}
}
// Ensure the method has only the one parameter
if (method.getParameterTypes().length != 1) {
throw new StripesRuntimeException(
"A method marked with @Reference must have exactly one parameter: " +
"the bean to be injected. Method [" + method.toGenericString() + "] has " +
method.getParameterTypes().length + " parameters."
);
}
}
}
methodMap.put(clazz, methods);
}
return methods;
}
/**
* Fetches the fields on a class that are annotated with Refernece. The first time it
* is called for a particular class it will introspect the class and cache the results.
* All non-overridden fields are examined, including protected and private fields.
* If a field is not public an attempt it made to make it accessible - if it fails
* it is removed from the collection and an error is logged.
*
* @param clazz the class on which to look for Reference annotated fields
* @return the collection of methods with the annotation
*/
protected static Collection<Field> getFields(Class<?> clazz) {
Collection<Field> fields = fieldMap.get(clazz);
if (fields == null) {
fields = ReflectUtil.getFields(clazz);
Iterator<Field> iterator = fields.iterator();
while (iterator.hasNext()) {
Field field = iterator.next();
if (!field.isAnnotationPresent(Reference.class)) {
iterator.remove();
}
else if (!field.isAccessible()) {
// If the field isn't public, try to make it accessible
try {
field.setAccessible(true);
}
catch (SecurityException se) {
throw new StripesRuntimeException(
"Field " + clazz.getName() + "." + field.getName() + "is marked " +
"with @Reference and is not public. An attempt to call " +
"setAccessible(true) resulted in a SecurityException. Please " +
"either make the field public, annotate a public setter instead " +
"or modify your JVM security policy to allow Stripes to " +
"setAccessible(true).", se);
}
}
}
fieldMap.put(clazz, fields);
}
return fields;
}
/**
* Looks up an SCA Reference from a ComponentContext. First looks for a bean
* with name specified. If no such bean exists, looks for a bean by type. If there is
* only one bean of the appropriate type, it is returned. If zero or more than one bean
* of the correct type exists, an exception is thrown.
*
* @param ctx the SCA ComponentContext
* @param name the name of the reference to look for
* @param type the type of bean to look for
* @param allowFindByType true to indicate that finding a bean by type is acceptable
* if find by name fails.
* @exception RuntimeException various subclasses of RuntimeException are thrown if it
* is not possible to find a unique matching bean in the ComponentContext given
* the constraints supplied.
*/
protected static Object findReference(ServletContext ctx,
String name,
Class<?> type,
boolean allowFindByType) {
// First try to lookup using the name provided
Object bean = ContextHelper.getReference(name, type, ctx);
if (bean == null) {
throw new StripesRuntimeException("no reference defined:" + name);
}
log.debug("Found sca reference with name [", name, "] and type [",
bean.getClass().getName(), "]");
return bean;
// TODO: Support get by type (sca autowire?)
// // If we got here then we didn't find a bean yet, try by type
// String[] beanNames = ctx.getBeanNamesForType(type);
// if (beanNames.length == 0) {
// throw new StripesRuntimeException(
// "Unable to find SpringBean with name [" + name + "] or type [" +
// type.getName() + "] in the Spring application context.");
// }
// else if (beanNames.length > 1) {
// throw new StripesRuntimeException(
// "Unable to find SpringBean with name [" + name + "] or unique bean with type [" +
// type.getName() + "] in the Spring application context. Found " + beanNames.length +
// "beans of matching type.");
// }
// else {
// log.warn("Found unique SpringBean with type [" + type.getName() + "]. Matching on ",
// "type is a little risky so watch out!");
// return ctx.getBean(beanNames[0], type);
// }
}
/**
* A slightly unusual, and somewhat "loose" conversion of a method name to a property
* name. Assumes that the name is in fact a mutator for a property and will do the
* usual {@code setFoo} to {@code foo} conversion if the method follows the normal
* syntax, otherwise will just return the method name.
*
* @param m the method to determine the property name of
* @return a String property name
*/
protected static String methodToPropertyName(Method m) {
String name = m.getName();
if (name.startsWith("set") && name.length() > 3) {
String ret = name.substring(3,4).toLowerCase();
if (name.length() > 4) ret += name.substring(4);
return ret;
}
else {
return name;
}
}
}