    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
    015    package org.apache.tapestry5.internal.beaneditor;
    016
    017    import org.apache.tapestry5.PropertyConduit;
    018    import org.apache.tapestry5.beaneditor.BeanModel;
    019    import org.apache.tapestry5.beaneditor.PropertyModel;
    020    import org.apache.tapestry5.beaneditor.RelativePosition;
    021    import org.apache.tapestry5.internal.services.CoercingPropertyConduitWrapper;
    022    import org.apache.tapestry5.ioc.Messages;
    023    import org.apache.tapestry5.ioc.ObjectLocator;
    024    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    025    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    026    import org.apache.tapestry5.ioc.services.TypeCoercer;
    027    import org.apache.tapestry5.ioc.util.AvailableValues;
    028    import org.apache.tapestry5.ioc.util.UnknownValueException;
    029    import org.apache.tapestry5.plastic.PlasticUtils;
    030    import org.apache.tapestry5.services.PropertyConduitSource;
    031
    032    import java.util.List;
    033    import java.util.Map;
    034
    035    public 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 InternalUtils.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    }