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