    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}