    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}