    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
    015    package org.apache.tapestry5.internal.transform;
    016
    017    import org.apache.tapestry5.Binding;
    018    import org.apache.tapestry5.BindingConstants;
    019    import org.apache.tapestry5.ComponentResources;
    020    import org.apache.tapestry5.annotations.Cached;
    021    import org.apache.tapestry5.internal.TapestryInternalUtils;
    022    import org.apache.tapestry5.ioc.services.PerThreadValue;
    023    import org.apache.tapestry5.ioc.services.PerthreadManager;
    024    import org.apache.tapestry5.model.MutableComponentModel;
    025    import org.apache.tapestry5.plastic.*;
    026    import org.apache.tapestry5.runtime.PageLifecycleListener;
    027    import org.apache.tapestry5.services.BindingSource;
    028    import org.apache.tapestry5.services.TransformConstants;
    029    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    030    import org.apache.tapestry5.services.transform.TransformationSupport;
    031
    032    import java.util.List;
    033
    034    /**
    035     * Caches method return values for methods annotated with {@link Cached}.
    036     */
    037    @SuppressWarnings("all")
    038    public 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                    invocation.rethrow();
    186
    187                    cache.set(invocation.getReturnValue());
    188                }
    189
    190                private MethodResultCache getOrCreateCache(MethodInvocation invocation)
    191                {
    192                    Object instance = invocation.getInstance();
    193
    194                    // The PerThreadValue is created in the instance constructor.
    195
    196                    PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) fieldHandle
    197                            .get(instance);
    198
    199                    // But it will be empty when first created, or at the start of a new request.
    200                    if (value.exists())
    201                    {
    202                        return value.get();
    203                    }
    204
    205                    // Use the factory to create a MethodResultCache for the combination of instance, method, and thread.
    206
    207                    return value.set(factory.create(instance));
    208                }
    209            };
    210        }
    211
    212
    213        private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch,
    214                                                       PlasticMethod method)
    215        {
    216            // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache
    217            // will suffice.
    218            if (watch.equals(""))
    219            {
    220                return nonWatchFactory;
    221            }
    222
    223            // Because of the watch, its necessary to create a factory for instances of this component and method.
    224
    225            final FieldHandle bindingFieldHandle = plasticClass.introduceField(Binding.class, "cache$watchBinding$" + method.getDescription().methodName).getHandle();
    226
    227
    228            // Each component instance will get its own Binding instance. That handles both different locales,
    229            // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized
    230            // until the page loads.
    231
    232            plasticClass.introduceInterface(PageLifecycleListener.class);
    233            plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice()
    234            {
    235                public void advise(MethodInvocation invocation)
    236                {
    237                    ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class);
    238
    239                    Binding binding = bindingSource.newBinding("@Cached watch", resources,
    240                            BindingConstants.PROP, watch);
    241
    242                    bindingFieldHandle.set(invocation.getInstance(), binding);
    243
    244                    invocation.proceed();
    245                }
    246            });
    247
    248            return new MethodResultCacheFactory()
    249            {
    250                public MethodResultCache create(Object instance)
    251                {
    252                    Binding binding = (Binding) bindingFieldHandle.get(instance);
    253
    254                    return new WatchedBindingMethodResultCache(binding);
    255                }
    256            };
    257        }
    258
    259        private void validateMethod(PlasticMethod method)
    260        {
    261            MethodDescription description = method.getDescription();
    262
    263            if (description.returnType.equals("void"))
    264                throw new IllegalArgumentException(String.format(
    265                        "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier()));
    266
    267            if (description.argumentTypes.length != 0)
    268                throw new IllegalArgumentException(String.format(
    269                        "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier()));
    270        }
    271    }