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    }