blob: fdb9c095909150806f93d15f1dadd77731b29046 [file] [log] [blame]
// Copyright 2007, 2008 The Apache Software Foundation
//
// 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 org.apache.tapestry.internal.services;
import org.apache.tapestry.PropertyConduit;
import org.apache.tapestry.internal.events.InvalidationListener;
import org.apache.tapestry.internal.util.MultiKey;
import org.apache.tapestry.ioc.AnnotationProvider;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newConcurrentMap;
import static org.apache.tapestry.ioc.internal.util.Defense.notBlank;
import static org.apache.tapestry.ioc.internal.util.Defense.notNull;
import org.apache.tapestry.ioc.internal.util.GenericsUtils;
import org.apache.tapestry.ioc.services.*;
import org.apache.tapestry.ioc.util.BodyBuilder;
import org.apache.tapestry.services.ComponentLayer;
import org.apache.tapestry.services.PropertyConduitSource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.regex.Pattern;
public class PropertyConduitSourceImpl implements PropertyConduitSource, InvalidationListener
{
private interface ReadInfo extends AnnotationProvider
{
/**
* The name of the method to invoke.
*/
String getMethodName();
/**
* The return type of the method, or the type of the property.
*/
Class getType();
/**
* True if an explicit cast to the return type is needed (typically because of generics).
*/
boolean isCastRequired();
}
/**
* Result from writing the property navigation portion of the expression. For getter methods, the navigation is all
* terms in the expression; for setter methods, the navigation is all but the last term.
*/
private interface PropertyNavigationResult
{
/**
* The name of the variable holding the final step in the expression.
*/
String getFinalStepVariable();
/**
* The type of the final step variable.
*/
Class getFinalStepType();
/**
* The method read information for the final term in the navigation portion of the expression.
*/
ReadInfo getFinalReadInfo();
}
private static final String PARENS = "()";
private final PropertyAccess access;
private final ClassFactory classFactory;
private final Map<Class, Class> classToEffectiveClass = newConcurrentMap();
/**
* Keyed on combination of root class and expression.
*/
private final Map<MultiKey, PropertyConduit> cache = newConcurrentMap();
private static final MethodSignature GET_SIGNATURE = new MethodSignature(Object.class, "get",
new Class[] { Object.class }, null);
private static final MethodSignature SET_SIGNATURE = new MethodSignature(void.class, "set",
new Class[] { Object.class, Object.class },
null);
private final Pattern SPLIT_AT_DOTS = Pattern.compile("\\.");
public PropertyConduitSourceImpl(PropertyAccess access, @ComponentLayer ClassFactory classFactory)
{
this.access = access;
this.classFactory = classFactory;
}
public PropertyConduit create(Class rootClass, String expression)
{
notNull(rootClass, "rootClass");
notBlank(expression, "expression");
Class effectiveClass = toEffectiveClass(rootClass);
MultiKey key = new MultiKey(effectiveClass, expression);
PropertyConduit result = cache.get(key);
if (result == null)
{
result = build(effectiveClass, expression);
cache.put(key, result);
}
return result;
}
private Class toEffectiveClass(Class rootClass)
{
Class result = classToEffectiveClass.get(rootClass);
if (result == null)
{
result = classFactory.importClass(rootClass);
classToEffectiveClass.put(rootClass, result);
}
return result;
}
/**
* Clears its caches when the component class loader is invalidated; this is because it will be common to generate
* conduits rooted in a component class (which will no longer be valid and must be released to the garbage
* collector).
*/
public void objectWasInvalidated()
{
cache.clear();
classToEffectiveClass.clear();
}
/**
* Builds a subclass of {@link BasePropertyConduit} that implements the get() and set() methods and overrides the
* constructor. In a worst-case race condition, we may build two (or more) conduits for the same
* rootClass/expression, and it will get sorted out when the conduit is stored into the cache.
*
* @param rootClass
* @param expression
* @return the conduit
*/
private PropertyConduit build(Class rootClass, String expression)
{
String name = ClassFabUtils.generateClassName("PropertyConduit");
ClassFab classFab = classFactory.newClass(name, BasePropertyConduit.class);
classFab.addConstructor(new Class[] { Class.class, AnnotationProvider.class, String.class }, null,
"super($$);");
String[] terms = SPLIT_AT_DOTS.split(expression);
final ReadInfo readInfo = buildGetter(rootClass, classFab, expression, terms);
final Method writeMethod = buildSetter(rootClass, classFab, expression, terms);
// A conduit is either readable or writable, otherwise there will already have been
// an error about unknown method name or property name.
Class propertyType = readInfo != null ? readInfo.getType() : writeMethod
.getParameterTypes()[0];
String description = String.format("PropertyConduit[%s %s]", rootClass.getName(), expression);
Class conduitClass = classFab.createClass();
AnnotationProvider provider = new AnnotationProvider()
{
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
{
T result = readInfo == null ? null : readInfo.getAnnotation(annotationClass);
if (result == null && writeMethod != null) result = writeMethod.getAnnotation(annotationClass);
return result;
}
};
try
{
return (PropertyConduit) conduitClass.getConstructors()[0].newInstance(propertyType, provider, description);
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
}
private ReadInfo buildGetter(Class rootClass, ClassFab classFab, String expression, String[] terms)
{
BodyBuilder builder = new BodyBuilder();
builder.begin();
PropertyNavigationResult result = writePropertyNavigationCode(builder, rootClass, expression, terms, false);
if (result == null)
{
builder.clear();
builder
.addln("throw new RuntimeException(\"Expression %s for class %s is write-only.\");", expression,
rootClass.getName());
}
else
{
builder.addln("return %s;", result.getFinalStepVariable());
builder.end();
}
classFab.addMethod(Modifier.PUBLIC, GET_SIGNATURE, builder.toString());
return result == null ? null : result.getFinalReadInfo();
}
/**
* Writes the code for navigation
*
* @param builder
* @param rootClass
* @param expression
* @param terms
* @param forSetter if true, then the last term is not read since it will be updated
* @return
*/
private PropertyNavigationResult writePropertyNavigationCode(BodyBuilder builder, Class rootClass,
String expression, String[] terms, boolean forSetter)
{
builder.addln("%s root = (%<s) $1;", ClassFabUtils.toJavaClassName(rootClass));
String previousStep = "root";
builder.addln(
"if (root == null) throw new NullPointerException(\"Root object of property expression '%s' is null.\");",
expression);
Class activeType = rootClass;
ReadInfo readInfo = null;
// For a setter method, the navigation stops with the penultimate
// term in the expression (the final term is what gets updated).
int lastIndex = forSetter ? terms.length - 1 : terms.length;
for (int i = 0; i < lastIndex; i++)
{
String thisStep = "step" + (i + 1);
String term = terms[i];
boolean nullable = term.endsWith("?");
if (nullable) term = term.substring(0, term.length() - 1);
// All the navigation terms in the expression must be readable properties.
// The only exception is the final term in a reader method.
boolean mustExist = forSetter || i < terms.length - 1;
readInfo = readInfoForTerm(activeType, expression, term, mustExist);
// Means the property for this step exists but is write only, which is a problem!
// This can only happen for getter methods, we return null to indicate that
// the expression is write-only.
if (readInfo == null) return null;
// If a primitive type, convert to wrapper type
Class termType = readInfo.getType();
Class wrappedType = ClassFabUtils.getWrapperType(termType);
String termJavaName = ClassFabUtils.toJavaClassName(wrappedType);
builder.add("%s %s = ", termJavaName, thisStep);
// Casts are needed for primitives, and for the case where
// generics are involved.
if (termType.isPrimitive())
{
builder.add(" ($w) ");
}
else if (readInfo.isCastRequired())
{
builder.add(" (%s) ", termJavaName);
}
builder.addln("%s.%s();", previousStep, readInfo.getMethodName());
if (nullable)
{
builder.add("if (%s == null) return", thisStep);
if (!forSetter) builder.add(" null");
builder.addln(";");
}
else
{
// Perform a null check on intermediate terms.
if (i < lastIndex - 1)
{
builder.addln("if (%s == null) throw new NullPointerException(%s.nullTerm(\"%s\", \"%s\", root));",
thisStep, getClass().getName(), term, expression);
}
}
activeType = wrappedType;
previousStep = thisStep;
}
final String finalStepVariable = previousStep;
final Class finalStepType = activeType;
final ReadInfo finalReadInfo = readInfo;
return new PropertyNavigationResult()
{
public String getFinalStepVariable()
{
return finalStepVariable;
}
public Class getFinalStepType()
{
return finalStepType;
}
public ReadInfo getFinalReadInfo()
{
return finalReadInfo;
}
};
}
private Method buildSetter(Class rootClass, ClassFab classFab, String expression, String[] terms)
{
BodyBuilder builder = new BodyBuilder();
builder.begin();
PropertyNavigationResult result = writePropertyNavigationCode(builder, rootClass, expression, terms, true);
// Because we pass true for the forSetter parameter, we know that the expression for the leading
// terms is a chain of readable expressions. But is the final term writable?
Method writeMethod = writeMethodForTerm(result.getFinalStepType(), expression, terms[terms.length - 1]);
if (writeMethod == null)
{
builder.clear();
builder
.addln("throw new RuntimeException(\"Expression %s for class %s is read-only.\");", expression,
rootClass.getName());
classFab.addMethod(Modifier.PUBLIC, SET_SIGNATURE, builder.toString());
return null;
}
Class propertyType = writeMethod.getParameterTypes()[0];
String propertyTypeName = ClassFabUtils.toJavaClassName(propertyType);
// Cast the parameter from Object to the expected type for the method.
builder.addln("%s value = %s;", propertyTypeName, ClassFabUtils.castReference("$2", propertyTypeName));
// Invoke the method.
builder.addln("%s.%s(value);", result.getFinalStepVariable(), writeMethod.getName());
builder.end();
classFab.addMethod(Modifier.PUBLIC, SET_SIGNATURE, builder.toString());
return writeMethod;
}
private Method writeMethodForTerm(Class activeType, String expression, String term)
{
if (term.endsWith(PARENS)) return null;
ClassPropertyAdapter classAdapter = access.getAdapter(activeType);
PropertyAdapter adapter = classAdapter.getPropertyAdapter(term);
if (adapter == null) throw new RuntimeException(
ServicesMessages.noSuchProperty(activeType, term, expression, classAdapter.getPropertyNames()));
return adapter.getWriteMethod();
}
private ReadInfo readInfoForTerm(Class activeType, String expression, String term, boolean mustExist)
{
if (term.endsWith(PARENS))
{
String methodName = term.substring(0, term.length() - PARENS.length());
try
{
final Method method = findMethod(activeType, methodName);
if (method.getReturnType().equals(void.class))
throw new RuntimeException(ServicesMessages.methodIsVoid(term, activeType, expression));
final Class genericType = GenericsUtils.extractGenericReturnType(activeType, method);
return new ReadInfo()
{
public String getMethodName()
{
return method.getName();
}
public Class getType()
{
return genericType;
}
public boolean isCastRequired()
{
return genericType != method.getReturnType();
}
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
{
return method.getAnnotation(annotationClass);
}
};
}
catch (NoSuchMethodException ex)
{
throw new RuntimeException(ServicesMessages.methodNotFound(term, activeType, expression), ex);
}
}
// Otherwise, just a property name.
ClassPropertyAdapter classAdapter = access.getAdapter(activeType);
final PropertyAdapter adapter = classAdapter.getPropertyAdapter(term);
if (adapter == null) throw new RuntimeException(
ServicesMessages.noSuchProperty(activeType, term, expression, classAdapter.getPropertyNames()));
if (!adapter.isRead())
{
if (mustExist) throw new RuntimeException(ServicesMessages.writeOnlyProperty(term, activeType, expression));
return null;
}
return new ReadInfo()
{
public String getMethodName()
{
return adapter.getReadMethod().getName();
}
public Class getType()
{
return adapter.getType();
}
public boolean isCastRequired()
{
return adapter.isCastRequired();
}
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
{
return adapter.getAnnotation(annotationClass);
}
};
}
private Method findMethod(Class activeType, String methodName) throws NoSuchMethodException
{
for (Method method : activeType.getMethods())
{
if (method.getParameterTypes().length == 0 && method.getName().equalsIgnoreCase(methodName)) return method;
}
throw new NoSuchMethodException(ServicesMessages.noSuchMethod(activeType, methodName));
}
public static String nullTerm(String term, String expression, Object root)
{
return String.format("Property '%s' (within property expression '%s', of %s) is null.",
term, expression, root);
}
}