blob: a0620c91e8770c3a1690c297163749596633264f [file] [log] [blame]
// Copyright 2008, 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.lang.reflect.Modifier;
import java.util.List;
import org.apache.tapestry5.Binding;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.ioc.services.PerThreadValue;
import org.apache.tapestry5.ioc.services.PerthreadManager;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.*;
/**
* Caches method return values for methods annotated with {@link Cached}.
*/
@SuppressWarnings("all")
public class CachedWorker implements ComponentClassTransformWorker
{
private final BindingSource bindingSource;
private final PerthreadManager perThreadManager;
interface MethodResultCacheFactory
{
MethodResultCache create(Component instance);
}
/**
* Handles the watching of a binding (usually a property or property expression), invalidating the
* cache early if the watched binding's value changes.
*/
private class SimpleMethodResultCache implements MethodResultCache
{
private boolean cached;
private Object cachedValue;
public void set(Object cachedValue)
{
cached = true;
this.cachedValue = cachedValue;
}
public void reset()
{
cached = false;
cachedValue = null;
}
public boolean isCached()
{
return cached;
}
public Object get()
{
return cachedValue;
}
}
private class WatchedBindingMethodResultCache extends SimpleMethodResultCache
{
private final Binding binding;
private Object cachedBindingValue;
public WatchedBindingMethodResultCache(Binding binding)
{
this.binding = binding;
}
@Override
public boolean isCached()
{
Object currentBindingValue = binding.get();
if (!TapestryInternalUtils.isEqual(cachedBindingValue, currentBindingValue))
{
reset();
cachedBindingValue = currentBindingValue;
}
return super.isCached();
}
}
public CachedWorker(BindingSource bindingSource, PerthreadManager perthreadManager)
{
this.bindingSource = bindingSource;
this.perThreadManager = perthreadManager;
}
public void transform(ClassTransformation transformation, MutableComponentModel model)
{
List<TransformMethod> methods = transformation.matchMethodsWithAnnotation(Cached.class);
for (TransformMethod method : methods)
{
validateMethod(method);
adviseMethod(transformation, method);
}
}
private void adviseMethod(ClassTransformation transformation, TransformMethod method)
{
// The key needs to reflect not just the method name, but also the containing
// page and component (otherwise, there would be unwanted sharing of cache
// between different instances of the same component within or across pages). This
// name can't be calculated until page instantiation time.
FieldAccess fieldAccess = createPerThreadValueField(transformation, method);
Cached annotation = method.getAnnotation(Cached.class);
MethodResultCacheFactory factory = createFactory(transformation, annotation.watch(), method);
ComponentMethodAdvice advice = createAdvice(fieldAccess, factory);
method.addAdvice(advice);
}
private FieldAccess createPerThreadValueField(ClassTransformation transformation, TransformMethod method)
{
TransformField field = transformation.createField(Modifier.PROTECTED, PerThreadValue.class.getName(),
"perThreadMethodCache$" + method.getName());
// Each instance of the component will get a new PerThreadValue.
field.injectIndirect(new ComponentValueProvider<PerThreadValue<MethodResultCache>>()
{
public PerThreadValue<MethodResultCache> get(ComponentResources resources)
{
return perThreadManager.createValue();
}
});
return field.getAccess();
}
private ComponentMethodAdvice createAdvice(final FieldAccess perThreadValueAccess,
final MethodResultCacheFactory factory)
{
return new ComponentMethodAdvice()
{
public void advise(ComponentMethodInvocation invocation)
{
MethodResultCache cache = getOrCreateCache(invocation);
if (cache.isCached())
{
invocation.overrideResult(cache.get());
return;
}
invocation.proceed();
invocation.rethrow();
cache.set(invocation.getResult());
}
private MethodResultCache getOrCreateCache(ComponentMethodInvocation invocation)
{
Component instance = invocation.getInstance();
PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) perThreadValueAccess
.read(instance);
if (value.exists())
return value.get();
return value.set(factory.create(instance));
}
};
}
private MethodResultCacheFactory createFactory(ClassTransformation transformation, final String watch,
TransformMethod method)
{
if (watch.equals(""))
return new MethodResultCacheFactory()
{
public MethodResultCache create(Component instance)
{
return new SimpleMethodResultCache();
}
};
// Each component instance will get its own Binding instance. That handles both different locales,
// and reuse of a component (with a cached method) within a page or across pages.
TransformField bindingField = transformation.createField(Modifier.PROTECTED, Binding.class.getName(),
"cache$watchBinding$" + method.getName());
final FieldAccess bindingAccess = bindingField.getAccess();
transformation.getOrCreateMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_SIGNATURE).addAdvice(
new ComponentMethodAdvice()
{
public void advise(ComponentMethodInvocation invocation)
{
Binding binding = bindingSource.newBinding("@Cached watch", invocation.getComponentResources(),
BindingConstants.PROP, watch);
bindingAccess.write(invocation.getInstance(), binding);
invocation.proceed();
}
});
return new MethodResultCacheFactory()
{
public MethodResultCache create(Component instance)
{
Binding binding = (Binding) bindingAccess.read(instance);
return new WatchedBindingMethodResultCache(binding);
}
};
}
private void validateMethod(TransformMethod method)
{
TransformMethodSignature signature = method.getSignature();
if (signature.getReturnType().equals("void"))
throw new IllegalArgumentException(String.format(
"Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier()));
if (signature.getParameterTypes().length != 0)
throw new IllegalArgumentException(String.format(
"Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier()));
}
}