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 }