    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.SymbolConstants;
    021import org.apache.tapestry5.annotations.Cached;
    022import org.apache.tapestry5.internal.TapestryInternalUtils;
    023import org.apache.tapestry5.ioc.annotations.Inject;
    024import org.apache.tapestry5.ioc.annotations.Symbol;
    025import org.apache.tapestry5.ioc.services.PerThreadValue;
    026import org.apache.tapestry5.ioc.services.PerthreadManager;
    027import org.apache.tapestry5.model.MutableComponentModel;
    028import org.apache.tapestry5.plastic.*;
    029import org.apache.tapestry5.plastic.PlasticUtils.FieldInfo;
    030import org.apache.tapestry5.runtime.PageLifecycleListener;
    031import org.apache.tapestry5.services.BindingSource;
    032import org.apache.tapestry5.services.TransformConstants;
    033import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    034import org.apache.tapestry5.services.transform.TransformationSupport;
    035
    036import java.util.HashSet;
    037import java.util.List;
    038import java.util.Set;
    039
    040/**
    041 * Caches method return values for methods annotated with {@link Cached}.
    042 */
    043@SuppressWarnings("all")
    044public class CachedWorker implements ComponentClassTransformWorker2
    045{
    046    private static final String FIELD_PREFIX = "cache$";
    047
    048    private final BindingSource bindingSource;
    049
    050    private final PerthreadManager perThreadManager;
    051
    052    private final PropertyValueProviderWorker propertyValueProviderWorker;
    053
    054    private final boolean multipleClassLoaders;
    055
    056    interface MethodResultCacheFactory
    057    {
    058        MethodResultCache create(Object instance);
    059    }
    060
    061
    062    private class SimpleMethodResultCache implements MethodResultCache
    063    {
    064        private boolean cached;
    065        private Object cachedValue;
    066
    067        public void set(Object cachedValue)
    068        {
    069            cached = true;
    070            this.cachedValue = cachedValue;
    071        }
    072
    073        public void reset()
    074        {
    075            cached = false;
    076            cachedValue = null;
    077        }
    078
    079        public boolean isCached()
    080        {
    081            return cached;
    082        }
    083
    084        public Object get()
    085        {
    086            return cachedValue;
    087        }
    088    }
    089
    090    /**
    091     * When there is no watch, all cached methods look the same.
    092     */
    093    private final MethodResultCacheFactory nonWatchFactory = new MethodResultCacheFactory()
    094    {
    095        public MethodResultCache create(Object instance)
    096        {
    097            return new SimpleMethodResultCache();
    098        }
    099    };
    100
    101    /**
    102     * Handles the watching of a binding (usually a property or property expression), invalidating the
    103     * cache early if the watched binding's value changes.
    104     */
    105    private class WatchedBindingMethodResultCache extends SimpleMethodResultCache
    106    {
    107        private final Binding binding;
    108
    109        private Object cachedBindingValue;
    110
    111        public WatchedBindingMethodResultCache(Binding binding)
    112        {
    113            this.binding = binding;
    114        }
    115
    116        @Override
    117        public boolean isCached()
    118        {
    119            Object currentBindingValue = binding.get();
    120
    121            if (!TapestryInternalUtils.isEqual(cachedBindingValue, currentBindingValue))
    122            {
    123                reset();
    124
    125                cachedBindingValue = currentBindingValue;
    126            }
    127
    128            return super.isCached();
    129        }
    130    }
    131
    132    public CachedWorker(BindingSource bindingSource, PerthreadManager perthreadManager,
    133            PropertyValueProviderWorker propertyValueProviderWorker,
    134            @Inject @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode,
    135            @Inject @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassloaders)
    136    {
    137        this.bindingSource = bindingSource;
    138        this.perThreadManager = perthreadManager;
    139        this.propertyValueProviderWorker = propertyValueProviderWorker;
    140        this.multipleClassLoaders = !productionMode && multipleClassloaders;
    141    }
    142
    143
    144    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    145    {
    146        List<PlasticMethod> methods = plasticClass.getMethodsWithAnnotation(Cached.class);
    147        Set<PlasticUtils.FieldInfo> fieldInfos = multipleClassLoaders ? new HashSet<>() : null;
    148
    149        for (PlasticMethod method : methods)
    150        {
    151            validateMethod(method);
    152
    153            adviseMethod(plasticClass, method, fieldInfos);
    154        }
    155
    156        if (multipleClassLoaders && !fieldInfos.isEmpty())
    157        {
    158            this.propertyValueProviderWorker.add(plasticClass, fieldInfos);
    159        }
    160    }
    161
    162    private void adviseMethod(PlasticClass plasticClass, PlasticMethod method, Set<FieldInfo> fieldInfos)
    163    {
    164        // Every instance of the class requires its own per-thread value. This handles the case of multiple
    165        // pages containing the component, or the same page containing the component multiple times.
    166
    167        PlasticField cacheField =
    168                plasticClass.introduceField(PerThreadValue.class, getFieldName(method));
    169
    170        cacheField.injectComputed(new ComputedValue<PerThreadValue>()
    171        {
    172            public PerThreadValue get(InstanceContext context)
    173            {
    174                // Each instance will get a new PerThreadValue
    175                return perThreadManager.createValue();
    176            }
    177        });
    178
    179        if (multipleClassLoaders)
    180        {
    181            fieldInfos.add(PlasticUtils.toFieldInfo(cacheField));
    182            cacheField.createAccessors(PropertyAccessType.READ_ONLY);
    183        }
    184
    185        Cached annotation = method.getAnnotation(Cached.class);
    186
    187        MethodResultCacheFactory factory = createFactory(plasticClass, annotation.watch(), method);
    188
    189        MethodAdvice advice = createAdvice(cacheField, factory);
    190
    191        method.addAdvice(advice);
    192    }
    193
    194    private String getFieldName(PlasticMethod method) {
    195        final StringBuilder builder = new StringBuilder(FIELD_PREFIX);
    196        builder.append(method.getDescription().methodName);
    197        if (multipleClassLoaders)
    198        {
    199            builder.append("_");
    200            builder.append(method.getPlasticClass().getClassName().replace('.', '_'));
    201        }
    202        return builder.toString();
    203    }
    204
    205
    206    private MethodAdvice createAdvice(PlasticField cacheField,
    207                                      final MethodResultCacheFactory factory)
    208    {
    209        final FieldHandle fieldHandle = cacheField.getHandle();
    210        final String fieldName = multipleClassLoaders ? cacheField.getName() : null;
    211
    212        return new MethodAdvice()
    213        {
    214            public void advise(MethodInvocation invocation)
    215            {
    216                MethodResultCache cache = getOrCreateCache(invocation);
    217
    218                if (cache.isCached())
    219                {
    220                    invocation.setReturnValue(cache.get());
    221                    return;
    222                }
    223
    224                invocation.proceed();
    225
    226                if(!invocation.didThrowCheckedException())
    227                {
    228                    cache.set(invocation.getReturnValue());
    229                }
    230            }
    231
    232            private MethodResultCache getOrCreateCache(MethodInvocation invocation)
    233            {
    234                Object instance = invocation.getInstance();
    235
    236                // The PerThreadValue is created in the instance constructor.
    237
    238                PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) (
    239                        multipleClassLoaders ?
    240                        PropertyValueProvider.get(instance, fieldName) :
    241                        fieldHandle.get(instance));
    242
    243                // But it will be empty when first created, or at the start of a new request.
    244                if (value.exists())
    245                {
    246                    return value.get();
    247                }
    248
    249                // Use the factory to create a MethodResultCache for the combination of instance, method, and thread.
    250
    251                return value.set(factory.create(instance));
    252            }
    253        };
    254    }
    255
    256
    257    private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch,
    258                                                   PlasticMethod method)
    259    {
    260        // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache
    261        // will suffice.
    262        if (watch.equals(""))
    263        {
    264            return nonWatchFactory;
    265        }
    266
    267        // Because of the watch, its necessary to create a factory for instances of this component and method.
    268
    269        final FieldHandle bindingFieldHandle = plasticClass.introduceField(Binding.class, "cache$watchBinding$" + method.getDescription().methodName).getHandle();
    270
    271
    272        // Each component instance will get its own Binding instance. That handles both different locales,
    273        // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized
    274        // until the page loads.
    275
    276        plasticClass.introduceInterface(PageLifecycleListener.class);
    277        plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice()
    278        {
    279            public void advise(MethodInvocation invocation)
    280            {
    281                ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class);
    282
    283                Binding binding = bindingSource.newBinding("@Cached watch", resources,
    284                        BindingConstants.PROP, watch);
    285
    286                bindingFieldHandle.set(invocation.getInstance(), binding);
    287
    288                invocation.proceed();
    289            }
    290        });
    291
    292        return new MethodResultCacheFactory()
    293        {
    294            public MethodResultCache create(Object instance)
    295            {
    296                Binding binding = (Binding) bindingFieldHandle.get(instance);
    297
    298                return new WatchedBindingMethodResultCache(binding);
    299            }
    300        };
    301    }
    302
    303    private void validateMethod(PlasticMethod method)
    304    {
    305        MethodDescription description = method.getDescription();
    306
    307        if (description.returnType.equals("void"))
    308            throw new IllegalArgumentException(String.format(
    309                    "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier()));
    310
    311        if (description.argumentTypes.length != 0)
    312            throw new IllegalArgumentException(String.format(
    313                    "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier()));
    314    }
    315}