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}