    001// Copyright 2007, 2008, 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.beaneditor;
    016
    017import org.apache.tapestry5.PropertyConduit;
    018import org.apache.tapestry5.beaneditor.BeanModel;
    019import org.apache.tapestry5.beaneditor.PropertyModel;
    020import org.apache.tapestry5.beaneditor.RelativePosition;
    021import org.apache.tapestry5.internal.services.CoercingPropertyConduitWrapper;
    022import org.apache.tapestry5.ioc.Messages;
    023import org.apache.tapestry5.ioc.ObjectLocator;
    024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    025import org.apache.tapestry5.ioc.internal.util.InternalCommonsUtils;
    026import org.apache.tapestry5.ioc.services.TypeCoercer;
    027import org.apache.tapestry5.ioc.util.AvailableValues;
    028import org.apache.tapestry5.ioc.util.UnknownValueException;
    029import org.apache.tapestry5.plastic.PlasticUtils;
    030import org.apache.tapestry5.services.PropertyConduitSource;
    031
    032import java.util.List;
    033import java.util.Map;
    034
    035public class BeanModelImpl<T> implements BeanModel<T>
    036{
    037    private final Class<T> beanType;
    038
    039    private final PropertyConduitSource propertyConduitSource;
    040
    041    private final TypeCoercer typeCoercer;
    042
    043    private final Messages messages;
    044
    045    private final ObjectLocator locator;
    046
    047    private final Map<String, PropertyModel> properties = CollectionFactory.newCaseInsensitiveMap();
    048
    049    // The list of property names, in desired order (generally not alphabetical order).
    050
    051    private final List<String> propertyNames = CollectionFactory.newList();
    052
    053    private static PropertyConduit NULL_PROPERTY_CONDUIT = null;
    054
    055    public BeanModelImpl(Class<T> beanType, PropertyConduitSource propertyConduitSource, TypeCoercer typeCoercer,
    056                         Messages messages, ObjectLocator locator)
    057
    058    {
    059        this.beanType = beanType;
    060        this.propertyConduitSource = propertyConduitSource;
    061        this.typeCoercer = typeCoercer;
    062        this.messages = messages;
    063        this.locator = locator;
    064    }
    065
    066    public Class<T> getBeanType()
    067    {
    068        return beanType;
    069    }
    070
    071    public T newInstance()
    072    {
    073        return locator.autobuild("Instantiating new instance of " + beanType.getName(), beanType);
    074    }
    075
    076    public PropertyModel add(String propertyName)
    077    {
    078        return addExpression(propertyName, propertyName);
    079    }
    080
    081    public PropertyModel addEmpty(String propertyName)
    082    {
    083        return add(propertyName, NULL_PROPERTY_CONDUIT);
    084    }
    085
    086    public PropertyModel addExpression(String propertyName, String expression)
    087    {
    088        PropertyConduit conduit = createConduit(expression);
    089
    090        return add(propertyName, conduit);
    091
    092    }
    093
    094    private void validateNewPropertyName(String propertyName)
    095    {
    096        assert InternalCommonsUtils.isNonBlank(propertyName);
    097        if (properties.containsKey(propertyName))
    098            throw new RuntimeException(String.format(
    099                    "Bean editor model for %s already contains a property model for property '%s'.",
    100                    beanType.getName(), propertyName));
    101    }
    102
    103    public PropertyModel add(RelativePosition position, String existingPropertyName, String propertyName,
    104                             PropertyConduit conduit)
    105    {
    106        assert position != null;
    107        validateNewPropertyName(propertyName);
    108
    109        // Locate the existing one.
    110
    111        PropertyModel existing = get(existingPropertyName);
    112
    113        // Use the case normalized property name.
    114
    115        int pos = propertyNames.indexOf(existing.getPropertyName());
    116
    117        PropertyModel newModel = new PropertyModelImpl(this, propertyName, conduit, messages);
    118
    119        properties.put(propertyName, newModel);
    120
    121        int offset = position == RelativePosition.AFTER ? 1 : 0;
    122
    123        propertyNames.add(pos + offset, propertyName);
    124
    125        return newModel;
    126    }
    127
    128    public PropertyModel add(RelativePosition position, String existingPropertyName, String propertyName)
    129    {
    130        PropertyConduit conduit = createConduit(propertyName);
    131
    132        return add(position, existingPropertyName, propertyName, conduit);
    133    }
    134
    135    public PropertyModel add(String propertyName, PropertyConduit conduit)
    136    {
    137        validateNewPropertyName(propertyName);
    138
    139        PropertyModel propertyModel = new PropertyModelImpl(this, propertyName, conduit, messages);
    140
    141        properties.put(propertyName, propertyModel);
    142
    143        // Remember the order in which the properties were added.
    144
    145        propertyNames.add(propertyName);
    146
    147        return propertyModel;
    148    }
    149
    150    private CoercingPropertyConduitWrapper createConduit(String propertyName)
    151    {
    152        return new CoercingPropertyConduitWrapper(propertyConduitSource.create(beanType, propertyName), typeCoercer);
    153    }
    154
    155    public PropertyModel get(String propertyName)
    156    {
    157        PropertyModel propertyModel = properties.get(propertyName);
    158
    159        if (propertyModel == null)
    160            throw new UnknownValueException(String.format(
    161                    "Bean editor model for %s does not contain a property named '%s'.", beanType.getName(),
    162                    propertyName), new AvailableValues("Defined properties", propertyNames));
    163
    164        return propertyModel;
    165    }
    166
    167    public PropertyModel getById(String propertyId)
    168    {
    169        for (PropertyModel model : properties.values())
    170        {
    171            if (model.getId().equalsIgnoreCase(propertyId))
    172                return model;
    173        }
    174
    175        // Not found, so we throw an exception. A bit of work to set
    176        // up the exception however.
    177
    178        List<String> ids = CollectionFactory.newList();
    179
    180        for (PropertyModel model : properties.values())
    181        {
    182            ids.add(model.getId());
    183        }
    184
    185        throw new UnknownValueException(String.format(
    186                "Bean editor model for %s does not contain a property with id '%s'.", beanType.getName(), propertyId),
    187                new AvailableValues("Defined property ids", ids));
    188    }
    189
    190    public List<String> getPropertyNames()
    191    {
    192        return CollectionFactory.newList(propertyNames);
    193    }
    194
    195    public BeanModel<T> exclude(String... propertyNames)
    196    {
    197        for (String propertyName : propertyNames)
    198        {
    199            PropertyModel model = properties.get(propertyName);
    200
    201            if (model == null)
    202                continue;
    203
    204            // De-referencing from the model is needed because the name provided may not be a
    205            // case-exact match, so we get the normalized or canonical name from the model because
    206            // that's the one in propertyNames.
    207
    208            this.propertyNames.remove(model.getPropertyName());
    209
    210            properties.remove(propertyName);
    211        }
    212
    213        return this;
    214    }
    215
    216    public BeanModel<T> reorder(String... propertyNames)
    217    {
    218        List<String> remainingPropertyNames = CollectionFactory.newList(this.propertyNames);
    219        List<String> reorderedPropertyNames = CollectionFactory.newList();
    220
    221        for (String name : propertyNames)
    222        {
    223            PropertyModel model = get(name);
    224
    225            // Get the canonical form (which may differ from name in terms of case)
    226            String canonical = model.getPropertyName();
    227
    228            reorderedPropertyNames.add(canonical);
    229
    230            remainingPropertyNames.remove(canonical);
    231        }
    232
    233        this.propertyNames.clear();
    234        this.propertyNames.addAll(reorderedPropertyNames);
    235
    236        // Any unspecified names are ordered to the end. Don't want them? Remove them instead.
    237        this.propertyNames.addAll(remainingPropertyNames);
    238
    239        return this;
    240    }
    241
    242    public BeanModel<T> include(String... propertyNames)
    243    {
    244        List<String> reorderedPropertyNames = CollectionFactory.newList();
    245        Map<String, PropertyModel> reduced = CollectionFactory.newCaseInsensitiveMap();
    246
    247        for (String name : propertyNames)
    248        {
    249
    250            PropertyModel model = get(name);
    251
    252            String canonical = model.getPropertyName();
    253
    254            reorderedPropertyNames.add(canonical);
    255            reduced.put(canonical, model);
    256
    257        }
    258
    259        this.propertyNames.clear();
    260        this.propertyNames.addAll(reorderedPropertyNames);
    261
    262        properties.clear();
    263        properties.putAll(reduced);
    264
    265        return this;
    266    }
    267
    268    @Override
    269    public String toString()
    270    {
    271        StringBuilder builder = new StringBuilder("BeanModel[");
    272        builder.append(PlasticUtils.toTypeName(beanType));
    273
    274        builder.append(" properties:");
    275        String sep = "";
    276
    277        for (String name : propertyNames)
    278        {
    279            builder.append(sep);
    280            builder.append(name);
    281
    282            sep = ", ";
    283        }
    284
    285        builder.append(']');
    286
    287        return builder.toString();
    288    }
    289}