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}