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
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.UnknownValueException;
028import org.apache.tapestry5.model.ComponentModel;
029import org.apache.tapestry5.model.EmbeddedComponentModel;
030import org.apache.tapestry5.model.MutableComponentModel;
031import org.apache.tapestry5.plastic.*;
032import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
033import org.apache.tapestry5.services.transform.TransformationSupport;
034
035import 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 */
043public 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