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    }