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}