    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}