    001    // Copyright 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
    018    import org.apache.tapestry5.ComponentResources;
    019    import org.apache.tapestry5.annotations.BindParameter;
    020    import org.apache.tapestry5.internal.InternalComponentResources;
    021    import org.apache.tapestry5.internal.services.ComponentClassCache;
    022    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    023    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    024    import org.apache.tapestry5.ioc.internal.util.TapestryException;
    025    import org.apache.tapestry5.ioc.services.TypeCoercer;
    026    import org.apache.tapestry5.ioc.util.AvailableValues;
    027    import org.apache.tapestry5.ioc.util.UnknownValueException;
    028    import org.apache.tapestry5.model.ComponentModel;
    029    import org.apache.tapestry5.model.EmbeddedComponentModel;
    030    import org.apache.tapestry5.model.MutableComponentModel;
    031    import org.apache.tapestry5.plastic.*;
    032    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
    033    import org.apache.tapestry5.services.transform.TransformationSupport;
    034
    035    import java.util.List;
    036
    037    /**
    038     * Responsible for identifying, via the {@link org.apache.tapestry5.annotations.BindParameter} annotation, mixin fields
    039     * that should be bound to a core-component parameter value.
    040     *
    041     * @since 5.2.0
    042     */
    043    public class BindParameterWorker implements ComponentClassTransformWorker2
    044    {
    045        private final class BoundParameterFieldValueConduit implements FieldConduit<Object>
    046        {
    047            private final String containerParameterName;
    048
    049            private final InternalComponentResources containerResources;
    050
    051            private final Class fieldType;
    052
    053            // Guarded by this
    054            private ParameterConduit conduit;
    055
    056            private BoundParameterFieldValueConduit(String containerParameterName,
    057                                                    InternalComponentResources containerResources, Class fieldType)
    058            {
    059                this.containerParameterName = containerParameterName;
    060                this.containerResources = containerResources;
    061                this.fieldType = fieldType;
    062            }
    063
    064            /**
    065             * Defer obtaining the conduit object until needed, to deal with the complex
    066             * lifecycle of
    067             * parameters. Perhaps this can be addressed by converting constructors into
    068             * methods invoked
    069             * from the page loaded lifecycle method?
    070             */
    071            private synchronized ParameterConduit getParameterConduit()
    072            {
    073                if (conduit == null)
    074                {
    075                    // if the parameter is not a formal parameter then it must be a published parameter
    076                    if (containerResources.getComponentModel().isFormalParameter(containerParameterName))
    077                        conduit = containerResources.getParameterConduit(containerParameterName);
    078                    else
    079                            conduit = getEmbeddedComponentResourcesForPublishedParameter(containerResources, containerParameterName)
    080                                            .getParameterConduit(containerParameterName);
    081                }
    082
    083                return conduit;
    084            }
    085
    086
    087            public Object get(Object instance, InstanceContext context)
    088            {
    089                // For the moment, this results in two passes through the TypeCoercer; we'll look
    090                // to optimize that in the future. The first pass is deep inside ParameterConduit (coercing
    091                // to the component parameter field type), the second is here (usually the same type so no
    092                // real coercion necessary).
    093
    094                Object result = getParameterConduit().get(instance, context);
    095
    096                return typeCoercer.coerce(result, fieldType);
    097            }
    098
    099            public void set(Object instance, InstanceContext context, Object newValue)
    100            {
    101                getParameterConduit().set(instance, context, newValue);
    102
    103            }
    104        }
    105
    106        private final TypeCoercer typeCoercer;
    107
    108        private final ComponentClassCache componentClassCache;
    109
    110        public BindParameterWorker(TypeCoercer typeCoercer, ComponentClassCache componentClassCache)
    111        {
    112            this.typeCoercer = typeCoercer;
    113            this.componentClassCache = componentClassCache;
    114        }
    115
    116        public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    117        {
    118            for (PlasticField field : plasticClass.getFieldsWithAnnotation(BindParameter.class))
    119            {
    120                convertFieldIntoContainerBoundParameter(field);
    121            }
    122        }
    123
    124        private void convertFieldIntoContainerBoundParameter(PlasticField field)
    125        {
    126            BindParameter annotation = field.getAnnotation(BindParameter.class);
    127
    128            field.claim(annotation);
    129
    130            final String[] possibleNames = annotation.value();
    131
    132            final String fieldTypeName = field.getTypeName();
    133
    134            final String fieldName = field.getName();
    135
    136            ComputedValue<FieldConduit<Object>> computedConduit = new ComputedValue<FieldConduit<Object>>()
    137            {
    138                public FieldConduit<Object> get(InstanceContext context)
    139                {
    140                    ComponentResources resources = context.get(ComponentResources.class);
    141
    142                    try
    143                    {
    144                        return createConduit(resources, fieldTypeName, fieldName, possibleNames);
    145                    } catch (Exception ex)
    146                    {
    147                        throw new TapestryException(String.format(
    148                                "Failure binding parameter field '%s' of mixin %s (type %s): %s", fieldName, resources
    149                                .getCompleteId(), resources.getComponentModel().getComponentClassName(),
    150                                InternalUtils.toMessage(ex)), ex);
    151                    }
    152                }
    153
    154            };
    155
    156            field.setComputedConduit(computedConduit);
    157        }
    158
    159        private FieldConduit<Object> createConduit(final ComponentResources resources, final String fieldTypeName,
    160                                                   final String fieldName, final String[] possibleNames)
    161        {
    162            if (!resources.isMixin())
    163                throw new TapestryException(TransformMessages.bindParameterOnlyOnMixin(fieldName, resources), null);
    164
    165            InternalComponentResources containerResources = (InternalComponentResources) resources.getContainerResources();
    166
    167            // Evaluate this early so that we get a fast fail.
    168
    169            String containerParameterName = identifyParameterName(resources, InternalUtils.stripMemberName(fieldName),
    170                    possibleNames);
    171
    172            Class fieldType = componentClassCache.forName(fieldTypeName);
    173
    174            return new BoundParameterFieldValueConduit(containerParameterName, containerResources, fieldType);
    175        }
    176
    177        private String identifyParameterName(ComponentResources resources, String firstGuess, String... otherGuesses)
    178        {
    179            ComponentModel model = resources.getContainerResources().getComponentModel();
    180
    181            List<String> guesses = CollectionFactory.newList();
    182            guesses.add(firstGuess);
    183
    184            for (String name : otherGuesses)
    185            {
    186                guesses.add(name);
    187            }
    188
    189            for (String name : guesses)
    190            {
    191                if (model.isFormalParameter(name))
    192                    return name;
    193
    194                if(isPublishedParameter(model, name))
    195                    return name;
    196            }
    197
    198            String message = String.format("Containing component %s does not contain a formal parameter or a published parameter %s %s.",
    199
    200                    model.getComponentClassName(),
    201
    202                    guesses.size() == 1 ? "matching" : "matching any of",
    203
    204                    InternalUtils.joinSorted(guesses));
    205
    206            List<String> formalAndPublishedParameters = CollectionFactory.newList(model.getParameterNames());
    207            formalAndPublishedParameters.addAll(getPublishedParameters(model));
    208
    209            throw new UnknownValueException(message, new AvailableValues("Formal and published parameters", formalAndPublishedParameters));
    210        }
    211
    212        /**
    213         * Returns true if the parameter with the given parameterName is a published parameter
    214         * of any of the embedded components for the component with the given model.
    215         */
    216        private boolean isPublishedParameter(ComponentModel model,  String parameterName)
    217            {
    218            for (String embeddedComponentId : model.getEmbeddedComponentIds())
    219                    {
    220                    EmbeddedComponentModel embeddedComponentModel = model
    221                                                    .getEmbeddedComponentModel(embeddedComponentId);
    222                    if (embeddedComponentModel.getPublishedParameters().contains(parameterName)) return true;
    223                    }
    224
    225            return false;
    226            }
    227
    228        private List<String> getPublishedParameters(ComponentModel model)
    229        {
    230            List<String> publishedParameters = CollectionFactory.newList();
    231            for (String embeddedComponentId : model.getEmbeddedComponentIds())
    232            {
    233                    EmbeddedComponentModel embeddedComponentModel = model.getEmbeddedComponentModel(embeddedComponentId);
    234                publishedParameters.addAll(embeddedComponentModel.getPublishedParameters());
    235            }
    236            return publishedParameters;
    237        }
    238
    239            /**
    240         * Returns the {@link InternalComponentResources} of an embeddedComponent that contains the published parameter
    241         * publishedParameterName. This is basically a recursive search for published parameters.
    242         */
    243            private InternalComponentResources getEmbeddedComponentResourcesForPublishedParameter(InternalComponentResources containerResources,
    244                            String publishedParameterName)
    245            {
    246                    List<InternalComponentResources> embeddedComponentResourcesList = CollectionFactory.newList();
    247
    248                    embeddedComponentResourcesList.add(containerResources);
    249
    250                    while(!embeddedComponentResourcesList.isEmpty())
    251                    {
    252                            InternalComponentResources resources = embeddedComponentResourcesList.remove(0);
    253
    254                            ComponentModel containerComponentModel = resources.getComponentModel();
    255
    256                            for(String embeddedComponentId : containerComponentModel.getEmbeddedComponentIds())
    257                            {
    258                                    EmbeddedComponentModel embeddedComponentModel = containerComponentModel
    259                                                    .getEmbeddedComponentModel(embeddedComponentId);
    260
    261                                    InternalComponentResources embeddedComponentResources = (InternalComponentResources) resources
    262                                                    .getEmbeddedComponent(embeddedComponentId).getComponentResources();
    263                                    /**
    264                             * If the parameter is not a formal parameter, then the parameter must be a published parameter
    265                             * of an embeddedComponent of the component we are currently examining.
    266                             */
    267                                    if(embeddedComponentModel.getPublishedParameters().contains(publishedParameterName)
    268                                                    && embeddedComponentResources.getComponentModel().isFormalParameter(publishedParameterName))
    269                                    {
    270                                            return embeddedComponentResources;
    271                                    }
    272
    273                                    embeddedComponentResourcesList.add(embeddedComponentResources);
    274                            }
    275                    }
    276
    277                    return null;
    278            }
    279    }
    280