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.beanmodel.internal.beanmodel;
016
017import org.apache.tapestry5.beaneditor.RelativePosition;
018import org.apache.tapestry5.beanmodel.BeanModel;
019import org.apache.tapestry5.beanmodel.PropertyConduit;
020import org.apache.tapestry5.beanmodel.PropertyModel;
021import org.apache.tapestry5.beanmodel.internal.services.CoercingPropertyConduitWrapper;
022import org.apache.tapestry5.beanmodel.services.PropertyConduitSource;
023import org.apache.tapestry5.commons.Messages;
024import org.apache.tapestry5.commons.ObjectLocator;
025import org.apache.tapestry5.commons.internal.util.InternalCommonsUtils;
026import org.apache.tapestry5.commons.services.TypeCoercer;
027import org.apache.tapestry5.commons.util.AvailableValues;
028import org.apache.tapestry5.commons.util.CollectionFactory;
029import org.apache.tapestry5.commons.util.UnknownValueException;
030import org.apache.tapestry5.plastic.PlasticUtils;
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}