    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}