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 }