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 278 // TAP5-2813 279 final String genericSignature = description.genericSignature != null ? 280 description.genericSignature.replaceAll("<[^>]+>", "") : null; 281 282 283 return new JSONObject( 284 MODIFIERS, description.modifiers, 285 RETURN_TYPE, description.returnType, 286 NAME, description.methodName, 287 GENERIC_SIGNATURE, genericSignature, 288 ARGUMENT_TYPES, new JSONArray(description.argumentTypes), 289 CHECKED_EXCEPTION_TYPES, new JSONArray(description.checkedExceptionTypes), 290 WATCH, method.getAnnotation(Cached.class).watch()); 291 } 292 293 private void adviseMethod(PlasticClass plasticClass, PlasticMethod method, Set<FieldInfo> fieldInfos, 294 MutableComponentModel model, Map<String, String> extraMethodCachedWatchMap) 295 { 296 // Every instance of the class requires its own per-thread value. This handles the case of multiple 297 // pages containing the component, or the same page containing the component multiple times. 298 299 PlasticField cacheField = 300 plasticClass.introduceField(PerThreadValue.class, getFieldName(method)); 301 302 cacheField.injectComputed(new ComputedValue<PerThreadValue>() 303 { 304 public PerThreadValue get(InstanceContext context) 305 { 306 // Each instance will get a new PerThreadValue 307 return perThreadManager.createValue(); 308 } 309 }); 310 311 if (multipleClassLoaders) 312 { 313 fieldInfos.add(PlasticUtils.toFieldInfo(cacheField)); 314 cacheField.createAccessors(PropertyAccessType.READ_ONLY); 315 } 316 317 Cached annotation = method.getAnnotation(Cached.class); 318 319 final String expression = annotation != null ? 320 annotation.watch() : 321 extraMethodCachedWatchMap.get(method.getDescription().methodName); 322 MethodResultCacheFactory factory = createFactory(plasticClass, expression, method, fieldInfos, model); 323 324 MethodAdvice advice = createAdvice(cacheField, factory); 325 326 method.addAdvice(advice); 327 } 328 329 private String getFieldName(PlasticMethod method) { 330 return getFieldName(method, FIELD_PREFIX); 331 } 332 333 private String getFieldName(PlasticMethod method, String prefix) 334 { 335 final String methodName = method.getDescription().methodName; 336 final String className = method.getPlasticClass().getClassName(); 337 return getFieldName(prefix, methodName, className); 338 } 339 340 private String getFieldName(String prefix, final String methodName, final String className) 341 { 342 final StringBuilder builder = new StringBuilder(prefix); 343 builder.append(methodName); 344 if (multipleClassLoaders) 345 { 346 builder.append("_"); 347 builder.append(className.replace('.', '_')); 348 } 349 return builder.toString(); 350 } 351 352 private MethodAdvice createAdvice(PlasticField cacheField, 353 final MethodResultCacheFactory factory) 354 { 355 final FieldHandle fieldHandle = cacheField.getHandle(); 356 final String fieldName = multipleClassLoaders ? cacheField.getName() : null; 357 358 return new MethodAdvice() 359 { 360 public void advise(MethodInvocation invocation) 361 { 362 MethodResultCache cache = getOrCreateCache(invocation); 363 364 if (cache.isCached()) 365 { 366 invocation.setReturnValue(cache.get()); 367 return; 368 } 369 370 invocation.proceed(); 371 372 if(!invocation.didThrowCheckedException()) 373 { 374 cache.set(invocation.getReturnValue()); 375 } 376 } 377 378 private MethodResultCache getOrCreateCache(MethodInvocation invocation) 379 { 380 Object instance = invocation.getInstance(); 381 382 // The PerThreadValue is created in the instance constructor. 383 384 PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) ( 385 multipleClassLoaders ? 386 PropertyValueProvider.get(instance, fieldName) : 387 fieldHandle.get(instance)); 388 389 // But it will be empty when first created, or at the start of a new request. 390 if (value.exists()) 391 { 392 return value.get(); 393 } 394 395 // Use the factory to create a MethodResultCache for the combination of instance, method, and thread. 396 397 return value.set(factory.create(instance)); 398 } 399 }; 400 } 401 402 403 private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch, 404 PlasticMethod method, Set<FieldInfo> fieldInfos, 405 MutableComponentModel model) 406 { 407 // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache 408 // will suffice. 409 if (watch.equals("")) 410 { 411 return nonWatchFactory; 412 } 413 414 // Because of the watch, its necessary to create a factory for instances of this component and method. 415 416 final String bindingFieldName = WATCH_BINDING_PREFIX + method.getDescription().methodName; 417 final PlasticField bindingField = plasticClass.introduceField(Binding.class, bindingFieldName); 418 final FieldHandle bindingFieldHandle = bindingField.getHandle(); 419 420 if (multipleClassLoaders) 421 { 422 fieldInfos.add(PlasticUtils.toFieldInfo(bindingField)); 423 try 424 { 425 bindingField.createAccessors(PropertyAccessType.READ_WRITE); 426 } 427 catch (IllegalArgumentException e) 428 { 429 // Method already implemented in superclass, so, given we only 430 // care the method exists, we ignore this exception 431 } 432 } 433 434 // Each component instance will get its own Binding instance. That handles both different locales, 435 // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized 436 // until the page loads. 437 438 plasticClass.introduceInterface(PageLifecycleListener.class); 439 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice() 440 { 441 public void advise(MethodInvocation invocation) 442 { 443 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class); 444 445 Binding binding = bindingSource.newBinding("@Cached watch", resources, 446 BindingConstants.PROP, watch); 447 448 final Object instance = invocation.getInstance(); 449 if (multipleClassLoaders) 450 { 451 PropertyValueProvider.set(instance, bindingFieldName, binding); 452 } 453 else 454 { 455 bindingFieldHandle.set(instance, binding); 456 } 457 458 invocation.proceed(); 459 } 460 }); 461 462 return new MethodResultCacheFactory() 463 { 464 public MethodResultCache create(Object instance) 465 { 466 Binding binding = (Binding) ( 467 multipleClassLoaders ? 468 PropertyValueProvider.get(instance, bindingFieldName) : 469 bindingFieldHandle.get(instance)); 470 471 return new WatchedBindingMethodResultCache(binding); 472 } 473 474 private Object getCacheBinding(final String methodName, String bindingFieldName, Object instance, ComponentModel model) 475 { 476 Object value = PropertyValueProvider.get(instance, bindingFieldName); 477 while (value == null && model.getParentModel() != null) 478 { 479 model = model.getParentModel(); 480 bindingFieldName = getFieldName(WATCH_BINDING_PREFIX, 481 methodName, model.getComponentClassName()); 482 value = PropertyValueProvider.get(instance, bindingFieldName); 483 } 484 return value; 485 } 486 }; 487 } 488 489 private void validateMethod(PlasticMethod method) 490 { 491 MethodDescription description = method.getDescription(); 492 493 if (description.returnType.equals("void")) 494 throw new IllegalArgumentException(String.format( 495 "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier())); 496 497 if (description.argumentTypes.length != 0) 498 throw new IllegalArgumentException(String.format( 499 "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier())); 500 } 501}