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}