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