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}