001// Copyright 2009, 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.pageload;
016
017import org.apache.tapestry5.internal.TapestryInternalUtils;
018import org.apache.tapestry5.internal.services.ComponentInstantiatorSource;
019import org.apache.tapestry5.internal.services.Instantiator;
020import org.apache.tapestry5.internal.structure.ComponentPageElement;
021import org.apache.tapestry5.ioc.Location;
022import org.apache.tapestry5.ioc.Orderable;
023import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025import org.apache.tapestry5.ioc.internal.util.TapestryException;
026import org.apache.tapestry5.model.ComponentModel;
027import org.apache.tapestry5.model.EmbeddedComponentModel;
028import org.apache.tapestry5.services.ComponentClassResolver;
029import org.apache.tapestry5.services.pageload.ComponentResourceSelector;
030
031import java.util.HashSet;
032import java.util.Map;
033import java.util.Set;
034
035public class EmbeddedComponentAssemblerImpl implements EmbeddedComponentAssembler
036{
037    private final ComponentInstantiatorSource instantiatorSource;
038
039    private final ComponentAssemblerSource assemblerSource;
040
041    private final ComponentResourceSelector selector;
042
043    private final ComponentModel componentModel;
044
045    private final Location location;
046
047    private final Map<String, Instantiator> mixinIdToInstantiator = CollectionFactory.newCaseInsensitiveMap();
048    private final Map<String, String[]> mixinsIdToOrderConstraints = CollectionFactory.newCaseInsensitiveMap();
049
050    /**
051     * Maps parameter names (both simple, and qualified with the mixin id) to the corresponding QualifiedParameterName.
052     */
053    private final Map<String, ParameterBinder> parameterNameToBinder = CollectionFactory.newCaseInsensitiveMap();
054
055    // The id of the mixin to receive informal parameters. If null, the component itself recieves them.
056    // If the component doesn't support them, they are quietly dropped.
057
058    private final String informalParametersMixinId;
059
060    private final String componentPsuedoMixinId;
061
062    private Map<String, Boolean> bound;
063
064    /**
065     * @param assemblerSource
066     * @param instantiatorSource     used to access component models
067     * @param componentClassResolver used to convert mixin types to component models
068     * @param componentClassName     class name of embedded component
069     * @param selector               used to select template and other resources
070     * @param embeddedModel          embedded model (may be null for components defined in the template)
071     * @param templateMixins         list of mixins from the t:mixins element (possibly null)
072     * @param location               location of components element in its container's template
073     */
074    public EmbeddedComponentAssemblerImpl(ComponentAssemblerSource assemblerSource,
075                                          ComponentInstantiatorSource instantiatorSource, ComponentClassResolver componentClassResolver,
076                                          String componentClassName, ComponentResourceSelector selector, EmbeddedComponentModel embeddedModel,
077                                          String templateMixins, Location location)
078    {
079        this.assemblerSource = assemblerSource;
080        this.instantiatorSource = instantiatorSource;
081        this.selector = selector;
082        this.location = location;
083
084        componentModel = getModel(componentClassName);
085
086        // Add the implementation mixins defined by the component model.
087
088        for (String className : componentModel.getMixinClassNames())
089        {
090            addMixin(className, componentModel.getOrderForMixin(className));
091        }
092
093        // If there's an embedded model (i.e., there was an @Component annotation)
094        // then it may define some mixins.
095
096        if (embeddedModel != null)
097        {
098            for (String className : embeddedModel.getMixinClassNames())
099            {
100                addMixin(className, embeddedModel.getConstraintsForMixin(className));
101            }
102        }
103
104        // And the template may include a t:mixins element to define yet more mixins.
105        // Template strings specified as:
106        for (String mixinDef : TapestryInternalUtils.splitAtCommas(templateMixins))
107        {
108            Orderable<String> order = TapestryInternalUtils.mixinTypeAndOrder(mixinDef);
109            String className = componentClassResolver.resolveMixinTypeToClassName(order.getId());
110
111            addMixin(className, order.getConstraints());
112        }
113
114        // Finally (new in 5.3, for TAP5-1680), the component itself can sometimes acts as a mixin;
115        // this allows for dealing with parameter name conflicts between the component and the mixin
116        // (especially where informal parameters are involved).
117
118        componentPsuedoMixinId = InternalUtils.lastTerm(componentClassName);
119
120        informalParametersMixinId = prescanMixins();
121
122    }
123
124    private String prescanMixins()
125    {
126        // Mixin id found to support informal parameters
127
128        String supportsInformals = null;
129
130        for (Map.Entry<String, Instantiator> entry : mixinIdToInstantiator.entrySet())
131        {
132            String mixinId = entry.getKey();
133            ComponentModel mixinModel = entry.getValue().getModel();
134
135            updateParameterNameToQualified(mixinId, mixinModel);
136
137            if (supportsInformals == null && mixinModel.getSupportsInformalParameters())
138                supportsInformals = mixinId;
139        }
140
141        // The component comes last and overwrites simple names from the others.
142
143        updateParameterNameToQualified(null, componentModel);
144
145        return supportsInformals;
146    }
147
148    private void updateParameterNameToQualified(String mixinId, ComponentModel model)
149    {
150        for (String parameterName : model.getParameterNames())
151        {
152            String defaultBindingPrefix = model.getParameterModel(parameterName).getDefaultBindingPrefix();
153
154            ParameterBinderImpl binder = new ParameterBinderImpl(mixinId, parameterName, defaultBindingPrefix);
155
156            parameterNameToBinder.put(parameterName, binder);
157
158            if (mixinId != null)
159                parameterNameToBinder.put(mixinId + "." + parameterName, binder);
160        }
161    }
162
163    private void addMixin(String className, String... order)
164    {
165        Instantiator mixinInstantiator = instantiatorSource.getInstantiator(className);
166
167        String mixinId = InternalUtils.lastTerm(className);
168
169        if (mixinIdToInstantiator.containsKey(mixinId))
170            throw new TapestryException(PageloadMessages.uniqueMixinRequired(mixinId), location, null);
171
172        mixinIdToInstantiator.put(mixinId, mixinInstantiator);
173        mixinsIdToOrderConstraints.put(mixinId, order);
174    }
175
176    private ComponentModel getModel(String className)
177    {
178        return instantiatorSource.getInstantiator(className).getModel();
179    }
180
181    public ComponentAssembler getComponentAssembler()
182    {
183        return assemblerSource.getAssembler(componentModel.getComponentClassName(), selector);
184    }
185
186
187    public ParameterBinder createParameterBinder(String qualifiedParameterName)
188    {
189        int dotx = qualifiedParameterName.indexOf('.');
190
191        if (dotx < 0)
192        {
193            return createParameterBinderFromSimpleParameterName(qualifiedParameterName);
194        }
195
196        return createParameterBinderFromQualifiedParameterName(qualifiedParameterName, qualifiedParameterName.substring(0, dotx),
197                qualifiedParameterName.substring(dotx + 1));
198    }
199
200    private ParameterBinder createParameterBinderFromSimpleParameterName(String parameterName)
201    {
202
203        // Look for a *formal* parameter with the simple name on the component itself.
204
205        ParameterBinder binder = getComponentAssembler().getBinder(parameterName);
206
207        if (binder != null)
208        {
209            return binder;
210        }
211
212        // Next see if any mixin has a formal parameter with this simple name.
213
214        binder = parameterNameToBinder.get(parameterName);
215
216        if (binder != null)
217        {
218            return binder;
219        }
220
221
222        // So, is there an mixin that's claiming all informal parameters?
223
224        if (informalParametersMixinId != null)
225        {
226            return new ParameterBinderImpl(informalParametersMixinId, parameterName, null);
227        }
228
229        // Maybe the component claims informal parameters?
230        if (componentModel.getSupportsInformalParameters())
231            return new ParameterBinderImpl(null, parameterName, null);
232
233        // Otherwise, informal parameter are not supported by the component or any mixin.
234
235        return null;
236    }
237
238    private ParameterBinder createParameterBinderFromQualifiedParameterName(String qualifiedParameterName, String mixinId, String parameterName)
239    {
240
241        if (mixinId.equalsIgnoreCase(componentPsuedoMixinId))
242        {
243            return createParameterBinderForComponent(qualifiedParameterName, parameterName);
244        }
245
246        if (!mixinIdToInstantiator.containsKey(mixinId))
247        {
248            throw new TapestryException(
249                    String.format("Mixin id for parameter '%s' not found. Attached mixins: %s.", qualifiedParameterName,
250                            InternalUtils.joinSorted(mixinIdToInstantiator.keySet())), location,
251                    null);
252        }
253
254        ParameterBinder binder = parameterNameToBinder.get(qualifiedParameterName);
255
256        if (binder != null)
257        {
258            return binder;
259        }
260
261        // Ok, so perhaps this is a qualified name for an informal parameter of the mixin.
262
263        Instantiator instantiator = mixinIdToInstantiator.get(mixinId);
264
265        assert instantiator != null;
266
267        return bindInformalParameter(qualifiedParameterName, mixinId, parameterName, instantiator.getModel());
268    }
269
270    private ParameterBinder bindInformalParameter(String qualifiedParameterName, String mixinId, String parameterName, ComponentModel model)
271    {
272        if (model.getSupportsInformalParameters())
273        {
274            return new ParameterBinderImpl(mixinId, parameterName, null);
275        }
276
277        // Pretty sure this was not caught as an error in 5.2.
278
279        throw new TapestryException(String.format("Binding parameter %s as an informal parameter does not make sense, as %s does not support informal parameters.",
280                qualifiedParameterName, model.getComponentClassName()), location, null);
281    }
282
283    private ParameterBinder createParameterBinderForComponent(String qualifiedParameterName, String parameterName)
284    {
285        ParameterBinder binder = getComponentAssembler().getBinder(parameterName);
286
287        if (binder != null)
288        {
289            return binder;
290        }
291
292        return bindInformalParameter(qualifiedParameterName, null, parameterName, componentModel);
293    }
294
295
296    public boolean isBound(String parameterName)
297    {
298        return InternalUtils.get(bound, parameterName) != null;
299    }
300
301    public void setBound(String parameterName)
302    {
303        if (bound == null)
304            bound = CollectionFactory.newCaseInsensitiveMap();
305
306        bound.put(parameterName, true);
307    }
308
309    public int addMixinsToElement(ComponentPageElement newElement)
310    {
311        for (Map.Entry<String, Instantiator> entry : mixinIdToInstantiator.entrySet())
312        {
313            String mixinId = entry.getKey();
314            Instantiator instantiator = entry.getValue();
315
316            newElement.addMixin(mixinId, instantiator, mixinsIdToOrderConstraints.get(mixinId));
317        }
318
319        return mixinIdToInstantiator.size();
320    }
321
322    public Location getLocation()
323    {
324        return location;
325    }
326
327    public Set<String> getFormalParameterNames()
328    {
329        return new HashSet<String>(componentModel.getParameterNames());
330    }
331}