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 }