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}