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 }