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}