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.json.JSONArray; 028import org.apache.tapestry5.json.JSONObject; 029import org.apache.tapestry5.model.ComponentModel; 030import org.apache.tapestry5.model.MutableComponentModel; 031import org.apache.tapestry5.plastic.*; 032import org.apache.tapestry5.plastic.PlasticUtils.FieldInfo; 033import org.apache.tapestry5.runtime.PageLifecycleListener; 034import org.apache.tapestry5.services.BindingSource; 035import org.apache.tapestry5.services.TransformConstants; 036import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 037import org.apache.tapestry5.services.transform.TransformationSupport; 038 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.Collection; 042import java.util.HashMap; 043import java.util.HashSet; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047import java.util.stream.Collectors; 048 049/** 050 * Caches method return values for methods annotated with {@link Cached}. 051 */ 052@SuppressWarnings("all") 053public class CachedWorker implements ComponentClassTransformWorker2 054{ 055 private static final String WATCH_BINDING_PREFIX = "cache$watchBinding$"; 056 057 private static final String FIELD_PREFIX = "cache$"; 058 059 private static final String META_PROPERTY = "cachedWorker"; 060 061 private static final String MODIFIERS = "modifiers"; 062 063 private static final String RETURN_TYPE = "returnType"; 064 065 private static final String NAME = "name"; 066 067 private static final String GENERIC_SIGNATURE = "genericSignature"; 068 069 private static final String ARGUMENT_TYPES = "argumentTypes"; 070 071 private static final String CHECKED_EXCEPTION_TYPES = "checkedExceptionTypes"; 072 073 private static final String WATCH = "watch"; 074 075 private final BindingSource bindingSource; 076 077 private final PerthreadManager perThreadManager; 078 079 private final PropertyValueProviderWorker propertyValueProviderWorker; 080 081 private final boolean multipleClassLoaders; 082 083 interface MethodResultCacheFactory 084 { 085 MethodResultCache create(Object instance); 086 } 087 088 089 private class SimpleMethodResultCache implements MethodResultCache 090 { 091 private boolean cached; 092 private Object cachedValue; 093 094 public void set(Object cachedValue) 095 { 096 cached = true; 097 this.cachedValue = cachedValue; 098 } 099 100 public void reset() 101 { 102 cached = false; 103 cachedValue = null; 104 } 105 106 public boolean isCached() 107 { 108 return cached; 109 } 110 111 public Object get() 112 { 113 return cachedValue; 114 } 115 } 116 117 /** 118 * When there is no watch, all cached methods look the same. 119 */ 120 private final MethodResultCacheFactory nonWatchFactory = new MethodResultCacheFactory() 121 { 122 public MethodResultCache create(Object instance) 123 { 124 return new SimpleMethodResultCache(); 125 } 126 }; 127 128 /** 129 * Handles the watching of a binding (usually a property or property expression), invalidating the 130 * cache early if the watched binding's value changes. 131 */ 132 private class WatchedBindingMethodResultCache extends SimpleMethodResultCache 133 { 134 private final Binding binding; 135 136 private Object cachedBindingValue; 137 138 public WatchedBindingMethodResultCache(Binding binding) 139 { 140 this.binding = binding; 141 } 142 143 @Override 144 public boolean isCached() 145 { 146 Object currentBindingValue = binding.get(); 147 148 if (!TapestryInternalUtils.isEqual(cachedBindingValue, currentBindingValue)) 149 { 150 reset(); 151 152 cachedBindingValue = currentBindingValue; 153 } 154 155 return super.isCached(); 156 } 157 } 158 159 public CachedWorker(BindingSource bindingSource, PerthreadManager perthreadManager, 160 PropertyValueProviderWorker propertyValueProviderWorker, 161 @Inject @Symbol(SymbolConstants.PRODUCTION_MODE) boolean productionMode, 162 @Inject @Symbol(SymbolConstants.MULTIPLE_CLASSLOADERS) boolean multipleClassloaders) 163 { 164 this.bindingSource = bindingSource; 165 this.perThreadManager = perthreadManager; 166 this.propertyValueProviderWorker = propertyValueProviderWorker; 167 this.multipleClassLoaders = !productionMode && multipleClassloaders; 168 } 169 170 171 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) 172 { 173 final List<PlasticMethod> methods = plasticClass.getMethodsWithAnnotation(Cached.class); 174 final Set<PlasticUtils.FieldInfo> fieldInfos = multipleClassLoaders ? new HashSet<>() : null; 175 final Map<String, String> extraMethodCachedWatchMap = multipleClassLoaders ? new HashMap<>() : null; 176 177 if (multipleClassLoaders) 178 { 179 180 // Store @Cache-annotated methods information so subclasses can 181 // know about them. 182 183 model.setMeta(META_PROPERTY, toJSONArray(methods).toCompactString()); 184 185 // Use the information from superclasses 186 187 ComponentModel parentModel = model.getParentModel(); 188 Set<PlasticMethod> extraMethods = new HashSet<>(); 189 while (parentModel != null) 190 { 191 extraMethods.addAll( 192 toPlasticMethodList( 193 parentModel.getMeta(META_PROPERTY), plasticClass, extraMethodCachedWatchMap)); 194 parentModel = parentModel.getParentModel(); 195 } 196 197 methods.addAll(extraMethods); 198 199 } 200 201 for (PlasticMethod method : methods) 202 { 203 validateMethod(method); 204 205 adviseMethod(plasticClass, method, fieldInfos, model, extraMethodCachedWatchMap); 206 } 207 208 if (multipleClassLoaders && !fieldInfos.isEmpty()) 209 { 210 this.propertyValueProviderWorker.add(plasticClass, fieldInfos); 211 } 212 } 213 214 private Collection<PlasticMethod> toPlasticMethodList(String meta, PlasticClass plasticClass, 215 Map<String, String> extraMethodCachedWatchMap) 216 { 217 final JSONArray array = new JSONArray(meta); 218 List<PlasticMethod> methods = new ArrayList<>(array.size()); 219 for (int i = 0; i < array.size(); i++) 220 { 221 final JSONObject jsonObject = array.getJSONObject(i); 222 methods.add(toPlasticMethod(jsonObject, plasticClass, extraMethodCachedWatchMap)); 223 } 224 return methods; 225 } 226 227 228 private static PlasticMethod toPlasticMethod(JSONObject jsonObject, PlasticClass plasticClass, 229 Map<String, String> extraMethodCachedWatchMap) 230 { 231 final int modifiers = jsonObject.getInt(MODIFIERS); 232 final String returnType = jsonObject.getString(RETURN_TYPE); 233 final String methodName = jsonObject.getString(NAME); 234 final String genericSignature = jsonObject.getStringOrDefault(GENERIC_SIGNATURE, null); 235 final JSONArray argumentTypesArray = jsonObject.getJSONArray(ARGUMENT_TYPES); 236 final String[] argumentTypes = argumentTypesArray.stream() 237 .collect(Collectors.toList()).toArray(new String[argumentTypesArray.size()]); 238 final JSONArray checkedExceptionTypesArray = jsonObject.getJSONArray(CHECKED_EXCEPTION_TYPES); 239 final String[] checkedExceptionTypes = checkedExceptionTypesArray.stream() 240 .collect(Collectors.toList()).toArray(new String[checkedExceptionTypesArray.size()]); 241 242 if (!extraMethodCachedWatchMap.containsKey(methodName)) 243 { 244 extraMethodCachedWatchMap.put(methodName, jsonObject.getString(WATCH)); 245 } 246 247 return plasticClass.introduceMethod(new MethodDescription( 248 modifiers, returnType, methodName, argumentTypes, 249 genericSignature, checkedExceptionTypes)); 250 } 251 252 private static JSONArray toJSONArray(List<PlasticMethod> methods) 253 { 254 final JSONArray array = new JSONArray(); 255 for (PlasticMethod method : methods) 256 { 257 array.add(toJSONObject(method)); 258 } 259 return array; 260 } 261 262 private static JSONObject toJSONObject(PlasticMethod method) 263 { 264 final MethodDescription description = method.getDescription(); 265 return new JSONObject( 266 MODIFIERS, description.modifiers, 267 RETURN_TYPE, description.returnType, 268 NAME, description.methodName, 269 GENERIC_SIGNATURE, description.genericSignature, 270 ARGUMENT_TYPES, new JSONArray(description.argumentTypes), 271 CHECKED_EXCEPTION_TYPES, new JSONArray(description.checkedExceptionTypes), 272 WATCH, method.getAnnotation(Cached.class).watch()); 273 } 274 275 private void adviseMethod(PlasticClass plasticClass, PlasticMethod method, Set<FieldInfo> fieldInfos, 276 MutableComponentModel model, Map<String, String> extraMethodCachedWatchMap) 277 { 278 // Every instance of the class requires its own per-thread value. This handles the case of multiple 279 // pages containing the component, or the same page containing the component multiple times. 280 281 PlasticField cacheField = 282 plasticClass.introduceField(PerThreadValue.class, getFieldName(method)); 283 284 cacheField.injectComputed(new ComputedValue<PerThreadValue>() 285 { 286 public PerThreadValue get(InstanceContext context) 287 { 288 // Each instance will get a new PerThreadValue 289 return perThreadManager.createValue(); 290 } 291 }); 292 293 if (multipleClassLoaders) 294 { 295 fieldInfos.add(PlasticUtils.toFieldInfo(cacheField)); 296 cacheField.createAccessors(PropertyAccessType.READ_ONLY); 297 } 298 299 Cached annotation = method.getAnnotation(Cached.class); 300 301 final String expression = annotation != null ? 302 annotation.watch() : 303 extraMethodCachedWatchMap.get(method.getDescription().methodName); 304 MethodResultCacheFactory factory = createFactory(plasticClass, expression, method, fieldInfos, model); 305 306 MethodAdvice advice = createAdvice(cacheField, factory); 307 308 method.addAdvice(advice); 309 } 310 311 private String getFieldName(PlasticMethod method) { 312 return getFieldName(method, FIELD_PREFIX); 313 } 314 315 private String getFieldName(PlasticMethod method, String prefix) 316 { 317 final String methodName = method.getDescription().methodName; 318 final String className = method.getPlasticClass().getClassName(); 319 return getFieldName(prefix, methodName, className); 320 } 321 322 private String getFieldName(String prefix, final String methodName, final String className) 323 { 324 final StringBuilder builder = new StringBuilder(prefix); 325 builder.append(methodName); 326 if (multipleClassLoaders) 327 { 328 builder.append("_"); 329 builder.append(className.replace('.', '_')); 330 } 331 return builder.toString(); 332 } 333 334 private MethodAdvice createAdvice(PlasticField cacheField, 335 final MethodResultCacheFactory factory) 336 { 337 final FieldHandle fieldHandle = cacheField.getHandle(); 338 final String fieldName = multipleClassLoaders ? cacheField.getName() : null; 339 340 return new MethodAdvice() 341 { 342 public void advise(MethodInvocation invocation) 343 { 344 MethodResultCache cache = getOrCreateCache(invocation); 345 346 if (cache.isCached()) 347 { 348 invocation.setReturnValue(cache.get()); 349 return; 350 } 351 352 invocation.proceed(); 353 354 if(!invocation.didThrowCheckedException()) 355 { 356 cache.set(invocation.getReturnValue()); 357 } 358 } 359 360 private MethodResultCache getOrCreateCache(MethodInvocation invocation) 361 { 362 Object instance = invocation.getInstance(); 363 364 // The PerThreadValue is created in the instance constructor. 365 366 PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) ( 367 multipleClassLoaders ? 368 PropertyValueProvider.get(instance, fieldName) : 369 fieldHandle.get(instance)); 370 371 // But it will be empty when first created, or at the start of a new request. 372 if (value.exists()) 373 { 374 return value.get(); 375 } 376 377 // Use the factory to create a MethodResultCache for the combination of instance, method, and thread. 378 379 return value.set(factory.create(instance)); 380 } 381 }; 382 } 383 384 385 private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch, 386 PlasticMethod method, Set<FieldInfo> fieldInfos, 387 MutableComponentModel model) 388 { 389 // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache 390 // will suffice. 391 if (watch.equals("")) 392 { 393 return nonWatchFactory; 394 } 395 396 // Because of the watch, its necessary to create a factory for instances of this component and method. 397 398 final String bindingFieldName = WATCH_BINDING_PREFIX + method.getDescription().methodName; 399 final PlasticField bindingField = plasticClass.introduceField(Binding.class, bindingFieldName); 400 final FieldHandle bindingFieldHandle = bindingField.getHandle(); 401 402 if (multipleClassLoaders) 403 { 404 fieldInfos.add(PlasticUtils.toFieldInfo(bindingField)); 405 try 406 { 407 bindingField.createAccessors(PropertyAccessType.READ_WRITE); 408 } 409 catch (IllegalArgumentException e) 410 { 411 // Method already implemented in superclass, so, given we only 412 // care the method exists, we ignore this exception 413 } 414 } 415 416 // Each component instance will get its own Binding instance. That handles both different locales, 417 // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized 418 // until the page loads. 419 420 plasticClass.introduceInterface(PageLifecycleListener.class); 421 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice() 422 { 423 public void advise(MethodInvocation invocation) 424 { 425 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class); 426 427 Binding binding = bindingSource.newBinding("@Cached watch", resources, 428 BindingConstants.PROP, watch); 429 430 final Object instance = invocation.getInstance(); 431 if (multipleClassLoaders) 432 { 433 PropertyValueProvider.set(instance, bindingFieldName, binding); 434 } 435 else 436 { 437 bindingFieldHandle.set(instance, binding); 438 } 439 440 invocation.proceed(); 441 } 442 }); 443 444 return new MethodResultCacheFactory() 445 { 446 public MethodResultCache create(Object instance) 447 { 448 Binding binding = (Binding) ( 449 multipleClassLoaders ? 450 PropertyValueProvider.get(instance, bindingFieldName) : 451 bindingFieldHandle.get(instance)); 452 453 return new WatchedBindingMethodResultCache(binding); 454 } 455 456 private Object getCacheBinding(final String methodName, String bindingFieldName, Object instance, ComponentModel model) 457 { 458 Object value = PropertyValueProvider.get(instance, bindingFieldName); 459 while (value == null && model.getParentModel() != null) 460 { 461 model = model.getParentModel(); 462 bindingFieldName = getFieldName(WATCH_BINDING_PREFIX, 463 methodName, model.getComponentClassName()); 464 value = PropertyValueProvider.get(instance, bindingFieldName); 465 } 466 return value; 467 } 468 }; 469 } 470 471 private void validateMethod(PlasticMethod method) 472 { 473 MethodDescription description = method.getDescription(); 474 475 if (description.returnType.equals("void")) 476 throw new IllegalArgumentException(String.format( 477 "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier())); 478 479 if (description.argumentTypes.length != 0) 480 throw new IllegalArgumentException(String.format( 481 "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier())); 482 } 483}