    001// Copyright 2006-2013 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.annotations.Parameter;
    019import org.apache.tapestry5.func.F;
    020import org.apache.tapestry5.func.Flow;
    021import org.apache.tapestry5.func.Predicate;
    022import org.apache.tapestry5.internal.InternalComponentResources;
    023import org.apache.tapestry5.internal.bindings.LiteralBinding;
    024import org.apache.tapestry5.internal.services.ComponentClassCache;
    025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026import org.apache.tapestry5.ioc.internal.util.TapestryException;
    027import org.apache.tapestry5.ioc.services.PerThreadValue;
    028import org.apache.tapestry5.ioc.services.PerthreadManager;
    029import org.apache.tapestry5.ioc.services.TypeCoercer;
    030import org.apache.tapestry5.ioc.util.ExceptionUtils;
    031import org.apache.tapestry5.model.MutableComponentModel;
    032import org.apache.tapestry5.plastic.*;
    033import org.apache.tapestry5.services.BindingSource;
    034import org.apache.tapestry5.services.ComponentDefaultProvider;
    035import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    036import org.apache.tapestry5.services.transform.TransformationSupport;
    037import org.slf4j.Logger;
    038import org.slf4j.LoggerFactory;
    039
    040import java.util.Comparator;
    041
    042/**
    043 * Responsible for identifying parameters via the {@link org.apache.tapestry5.annotations.Parameter} annotation on
    044 * component fields. This is one of the most complex of the transformations.
    045 */
    046public class ParameterWorker implements ComponentClassTransformWorker2
    047{
    048    private final Logger logger = LoggerFactory.getLogger(ParameterWorker.class);
    049
    050    /**
    051     * Contains the per-thread state about a parameter, as stored (using
    052     * a unique key) in the {@link PerthreadManager}. Externalizing such state
    053     * is part of Tapestry 5.2's pool-less pages.
    054     */
    055    private final class ParameterState
    056    {
    057        boolean cached;
    058
    059        Object value;
    060
    061        void reset(Object defaultValue)
    062        {
    063            cached = false;
    064            value = defaultValue;
    065        }
    066    }
    067
    068    private final ComponentClassCache classCache;
    069
    070    private final BindingSource bindingSource;
    071
    072    private final ComponentDefaultProvider defaultProvider;
    073
    074    private final TypeCoercer typeCoercer;
    075
    076    private final PerthreadManager perThreadManager;
    077
    078    public ParameterWorker(ComponentClassCache classCache, BindingSource bindingSource,
    079                           ComponentDefaultProvider defaultProvider, TypeCoercer typeCoercer, PerthreadManager perThreadManager)
    080    {
    081        this.classCache = classCache;
    082        this.bindingSource = bindingSource;
    083        this.defaultProvider = defaultProvider;
    084        this.typeCoercer = typeCoercer;
    085        this.perThreadManager = perThreadManager;
    086    }
    087
    088    private final Comparator<PlasticField> byPrincipalThenName = new Comparator<PlasticField>()
    089    {
    090        public int compare(PlasticField o1, PlasticField o2)
    091        {
    092            boolean principal1 = o1.getAnnotation(Parameter.class).principal();
    093            boolean principal2 = o2.getAnnotation(Parameter.class).principal();
    094
    095            if (principal1 == principal2)
    096            {
    097                return o1.getName().compareTo(o2.getName());
    098            }
    099
    100            return principal1 ? -1 : 1;
    101        }
    102    };
    103
    104
    105    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    106    {
    107        Flow<PlasticField> parametersFields = F.flow(plasticClass.getFieldsWithAnnotation(Parameter.class)).sort(byPrincipalThenName);
    108
    109        for (PlasticField field : parametersFields)
    110        {
    111            convertFieldIntoParameter(plasticClass, model, field);
    112        }
    113    }
    114
    115    private void convertFieldIntoParameter(PlasticClass plasticClass, MutableComponentModel model,
    116                                           PlasticField field)
    117    {
    118
    119        Parameter annotation = field.getAnnotation(Parameter.class);
    120
    121        String fieldType = field.getTypeName();
    122
    123        String parameterName = getParameterName(field.getName(), annotation.name());
    124
    125        field.claim(annotation);
    126
    127        model.addParameter(parameterName, annotation.required(), annotation.allowNull(), annotation.defaultPrefix(),
    128                annotation.cache());
    129
    130        MethodHandle defaultMethodHandle = findDefaultMethodHandle(plasticClass, parameterName);
    131
    132        ComputedValue<FieldConduit<Object>> computedParameterConduit = createComputedParameterConduit(parameterName, fieldType,
    133                annotation, defaultMethodHandle);
    134
    135        field.setComputedConduit(computedParameterConduit);
    136    }
    137
    138
    139    private MethodHandle findDefaultMethodHandle(PlasticClass plasticClass, String parameterName)
    140    {
    141        final String methodName = "default" + parameterName;
    142
    143        Predicate<PlasticMethod> predicate = new Predicate<PlasticMethod>()
    144        {
    145            public boolean accept(PlasticMethod method)
    146            {
    147                return method.getDescription().argumentTypes.length == 0
    148                        && method.getDescription().methodName.equalsIgnoreCase(methodName);
    149            }
    150        };
    151
    152        Flow<PlasticMethod> matches = F.flow(plasticClass.getMethods()).filter(predicate);
    153
    154        // This will match exactly 0 or 1 (unless the user does something really silly)
    155        // methods, and if it matches, we know the name of the method.
    156
    157        return matches.isEmpty() ? null : matches.first().getHandle();
    158    }
    159
    160    @SuppressWarnings("all")
    161    private ComputedValue<FieldConduit<Object>> createComputedParameterConduit(final String parameterName,
    162                                                                               final String fieldTypeName, final Parameter annotation,
    163                                                                               final MethodHandle defaultMethodHandle)
    164    {
    165        boolean primitive = PlasticUtils.isPrimitive(fieldTypeName);
    166
    167        final boolean allowNull = annotation.allowNull() && !primitive;
    168
    169        return new ComputedValue<FieldConduit<Object>>()
    170        {
    171            public ParameterConduit get(InstanceContext context)
    172            {
    173                final InternalComponentResources icr = context.get(InternalComponentResources.class);
    174
    175                final Class fieldType = classCache.forName(fieldTypeName);
    176
    177                final PerThreadValue<ParameterState> stateValue = perThreadManager.createValue();
    178
    179                // Rely on some code generation in the component to set the default binding from
    180                // the field, or from a default method.
    181
    182                return new ParameterConduit()
    183                {
    184                    // Default value for parameter, computed *once* at
    185                    // page load time.
    186
    187                    private Object defaultValue = classCache.defaultValueForType(fieldTypeName);
    188
    189                    private Binding parameterBinding;
    190
    191                    boolean loaded = false;
    192
    193                    private boolean invariant = false;
    194
    195                    {
    196                        // Inform the ComponentResources about the parameter conduit, so it can be
    197                        // shared with mixins.
    198
    199                        icr.setParameterConduit(parameterName, this);
    200                        icr.getPageLifecycleCallbackHub().addPageLoadedCallback(new Runnable()
    201                        {
    202                            public void run()
    203                            {
    204                                load();
    205                            }
    206                        });
    207                    }
    208
    209                    private ParameterState getState()
    210                    {
    211                        ParameterState state = stateValue.get();
    212
    213                        if (state == null)
    214                        {
    215                            state = new ParameterState();
    216                            state.value = defaultValue;
    217                            stateValue.set(state);
    218                        }
    219
    220                        return state;
    221                    }
    222
    223                    private boolean isLoaded()
    224                    {
    225                        return loaded;
    226                    }
    227
    228                    public void set(Object instance, InstanceContext context, Object newValue)
    229                    {
    230                        ParameterState state = getState();
    231
    232                        // Assignments before the page is loaded ultimately exist to set the
    233                        // default value for the field. Often this is from the (original)
    234                        // constructor method, which is converted to a real method as part of the transformation.
    235
    236                        if (!loaded)
    237                        {
    238                            state.value = newValue;
    239                            defaultValue = newValue;
    240                            return;
    241                        }
    242
    243                        // This will catch read-only or unbound parameters.
    244
    245                        writeToBinding(newValue);
    246
    247                        state.value = newValue;
    248
    249                        // If caching is enabled for the parameter (the typical case) and the
    250                        // component is currently rendering, then the result
    251                        // can be cached in this ParameterConduit (until the component finishes
    252                        // rendering).
    253
    254                        state.cached = annotation.cache() && icr.isRendering();
    255                    }
    256
    257                    private Object readFromBinding()
    258                    {
    259                        Object result;
    260
    261                        try
    262                        {
    263                            Object boundValue = parameterBinding.get();
    264
    265                            result = typeCoercer.coerce(boundValue, fieldType);
    266                        } catch (RuntimeException ex)
    267                        {
    268                            throw new TapestryException(String.format(
    269                                    "Failure reading parameter '%s' of component %s: %s", parameterName,
    270                                    icr.getCompleteId(), ExceptionUtils.toMessage(ex)), parameterBinding, ex);
    271                        }
    272
    273                        if (result == null && !allowNull)
    274                        {
    275                            throw new TapestryException(
    276                                    String.format(
    277                                            "Parameter '%s' of component %s is bound to null. This parameter is not allowed to be null.",
    278                                            parameterName, icr.getCompleteId()), parameterBinding, null);
    279                        }
    280
    281                        return result;
    282                    }
    283
    284                    private void writeToBinding(Object newValue)
    285                    {
    286                        // An unbound parameter acts like a simple field
    287                        // with no side effects.
    288
    289                        if (parameterBinding == null)
    290                        {
    291                            return;
    292                        }
    293
    294                        try
    295                        {
    296                            Object coerced = typeCoercer.coerce(newValue, parameterBinding.getBindingType());
    297
    298                            parameterBinding.set(coerced);
    299                        } catch (RuntimeException ex)
    300                        {
    301                            throw new TapestryException(String.format(
    302                                    "Failure writing parameter '%s' of component %s: %s", parameterName,
    303                                    icr.getCompleteId(), ExceptionUtils.toMessage(ex)), icr, ex);
    304                        }
    305                    }
    306
    307                    public void reset()
    308                    {
    309                        if (!invariant)
    310                        {
    311                            getState().reset(defaultValue);
    312                        }
    313                    }
    314
    315                    public void load()
    316                    {
    317                        if (logger.isDebugEnabled())
    318                        {
    319                            logger.debug(String.format("%s loading parameter %s", icr.getCompleteId(), parameterName));
    320                        }
    321
    322                        // If it's bound at this point, that's because of an explicit binding
    323                        // in the template or @Component annotation.
    324
    325                        if (!icr.isBound(parameterName))
    326                        {
    327                            if (logger.isDebugEnabled())
    328                            {
    329                                logger.debug(String.format("%s parameter %s not yet bound", icr.getCompleteId(),
    330                                        parameterName));
    331                            }
    332
    333                            // Otherwise, construct a default binding, or use one provided from
    334                            // the component.
    335
    336                            Binding binding = getDefaultBindingForParameter();
    337
    338                            if (logger.isDebugEnabled())
    339                            {
    340                                logger.debug(String.format("%s parameter %s bound to default %s", icr.getCompleteId(),
    341                                        parameterName, binding));
    342                            }
    343
    344                            if (binding != null)
    345                            {
    346                                icr.bindParameter(parameterName, binding);
    347                            }
    348                        }
    349
    350                        parameterBinding = icr.getBinding(parameterName);
    351
    352                        loaded = true;
    353
    354                        invariant = parameterBinding != null && parameterBinding.isInvariant();
    355
    356                        getState().value = defaultValue;
    357                    }
    358
    359                    public boolean isBound()
    360                    {
    361                        return parameterBinding != null;
    362                    }
    363
    364                    public Object get(Object instance, InstanceContext context)
    365                    {
    366                        if (!isLoaded())
    367                        {
    368                            return defaultValue;
    369                        }
    370
    371                        ParameterState state = getState();
    372
    373                        if (state.cached || !isBound())
    374                        {
    375                            return state.value;
    376                        }
    377
    378                        // Read the parameter's binding and cast it to the
    379                        // field's type.
    380
    381                        Object result = readFromBinding();
    382
    383                        // If the value is invariant, we can cache it until at least the end of the request (before
    384                        // 5.2, it would be cached forever in the pooled instance).
    385                        // Otherwise, we we may want to cache it for the remainder of the component render (if the
    386                        // component is currently rendering).
    387
    388                        if (invariant || (annotation.cache() && icr.isRendering()))
    389                        {
    390                            state.value = result;
    391                            state.cached = true;
    392                        }
    393
    394                        return result;
    395                    }
    396
    397                    private Binding getDefaultBindingForParameter()
    398                    {
    399                        if (InternalUtils.isNonBlank(annotation.value()))
    400                        {
    401                            return bindingSource.newBinding("default " + parameterName, icr,
    402                                    annotation.defaultPrefix(), annotation.value());
    403                        }
    404
    405                        if (annotation.autoconnect())
    406                        {
    407                            return defaultProvider.defaultBinding(parameterName, icr);
    408                        }
    409
    410                        // Invoke the default method and install any value or Binding returned there.
    411
    412                        invokeDefaultMethod();
    413
    414                        return parameterBinding;
    415                    }
    416
    417                    private void invokeDefaultMethod()
    418                    {
    419                        if (defaultMethodHandle == null)
    420                        {
    421                            return;
    422                        }
    423
    424                        if (logger.isDebugEnabled())
    425                        {
    426                            logger.debug(String.format("%s invoking method %s to obtain default for parameter %s",
    427                                    icr.getCompleteId(), defaultMethodHandle, parameterName));
    428                        }
    429
    430                        MethodInvocationResult result = defaultMethodHandle.invoke(icr.getComponent());
    431
    432                        result.rethrow();
    433
    434                        Object defaultValue = result.getReturnValue();
    435
    436                        if (defaultValue == null)
    437                        {
    438                            return;
    439                        }
    440
    441                        if (defaultValue instanceof Binding)
    442                        {
    443                            parameterBinding = (Binding) defaultValue;
    444                            return;
    445                        }
    446
    447                        parameterBinding = new LiteralBinding(null, "default " + parameterName, defaultValue);
    448                    }
    449
    450
    451                };
    452            }
    453        };
    454    }
    455
    456    private static String getParameterName(String fieldName, String annotatedName)
    457    {
    458        if (InternalUtils.isNonBlank(annotatedName))
    459        {
    460            return annotatedName;
    461        }
    462
    463        return InternalUtils.stripMemberName(fieldName);
    464    }
    465}