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