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