    001// Copyright 2008, 2010, 2011 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007// http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.internal.transform;
    016
    017import org.apache.tapestry5.Binding;
    018import org.apache.tapestry5.BindingConstants;
    019import org.apache.tapestry5.ComponentResources;
    020import org.apache.tapestry5.annotations.Cached;
    021import org.apache.tapestry5.internal.TapestryInternalUtils;
    022import org.apache.tapestry5.ioc.services.PerThreadValue;
    023import org.apache.tapestry5.ioc.services.PerthreadManager;
    024import org.apache.tapestry5.model.MutableComponentModel;
    025import org.apache.tapestry5.plastic.*;
    026import org.apache.tapestry5.runtime.PageLifecycleListener;
    027import org.apache.tapestry5.services.BindingSource;
    028import org.apache.tapestry5.services.TransformConstants;
    029import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    030import org.apache.tapestry5.services.transform.TransformationSupport;
    031
    032import java.util.List;
    033
    034/**
    035 * Caches method return values for methods annotated with {@link Cached}.
    036 */
    037@SuppressWarnings("all")
    038public class CachedWorker implements ComponentClassTransformWorker2
    039{
    040    private final BindingSource bindingSource;
    041
    042    private final PerthreadManager perThreadManager;
    043
    044    interface MethodResultCacheFactory
    045    {
    046        MethodResultCache create(Object instance);
    047    }
    048
    049
    050    private class SimpleMethodResultCache implements MethodResultCache
    051    {
    052        private boolean cached;
    053        private Object cachedValue;
    054
    055        public void set(Object cachedValue)
    056        {
    057            cached = true;
    058            this.cachedValue = cachedValue;
    059        }
    060
    061        public void reset()
    062        {
    063            cached = false;
    064            cachedValue = null;
    065        }
    066
    067        public boolean isCached()
    068        {
    069            return cached;
    070        }
    071
    072        public Object get()
    073        {
    074            return cachedValue;
    075        }
    076    }
    077
    078    /**
    079     * When there is no watch, all cached methods look the same.
    080     */
    081    private final MethodResultCacheFactory nonWatchFactory = new MethodResultCacheFactory()
    082    {
    083        public MethodResultCache create(Object instance)
    084        {
    085            return new SimpleMethodResultCache();
    086        }
    087    };
    088
    089    /**
    090     * Handles the watching of a binding (usually a property or property expression), invalidating the
    091     * cache early if the watched binding's value changes.
    092     */
    093    private class WatchedBindingMethodResultCache extends SimpleMethodResultCache
    094    {
    095        private final Binding binding;
    096
    097        private Object cachedBindingValue;
    098
    099        public WatchedBindingMethodResultCache(Binding binding)
    100        {
    101            this.binding = binding;
    102        }
    103
    104        @Override
    105        public boolean isCached()
    106        {
    107            Object currentBindingValue = binding.get();
    108
    109            if (!TapestryInternalUtils.isEqual(cachedBindingValue, currentBindingValue))
    110            {
    111                reset();
    112
    113                cachedBindingValue = currentBindingValue;
    114            }
    115
    116            return super.isCached();
    117        }
    118    }
    119
    120    public CachedWorker(BindingSource bindingSource, PerthreadManager perthreadManager)
    121    {
    122        this.bindingSource = bindingSource;
    123        this.perThreadManager = perthreadManager;
    124    }
    125
    126
    127    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    128    {
    129        List<PlasticMethod> methods = plasticClass.getMethodsWithAnnotation(Cached.class);
    130
    131        for (PlasticMethod method : methods)
    132        {
    133            validateMethod(method);
    134
    135            adviseMethod(plasticClass, method);
    136        }
    137    }
    138
    139    private void adviseMethod(PlasticClass plasticClass, PlasticMethod method)
    140    {
    141        // Every instance of the clas srequires its own per-thread value. This handles the case of multiple
    142        // pages containing the component, or the same page containing the component multiple times.
    143
    144        PlasticField cacheField =
    145                plasticClass.introduceField(PerThreadValue.class, "cache$" + method.getDescription().methodName);
    146
    147        cacheField.injectComputed(new ComputedValue<PerThreadValue>()
    148        {
    149            public PerThreadValue get(InstanceContext context)
    150            {
    151                // Each instance will get a new PerThreadValue
    152                return perThreadManager.createValue();
    153            }
    154        });
    155
    156        Cached annotation = method.getAnnotation(Cached.class);
    157
    158        MethodResultCacheFactory factory = createFactory(plasticClass, annotation.watch(), method);
    159
    160        MethodAdvice advice = createAdvice(cacheField, factory);
    161
    162        method.addAdvice(advice);
    163    }
    164
    165
    166    private MethodAdvice createAdvice(PlasticField cacheField,
    167                                      final MethodResultCacheFactory factory)
    168    {
    169        final FieldHandle fieldHandle = cacheField.getHandle();
    170
    171        return new MethodAdvice()
    172        {
    173            public void advise(MethodInvocation invocation)
    174            {
    175                MethodResultCache cache = getOrCreateCache(invocation);
    176
    177                if (cache.isCached())
    178                {
    179                    invocation.setReturnValue(cache.get());
    180                    return;
    181                }
    182
    183                invocation.proceed();
    184
    185                if(!invocation.didThrowCheckedException())
    186                {
    187                    cache.set(invocation.getReturnValue());
    188                }
    189            }
    190
    191            private MethodResultCache getOrCreateCache(MethodInvocation invocation)
    192            {
    193                Object instance = invocation.getInstance();
    194
    195                // The PerThreadValue is created in the instance constructor.
    196
    197                PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) fieldHandle
    198                        .get(instance);
    199
    200                // But it will be empty when first created, or at the start of a new request.
    201                if (value.exists())
    202                {
    203                    return value.get();
    204                }
    205
    206                // Use the factory to create a MethodResultCache for the combination of instance, method, and thread.
    207
    208                return value.set(factory.create(instance));
    209            }
    210        };
    211    }
    212
    213
    214    private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch,
    215                                                   PlasticMethod method)
    216    {
    217        // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache
    218        // will suffice.
    219        if (watch.equals(""))
    220        {
    221            return nonWatchFactory;
    222        }
    223
    224        // Because of the watch, its necessary to create a factory for instances of this component and method.
    225
    226        final FieldHandle bindingFieldHandle = plasticClass.introduceField(Binding.class, "cache$watchBinding$" + method.getDescription().methodName).getHandle();
    227
    228
    229        // Each component instance will get its own Binding instance. That handles both different locales,
    230        // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized
    231        // until the page loads.
    232
    233        plasticClass.introduceInterface(PageLifecycleListener.class);
    234        plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice()
    235        {
    236            public void advise(MethodInvocation invocation)
    237            {
    238                ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class);
    239
    240                Binding binding = bindingSource.newBinding("@Cached watch", resources,
    241                        BindingConstants.PROP, watch);
    242
    243                bindingFieldHandle.set(invocation.getInstance(), binding);
    244
    245                invocation.proceed();
    246            }
    247        });
    248
    249        return new MethodResultCacheFactory()
    250        {
    251            public MethodResultCache create(Object instance)
    252            {
    253                Binding binding = (Binding) bindingFieldHandle.get(instance);
    254
    255                return new WatchedBindingMethodResultCache(binding);
    256            }
    257        };
    258    }
    259
    260    private void validateMethod(PlasticMethod method)
    261    {
    262        MethodDescription description = method.getDescription();
    263
    264        if (description.returnType.equals("void"))
    265            throw new IllegalArgumentException(String.format(
    266                    "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier()));
    267
    268        if (description.argumentTypes.length != 0)
    269            throw new IllegalArgumentException(String.format(
    270                    "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier()));
    271    }
    272}