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