001 // Copyright 2006-2007 Daniel Gredler 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 net.sf.beanform.prop; 016 017 import java.beans.PropertyDescriptor; 018 import java.io.Serializable; 019 import java.lang.annotation.Annotation; 020 import java.lang.reflect.Field; 021 import java.lang.reflect.InvocationTargetException; 022 import java.util.ArrayList; 023 import java.util.Arrays; 024 import java.util.Date; 025 import java.util.List; 026 027 import net.sf.beanform.integration.IntegratorChain; 028 import net.sf.beanform.util.ReflectionUtils; 029 030 import org.apache.commons.logging.Log; 031 import org.apache.commons.logging.LogFactory; 032 import org.apache.hivemind.ApplicationRuntimeException; 033 import org.apache.hivemind.util.Defense; 034 035 /** 036 * <p>A property of a Java bean, identified by the owner class and the name of the property.</p> 037 * 038 * <p>Note that the name of the property may be recursive; for example, if the owner class is <tt>Person</tt>, 039 * and the property name is <tt>address.state.abbreviation</tt>, then this bean property represents a person's 040 * address's state's 2-letter abbreviation, and its type is <tt>java.lang.String</tt> (not <tt>Address</tt> 041 * or <tt>State</tt>).</p> 042 * 043 * @author Daniel Gredler 044 */ 045 public class BeanProperty implements Serializable { 046 047 private static final long serialVersionUID = -9064407627959060313L; 048 049 private static final Log LOG = LogFactory.getLog( BeanProperty.class ); 050 private static final Annotation[] EMPTY = new Annotation[ 0 ]; 051 052 private static final String STRING = String.class.getName(); 053 private static final String BOOLEAN = Boolean.class.getName(); 054 private static final String BOOL = boolean.class.getName(); 055 private static final String SHORT = Short.class.getName(); 056 private static final String SRT = short.class.getName(); 057 private static final String INTEGER = Integer.class.getName(); 058 private static final String INT = int.class.getName(); 059 private static final String LONG = Long.class.getName(); 060 private static final String LNG = long.class.getName(); 061 private static final String FLOAT = Float.class.getName(); 062 private static final String FLT = float.class.getName(); 063 private static final String DOUBLE = Double.class.getName(); 064 private static final String DBL = double.class.getName(); 065 private static final String DATE = Date.class.getName(); 066 private static final String BYTE_ARRAY = byte[].class.getName(); 067 068 private final static String INPUT_TEXTFIELD = "TextField"; 069 private final static String INPUT_TEXTAREA = "TextArea"; 070 private final static String INPUT_CHECKBOX = "Checkbox"; 071 private final static String INPUT_DATEPICKER = "DatePicker"; 072 private final static String INPUT_UPLOAD = "Upload"; 073 private final static String INPUT_INSERT = "Insert"; 074 075 private final static List<String> INPUTS = Arrays.asList( 076 INPUT_TEXTFIELD, 077 INPUT_TEXTAREA, 078 INPUT_CHECKBOX, 079 INPUT_DATEPICKER, 080 INPUT_UPLOAD, 081 INPUT_INSERT 082 ); 083 084 private final Class ownerClass; 085 private final String name; 086 private final String[] names; 087 private final String validators; 088 private final String input; 089 090 private transient PropertyDescriptor descriptor; 091 private transient Field field; 092 093 public BeanProperty( Class ownerClass, String name, String validators, String input ) { 094 095 Defense.notNull( ownerClass, "ownerClass" ); 096 Defense.notNull( name, "name" ); 097 098 this.ownerClass = ownerClass; 099 this.name = name; 100 this.names = name.split( "\\." ); 101 102 if( validators != null ) this.validators = validators; 103 else this.validators = IntegratorChain.getValidation( this ); 104 105 if( input != null ) this.input = input; 106 else this.input = this.inferInputFromType(); 107 108 if( (!this.usesTextField()) && (!this.usesTextArea()) && (!this.usesCheckbox()) && 109 (!this.usesDatePicker()) && (!this.usesUpload()) && (!this.usesInsert()) ) { 110 String msg = BeanPropertyMessages.unrecognizedInputType( this, INPUTS ); 111 throw new ApplicationRuntimeException( msg ); 112 } 113 } 114 115 private String inferInputFromType() { 116 if( this.isShortString() || this.isNumber() ) return INPUT_TEXTFIELD; 117 else if( this.isLongString() ) return INPUT_TEXTAREA; 118 else if( this.isBoolean() ) return INPUT_CHECKBOX; 119 else if( this.isDate() ) return INPUT_DATEPICKER; 120 else if( this.isByteArray() ) return INPUT_UPLOAD; 121 else return INPUT_INSERT; 122 } 123 124 private PropertyDescriptor getPropertyDescriptor() { 125 if( this.descriptor == null ) { 126 this.descriptor = ReflectionUtils.getPropertyDescriptor( this.ownerClass, this.names ); 127 } 128 return this.descriptor; 129 } 130 131 private Field getField() { 132 if( this.field == null ) { 133 try { 134 this.field = ReflectionUtils.getField( this.ownerClass, this.names ); 135 } 136 catch( NoSuchFieldException e ) { 137 if( LOG.isDebugEnabled() ) { 138 String msg = "The field '" + this.getName() + "' does not exist. "; 139 msg += "Perhaps the field name does not match the property name?"; 140 LOG.debug( msg ); 141 } 142 } 143 } 144 return this.field; 145 } 146 147 public Class getOwnerClass() { 148 return this.ownerClass; 149 } 150 151 public String getName() { 152 return this.name; 153 } 154 155 public String getValidators() { 156 return this.validators; 157 } 158 159 public String getInput() { 160 return this.input; 161 } 162 163 public boolean usesTextField() { 164 return INPUT_TEXTFIELD.equalsIgnoreCase( this.input ); 165 } 166 167 public boolean usesTextArea() { 168 return INPUT_TEXTAREA.equalsIgnoreCase( this.input ); 169 } 170 171 public boolean usesCheckbox() { 172 return INPUT_CHECKBOX.equalsIgnoreCase( this.input ); 173 } 174 175 public boolean usesDatePicker() { 176 return INPUT_DATEPICKER.equalsIgnoreCase( this.input ); 177 } 178 179 public boolean usesUpload() { 180 return INPUT_UPLOAD.equalsIgnoreCase( this.input ); 181 } 182 183 public boolean usesInsert() { 184 return INPUT_INSERT.equalsIgnoreCase( this.input ); 185 } 186 187 public List<Annotation> getAnnotations() { 188 PropertyDescriptor pd = this.getPropertyDescriptor(); 189 Annotation[] a1 = ( pd.getReadMethod() == null ? EMPTY : pd.getReadMethod().getAnnotations() ); 190 Annotation[] a2 = ( pd.getWriteMethod() == null ? EMPTY : pd.getWriteMethod().getAnnotations() ); 191 Annotation[] a3 = ( this.getField() == null ? EMPTY : this.getField().getAnnotations() ); 192 List<Annotation> list = new ArrayList<Annotation>(); 193 list.addAll( Arrays.asList( a1 ) ); 194 list.addAll( Arrays.asList( a2 ) ); 195 list.addAll( Arrays.asList( a3 ) ); 196 return list; 197 } 198 199 @SuppressWarnings( "unchecked" ) 200 public <T extends Annotation> T getAnnotation( Class<T> clazz ) { 201 Annotation result = null; 202 for( Annotation a : this.getAnnotations() ) { 203 if( clazz.isAssignableFrom( a.getClass() ) ) { 204 result = a; 205 break; 206 } 207 } 208 return (T) result; 209 } 210 211 public boolean isReadable() { 212 return this.getPropertyDescriptor().getReadMethod() != null; 213 } 214 215 public boolean isWriteable() { 216 return this.getPropertyDescriptor().getWriteMethod() != null; 217 } 218 219 public boolean isNullable() { 220 return IntegratorChain.isNullable( this ); 221 } 222 223 public Class getType() { 224 return this.getPropertyDescriptor().getPropertyType(); 225 } 226 227 public String getTypeName() { 228 Class type = this.getType(); 229 return ( type != null ? type.getName() : null ); 230 } 231 232 public boolean isEnum() { 233 Class type = this.getType(); 234 return ( type != null && type.isEnum() ); 235 } 236 237 public boolean isString() { 238 String typeName = this.getTypeName(); 239 return STRING.equals( typeName ); 240 } 241 242 public boolean isShortString() { 243 if( this.isString() == false ) return false; 244 Integer maxLength = IntegratorChain.getMaxLength( this ); 245 if( maxLength == null ) return true; 246 if( maxLength < 256 ) return true; 247 return false; 248 } 249 250 public boolean isLongString() { 251 return this.isString() && ! this.isShortString(); 252 } 253 254 public boolean isBoolean() { 255 String typeName = this.getTypeName(); 256 return BOOLEAN.equals( typeName ) || BOOL.equals( typeName ); 257 } 258 259 public boolean isShort() { 260 String typeName = this.getTypeName(); 261 return SHORT.equals( typeName ) || SRT.equals( typeName ); 262 } 263 264 public boolean isInteger() { 265 String typeName = this.getTypeName(); 266 return INTEGER.equals( typeName ) || INT.equals( typeName ); 267 } 268 269 public boolean isLong() { 270 String typeName = this.getTypeName(); 271 return LONG.equals( typeName ) || LNG.equals( typeName ); 272 } 273 274 public boolean isFloat() { 275 String typeName = this.getTypeName(); 276 return FLOAT.equals( typeName ) || FLT.equals( typeName ); 277 } 278 279 public boolean isDouble() { 280 String typeName = this.getTypeName(); 281 return DOUBLE.equals( typeName ) || DBL.equals( typeName ); 282 } 283 284 public boolean isNumber() { 285 return this.isShort() || this.isInteger() || this.isLong() || this.isFloat() || this.isDouble(); 286 } 287 288 public boolean isDate() { 289 String typeName = this.getTypeName(); 290 return DATE.equals( typeName ); 291 } 292 293 public boolean isByteArray() { 294 String typeName = this.getTypeName(); 295 return BYTE_ARRAY.equals( typeName ); 296 } 297 298 public boolean isEditableType() { 299 return this.isString() || this.isBoolean() || this.isNumber() || this.isDate() || this.isByteArray(); 300 } 301 302 @Override 303 public String toString() { 304 return this.name + 305 ( this.input != null ? "=" + this.input : "" ) + 306 ( this.validators != null ? "{" + this.validators + "}" : "" ); 307 } 308 309 /** 310 * Compares two <tt>BeanProperty</tt> instances based on the same fields that {@link #hashCode()} 311 * uses: <tt>ownerClass</tt>, <tt>name</tt>, <tt>validators</tt> and <tt>input</tt>. 312 */ 313 @Override 314 public boolean equals( Object o ) { 315 if( o instanceof BeanProperty == false ) return false; 316 BeanProperty prop = (BeanProperty) o; 317 boolean c = ( this.ownerClass == null ? prop.ownerClass == null : this.ownerClass.equals( prop.ownerClass ) ); 318 boolean n = ( this.name == null ? prop.name == null : this.name.equals( prop.name ) ); 319 boolean v = ( this.validators == null ? prop.validators == null : this.validators.equals( prop.validators ) ); 320 boolean i = ( this.input == null ? prop.input == null : this.input.equals( prop.input ) ); 321 return c && n && v && i; 322 } 323 324 /** 325 * Builds a hashCode based on the same fields that {@link #equals(Object)} uses: <tt>ownerClass</tt>, 326 * <tt>name</tt>, <tt>validators</tt> and <tt>input</tt>. 327 * 328 * @see http://jakarta.apache.org/commons/lang/xref/org/apache/commons/lang/builder/HashCodeBuilder.html 329 */ 330 @Override 331 public int hashCode() { 332 int result = 17; 333 result = ( this.ownerClass != null ? 37 * result + this.ownerClass.hashCode() : result * 37 ); 334 result = ( this.name != null ? 37 * result + this.name.hashCode() : result * 37 ); 335 result = ( this.validators != null ? 37 * result + this.validators.hashCode() : result * 37 ); 336 result = ( this.input != null ? 37 * result + this.input.hashCode() : result * 37 ); 337 return result; 338 } 339 340 /* -------------------------------------------------------------------------------------- */ 341 /* ------ Allow getting and setting the value of this property on a specific bean. ------ */ 342 /* -------------------------------------------------------------------------------------- */ 343 344 private transient Object bean; 345 346 public void setBean( Object bean ) { 347 if( bean == null ) { 348 LOG.warn( BeanPropertyMessages.setBeanNullBean() ); 349 } 350 this.bean = bean; 351 } 352 353 public Object getValue() { 354 if( this.bean == null ) { 355 String msg = BeanPropertyMessages.getValueNullBean(); 356 throw new ApplicationRuntimeException( msg ); 357 } 358 if( this.isReadable() == false ) { 359 String msg = BeanPropertyMessages.getValueNotReadable( this ); 360 throw new ApplicationRuntimeException( msg ); 361 } 362 try { 363 return ReflectionUtils.getPropertyValue( this.bean, this.names ); 364 } 365 catch( IllegalAccessException iae ) { 366 throw new ApplicationRuntimeException( iae ); 367 } 368 catch( InvocationTargetException ite ) { 369 throw new ApplicationRuntimeException( ite ); 370 } 371 finally { 372 this.bean = null; 373 } 374 } 375 376 public void setValue( Object value ) { 377 if( this.bean == null ) { 378 String msg = BeanPropertyMessages.setValueNullBean(); 379 throw new ApplicationRuntimeException( msg ); 380 } 381 if( this.isWriteable() == false ) { 382 String msg = BeanPropertyMessages.setValueNotWriteable( this ); 383 throw new ApplicationRuntimeException( msg ); 384 } 385 try { 386 ReflectionUtils.setPropertyValue( value, this.bean, this.names ); 387 } 388 catch( IllegalAccessException iae ) { 389 throw new ApplicationRuntimeException( iae ); 390 } 391 catch( InvocationTargetException ite ) { 392 throw new ApplicationRuntimeException( ite ); 393 } 394 catch( InstantiationException ie ) { 395 throw new ApplicationRuntimeException( ie ); 396 } 397 catch( IllegalArgumentException iae ) { 398 String msg = BeanPropertyMessages.setValueIllegalArgument( value ); 399 throw new ApplicationRuntimeException( msg, iae ); 400 } 401 finally { 402 this.bean = null; 403 } 404 } 405 406 }