blob: 28ea9df3b14d04d19d32f003cd75232937e26ca9 [file] [log] [blame]
// Copyright 2006, 2007, 2008, 2009, 2010 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.tapestry5.internal.transform;
import java.util.List;
import org.apache.tapestry5.Binding;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.func.Predicate;
import org.apache.tapestry5.internal.InternalComponentResources;
import org.apache.tapestry5.internal.bindings.LiteralBinding;
import org.apache.tapestry5.internal.services.ComponentClassCache;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.TapestryException;
import org.apache.tapestry5.ioc.services.PerthreadManager;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.services.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Responsible for identifying parameters via the {@link org.apache.tapestry5.annotations.Parameter} annotation on
* component fields. This is one of the most complex of the transformations.
*/
public class ParameterWorker implements ComponentClassTransformWorker
{
private final Logger logger = LoggerFactory.getLogger(ParameterWorker.class);
private final class InvokeResetOnParameterConduit implements ComponentMethodAdvice
{
private final FieldAccess conduitAccess;
private InvokeResetOnParameterConduit(FieldAccess conduitAccess)
{
this.conduitAccess = conduitAccess;
}
public void advise(ComponentMethodInvocation invocation)
{
getConduit(invocation, conduitAccess).reset();
invocation.proceed();
}
}
/**
* Contains the per-thread state about a parameter, as stored (using
* a unique key) in the {@link PerthreadManager}. Externalizing such state
* is part of Tapestry 5.2's pool-less pages.
*/
private final class ParameterState
{
boolean cached;
Object value;
void reset(Object defaultValue)
{
cached = false;
value = defaultValue;
}
}
private final class InvokeParameterDefaultMethod implements ComponentMethodAdvice
{
private final FieldAccess conduitAccess;
private final MethodAccess defaultMethodAccess;
private InvokeParameterDefaultMethod(FieldAccess conduitAccess, MethodAccess defaultMethodAccess)
{
this.conduitAccess = conduitAccess;
this.defaultMethodAccess = defaultMethodAccess;
}
public void advise(ComponentMethodInvocation invocation)
{
logger.debug(String.format("%s invoking default method %s", invocation.getComponentResources()
.getCompleteId(), defaultMethodAccess));
MethodInvocationResult result = defaultMethodAccess.invoke(invocation.getInstance());
result.rethrow();
getConduit(invocation, conduitAccess).setDefault(result.getReturnValue());
invocation.proceed();
}
}
private final class InvokeLoadOnParmeterConduit implements ComponentMethodAdvice
{
private final FieldAccess conduitAccess;
private InvokeLoadOnParmeterConduit(FieldAccess conduitAccess)
{
this.conduitAccess = conduitAccess;
}
public void advise(ComponentMethodInvocation invocation)
{
getConduit(invocation, conduitAccess).load();
invocation.proceed();
}
}
private final ComponentClassCache classCache;
private final BindingSource bindingSource;
private final ComponentDefaultProvider defaultProvider;
private final TypeCoercer typeCoercer;
private final PerthreadManager perThreadManager;
public ParameterWorker(ComponentClassCache classCache, BindingSource bindingSource,
ComponentDefaultProvider defaultProvider, TypeCoercer typeCoercer, PerthreadManager perThreadManager)
{
this.classCache = classCache;
this.bindingSource = bindingSource;
this.defaultProvider = defaultProvider;
this.typeCoercer = typeCoercer;
this.perThreadManager = perThreadManager;
}
public void transform(ClassTransformation transformation, MutableComponentModel model)
{
transformFields(transformation, model, true);
transformFields(transformation, model, false);
}
private void transformFields(ClassTransformation transformation, MutableComponentModel model, boolean principal)
{
for (TransformField field : matchParameterFields(transformation, principal))
{
convertFieldIntoParameter(transformation, model, field);
}
}
private List<TransformField> matchParameterFields(ClassTransformation transformation, final boolean principal)
{
Predicate<TransformField> predicate = new Predicate<TransformField>()
{
public boolean accept(TransformField field)
{
Parameter annotation = field.getAnnotation(Parameter.class);
return annotation != null && annotation.principal() == principal;
}
};
return transformation.matchFields(predicate);
}
private void convertFieldIntoParameter(ClassTransformation transformation, MutableComponentModel model,
TransformField field)
{
Parameter annotation = field.getAnnotation(Parameter.class);
String fieldType = field.getType();
String parameterName = getParameterName(field.getName(), annotation.name());
field.claim(annotation);
model.addParameter(parameterName, annotation.required(), annotation.allowNull(), annotation.defaultPrefix(),
annotation.cache());
ComponentValueProvider<ParameterConduit> provider = createParameterConduitProvider(parameterName, fieldType,
annotation);
TransformField conduitField = transformation.addIndirectInjectedField(ParameterConduit.class, parameterName
+ "$conduit", provider);
FieldAccess conduitAccess = conduitField.getAccess();
addCodeForParameterDefaultMethod(transformation, parameterName, conduitAccess);
field.replaceAccess(conduitField);
invokeLoadOnParameterConduitAtPageLoad(transformation, conduitAccess);
invokeResetOnParameterConduitAtPostRenderCleanup(transformation, conduitAccess);
}
private void invokeResetOnParameterConduitAtPostRenderCleanup(ClassTransformation transformation,
final FieldAccess conduitAccess)
{
ComponentMethodAdvice advice = new InvokeResetOnParameterConduit(conduitAccess);
addMethodAdvice(transformation, TransformConstants.POST_RENDER_CLEANUP_SIGNATURE, advice);
}
private void addMethodAdvice(ClassTransformation transformation, TransformMethodSignature methodSignature,
ComponentMethodAdvice advice)
{
transformation.getOrCreateMethod(methodSignature).addAdvice(advice);
}
private void invokeLoadOnParameterConduitAtPageLoad(ClassTransformation transformation, FieldAccess conduitAccess)
{
ComponentMethodAdvice pageLoadAdvice = new InvokeLoadOnParmeterConduit(conduitAccess);
addPageLoadAdvice(transformation, pageLoadAdvice);
}
@SuppressWarnings("all")
private ComponentValueProvider<ParameterConduit> createParameterConduitProvider(final String parameterName,
final String fieldTypeName, final Parameter annotation)
{
return new ComponentValueProvider<ParameterConduit>()
{
public ParameterConduit get(ComponentResources resources)
{
final InternalComponentResources icr = (InternalComponentResources) resources;
final String key = String.format("ParameterWorker:%s/%s", resources.getCompleteId(), parameterName);
final Class fieldType = classCache.forName(fieldTypeName);
// Rely on some code generation in the component to set the default binding from
// the field, or from a default method.
return new ParameterConduit()
{
// Default value for parameter, computed *once* at
// page load time.
private Object defaultValue = classCache.defaultValueForType(fieldTypeName);
private Binding parameterBinding;
boolean loaded = false;
private boolean invariant = false;
{
// Inform the ComponentResources about the parameter conduit, so it can be
// shared with mixins.
icr.setParameterConduit(parameterName, this);
}
private ParameterState getState()
{
ParameterState state = (ParameterState) perThreadManager.get(key);
if (state == null)
{
state = new ParameterState();
state.value = defaultValue;
perThreadManager.put(key, state);
}
return state;
}
private boolean isLoaded()
{
return loaded;
}
public void set(Object newValue)
{
ParameterState state = getState();
// Assignments before the page is loaded ultimately exist to set the
// default value for the field. Often this is from the (original)
// constructor method, which is converted to a real method as part of the transformation.
if (!loaded)
{
state.value = newValue;
defaultValue = newValue;
return;
}
// This will catch read-only or unbound parameters.
writeToBinding(newValue);
state.value = newValue;
// If caching is enabled for the parameter (the typical case) and the
// component is currently rendering, then the result
// can be cached in this ParameterConduit (until the component finishes
// rendering).
state.cached = annotation.cache() && icr.isRendering();
}
private Object readFromBinding()
{
Object result = null;
try
{
Object boundValue = parameterBinding.get();
result = typeCoercer.coerce(boundValue, fieldType);
}
catch (RuntimeException ex)
{
throw new TapestryException(String.format(
"Failure reading parameter '%s' of component %s: %s", parameterName,
icr.getCompleteId(), InternalUtils.toMessage(ex)), parameterBinding, ex);
}
if (result != null || annotation.allowNull())
return result;
throw new TapestryException(
String.format(
"Parameter '%s' of component %s is bound to null. This parameter is not allowed to be null.",
parameterName, icr.getCompleteId()), parameterBinding, null);
}
private void writeToBinding(Object newValue)
{
// An unbound parameter acts like a simple field
// with no side effects.
if (parameterBinding == null)
return;
try
{
Object coerced = typeCoercer.coerce(newValue, parameterBinding.getBindingType());
parameterBinding.set(coerced);
}
catch (RuntimeException ex)
{
throw new TapestryException(String.format(
"Failure writing parameter '%s' of component %s: %s", parameterName,
icr.getCompleteId(), InternalUtils.toMessage(ex)), icr, ex);
}
}
public void reset()
{
if (!invariant)
{
getState().reset(defaultValue);
}
}
public void load()
{
logger.debug(String.format("%s loading parameter %s", icr.getCompleteId(), parameterName));
// If it's bound at this point, that's because of an explicit binding
// in the template or @Component annotation.
if (!icr.isBound(parameterName))
{
logger.debug(String.format("%s parameter %s not yet bound", icr.getCompleteId(),
parameterName));
// Otherwise, construct a default binding, or use one provided from
// the component.
Binding binding = getDefaultBindingForParameter();
logger.debug(String.format("%s parameter %s bound to default %s", icr.getCompleteId(),
parameterName, binding));
if (binding != null)
icr.bindParameter(parameterName, binding);
}
parameterBinding = icr.getBinding(parameterName);
loaded = true;
invariant = parameterBinding != null && parameterBinding.isInvariant();
getState().value = defaultValue;
}
public boolean isBound()
{
return parameterBinding != null;
}
public Object get()
{
if (!isLoaded()) { return defaultValue; }
ParameterState state = getState();
if (state.cached || !isBound()) { return state.value; }
// Read the parameter's binding and cast it to the
// field's type.
Object result = readFromBinding();
// If the value is invariant, we can cache it until at least the end of the request (before
// 5.2, it would be cached forever in the pooled instance).
// Otherwise, we
// we may want to cache it for the remainder of the component render (if the
// component is currently rendering).
if (invariant || (annotation.cache() && icr.isRendering()))
{
state.value = result;
state.cached = true;
}
return result;
}
private Binding getDefaultBindingForParameter()
{
if (InternalUtils.isNonBlank(annotation.value()))
return bindingSource.newBinding("default " + parameterName, icr,
annotation.defaultPrefix(), annotation.value());
if (annotation.autoconnect())
return defaultProvider.defaultBinding(parameterName, icr);
// Return (if not null) the binding from the setDefault() method which is
// set via a default method on the component, or from the field's initial
// value.
return parameterBinding;
}
public void setDefault(Object value)
{
if (value == null)
return;
if (value instanceof Binding)
{
parameterBinding = (Binding) value;
return;
}
parameterBinding = new LiteralBinding(null, "default " + parameterName, value);
}
};
}
};
}
private ParameterConduit getConduit(ComponentMethodInvocation invocation, FieldAccess access)
{
return (ParameterConduit) access.read(invocation.getInstance());
}
private void addCodeForParameterDefaultMethod(ClassTransformation transformation, final String parameterName,
final FieldAccess conduitAccess)
{
final String methodName = "default" + parameterName;
Predicate<TransformMethod> predicate = new Predicate<TransformMethod>()
{
public boolean accept(TransformMethod method)
{
return method.getSignature().getParameterTypes().length == 0
&& method.getName().equalsIgnoreCase(methodName);
}
};
List<TransformMethod> matches = transformation.matchMethods(predicate);
// This will match exactly 0 or 1 (unless the user does something really silly)
// methods, and if it matches, we know the name of the method.
if (matches.isEmpty())
return;
TransformMethod defaultMethod = matches.get(0);
captureDefaultValueFromDefaultMethod(transformation, defaultMethod, conduitAccess);
}
private void captureDefaultValueFromDefaultMethod(ClassTransformation transformation,
TransformMethod defaultMethod, final FieldAccess conduitAccess)
{
final MethodAccess access = defaultMethod.getAccess();
ComponentMethodAdvice advice = new InvokeParameterDefaultMethod(conduitAccess, access);
addPageLoadAdvice(transformation, advice);
}
private void addPageLoadAdvice(ClassTransformation transformation, ComponentMethodAdvice advice)
{
addMethodAdvice(transformation, TransformConstants.CONTAINING_PAGE_DID_LOAD_SIGNATURE, advice);
}
private static String getParameterName(String fieldName, String annotatedName)
{
if (InternalUtils.isNonBlank(annotatedName))
return annotatedName;
return InternalUtils.stripMemberName(fieldName);
}
}