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    }