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.annotations.Cached; 021import org.apache.tapestry5.internal.TapestryInternalUtils; 022import org.apache.tapestry5.ioc.services.PerThreadValue; 023import org.apache.tapestry5.ioc.services.PerthreadManager; 024import org.apache.tapestry5.model.MutableComponentModel; 025import org.apache.tapestry5.plastic.*; 026import org.apache.tapestry5.runtime.PageLifecycleListener; 027import org.apache.tapestry5.services.BindingSource; 028import org.apache.tapestry5.services.TransformConstants; 029import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 030import org.apache.tapestry5.services.transform.TransformationSupport; 031 032import java.util.List; 033 034/** 035 * Caches method return values for methods annotated with {@link Cached}. 036 */ 037@SuppressWarnings("all") 038public 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 if(!invocation.didThrowCheckedException()) 186 { 187 cache.set(invocation.getReturnValue()); 188 } 189 } 190 191 private MethodResultCache getOrCreateCache(MethodInvocation invocation) 192 { 193 Object instance = invocation.getInstance(); 194 195 // The PerThreadValue is created in the instance constructor. 196 197 PerThreadValue<MethodResultCache> value = (PerThreadValue<MethodResultCache>) fieldHandle 198 .get(instance); 199 200 // But it will be empty when first created, or at the start of a new request. 201 if (value.exists()) 202 { 203 return value.get(); 204 } 205 206 // Use the factory to create a MethodResultCache for the combination of instance, method, and thread. 207 208 return value.set(factory.create(instance)); 209 } 210 }; 211 } 212 213 214 private MethodResultCacheFactory createFactory(PlasticClass plasticClass, final String watch, 215 PlasticMethod method) 216 { 217 // When there's no watch, a shared factory that just returns a new SimpleMethodResultCache 218 // will suffice. 219 if (watch.equals("")) 220 { 221 return nonWatchFactory; 222 } 223 224 // Because of the watch, its necessary to create a factory for instances of this component and method. 225 226 final FieldHandle bindingFieldHandle = plasticClass.introduceField(Binding.class, "cache$watchBinding$" + method.getDescription().methodName).getHandle(); 227 228 229 // Each component instance will get its own Binding instance. That handles both different locales, 230 // and reuse of a component (with a cached method) within a page or across pages. However, the binding can't be initialized 231 // until the page loads. 232 233 plasticClass.introduceInterface(PageLifecycleListener.class); 234 plasticClass.introduceMethod(TransformConstants.CONTAINING_PAGE_DID_LOAD_DESCRIPTION).addAdvice(new MethodAdvice() 235 { 236 public void advise(MethodInvocation invocation) 237 { 238 ComponentResources resources = invocation.getInstanceContext().get(ComponentResources.class); 239 240 Binding binding = bindingSource.newBinding("@Cached watch", resources, 241 BindingConstants.PROP, watch); 242 243 bindingFieldHandle.set(invocation.getInstance(), binding); 244 245 invocation.proceed(); 246 } 247 }); 248 249 return new MethodResultCacheFactory() 250 { 251 public MethodResultCache create(Object instance) 252 { 253 Binding binding = (Binding) bindingFieldHandle.get(instance); 254 255 return new WatchedBindingMethodResultCache(binding); 256 } 257 }; 258 } 259 260 private void validateMethod(PlasticMethod method) 261 { 262 MethodDescription description = method.getDescription(); 263 264 if (description.returnType.equals("void")) 265 throw new IllegalArgumentException(String.format( 266 "Method %s may not be used with @Cached because it returns void.", method.getMethodIdentifier())); 267 268 if (description.argumentTypes.length != 0) 269 throw new IllegalArgumentException(String.format( 270 "Method %s may not be used with @Cached because it has parameters.", method.getMethodIdentifier())); 271 } 272}