View Javadoc

1   package net.sf.stitch.crud;
2   
3   import java.lang.annotation.Annotation;
4   import java.lang.reflect.Field;
5   import java.lang.reflect.Method;
6   import java.util.Map;
7   import java.util.TreeMap;
8   
9   import javax.persistence.Basic;
10  import javax.persistence.Column;
11  import javax.persistence.EnumType;
12  import javax.persistence.Enumerated;
13  import javax.persistence.FetchType;
14  import javax.persistence.Id;
15  import javax.persistence.Lob;
16  import javax.persistence.ManyToOne;
17  import javax.persistence.OneToMany;
18  import javax.persistence.OneToOne;
19  import javax.persistence.Temporal;
20  import javax.persistence.TemporalType;
21  import javax.persistence.Version;
22  
23  import org.hibernate.validator.NotNull;
24  
25  
26  /**
27   * Represents an Entity Property.  We track the Field and the Getter and
28   * Setter Methods because annotations may be made at either the field or
29   * the getter method, and the presence of a setter tells us the field is
30   * not read-only.
31   * @author Logan Hutchinson
32   */
33  public class CrudEntityProperty
34  {
35      /** Entity property field */
36      private final Field field;
37      /** Getter method */
38      private Method getter;
39      /** Setter method */
40      private Method setter;
41  
42      public CrudEntityProperty (final Field field)
43      {
44          assert null != field;
45          this.field = field;
46      }
47  
48      public Method getGetter()
49      {
50          return getter;
51      }
52  
53      public void setGetter(final Method getter)
54      {
55          assert getter.getReturnType().equals(this.field.getType());
56          this.getter = getter;
57      }
58  
59      public Method getSetter()
60      {
61          return setter;
62      }
63  
64      public void setSetter(final Method setter)
65      {
66          this.setter = setter;
67      }
68  
69      public Field getField()
70      {
71          return field;
72      }
73  
74      /**
75       * Convenience method to get the name of the field.
76       * @return See Field.getName()
77       */
78      public String getFieldName()
79      {
80          return this.field.getName();
81      }
82  
83      /**
84       * If the property has no setter, the field is read-only.
85       * @return True if no setter is specified, False otherwise.
86       */
87      public boolean isReadOnly ()
88      {
89          return null == this.setter;
90      }
91      
92      /**
93       * Gets the annotation from the field or getter. 
94       * TODO Does field or getter have preference?
95       * @param annotationClass
96       * @return Annotation for the field, if it exists, or the getter method,
97       * if the annotation exists, otherwise null.
98       */
99      private <T extends Annotation> T getAnnotation (final Class<T> annotationClass)
100     {
101         final T annotationField = this.field.getAnnotation(annotationClass);
102         final T annotationGetter = this.getter.getAnnotation(annotationClass);
103         // If the annotation exists at both the field and getter, then they better be equal...
104         if (null != annotationField && null != annotationGetter
105                 && !annotationField.equals(annotationGetter))
106         {
107             throw new RuntimeException("Inconsistent annotation ("
108                     + annotationClass.getCanonicalName() + ") for entity property "
109                     + this.getFieldName());
110         }
111         return (null != annotationField) ? annotationField : annotationGetter;
112     }
113 
114     /**
115      * Utility method to test for annotation present at the field or getter level.
116      */
117     private boolean isAnnotationPresent (final Class<? extends Annotation> annotationClass)
118     {
119         return this.field.isAnnotationPresent(annotationClass)
120             || (null != this.getter && this.getter.isAnnotationPresent(annotationClass));
121     }
122 
123     /**
124      * Whether the field is required is determined from the type and its
125      * annotations.
126      * @return True if a null value is unacceptable, False otherwise.
127      */
128     public boolean isRequired ()
129     {
130         //  If a primitive type or enum is used, then a null value is not an option...
131         final Class<?> fieldType = this.field.getType();
132         boolean isRequired = fieldType.isPrimitive() || fieldType.isEnum();
133         if (!isRequired)
134         {
135             //  If an object is annotated with nullable or optional, then we can determine required that way...
136 
137             // Basic...
138             final Basic basicAnnotation = getAnnotation(Basic.class);
139             final boolean isOptional = (null != basicAnnotation) ? basicAnnotation.optional() : true;
140 
141             // Column...
142             final Column columnAnnotation = getAnnotation(Column.class);
143             final boolean isNullable = (null != columnAnnotation) ? columnAnnotation.nullable() : true;
144 
145             // ManyToOne...
146             final ManyToOne manyToOneAnnotation = getAnnotation(ManyToOne.class);
147             final boolean isNto1Optional = (null != manyToOneAnnotation) ? manyToOneAnnotation.optional() : true; 
148 
149             // OneToOne...
150             final OneToOne oneToOneAnnotation = getAnnotation(OneToOne.class);
151             final boolean is1to1Optional = (null != oneToOneAnnotation) ? oneToOneAnnotation.optional() : true; 
152 
153             // "Legacy" Hibernate Validator...
154             final boolean hibernateNotNull = isAnnotationPresent(NotNull.class); 
155 
156             isRequired = !isOptional || !isNullable || !isNto1Optional || !is1to1Optional
157                 || hibernateNotNull;
158         }
159 
160         return isRequired;
161     }
162     
163     /**
164      * Is the property annotated with @Id
165      * @return true if the property is annotated with @Id, false otherwise.
166      */
167     public boolean isId ()
168     {
169         return isAnnotationPresent(Id.class);
170     }
171     
172     /**
173      * Is the property annotated with @Version
174      * @return true if the property is annotated with @Version, false otherwise.
175      */
176     public boolean isVersion ()
177     {
178         return isAnnotationPresent(Version.class);
179     }
180     
181     /**
182      * Is the property annotated with @Lob
183      * @return true if the property is annotated with @Lob, false otherwise.
184      */
185     public boolean isLob ()
186     {
187         return isAnnotationPresent(Lob.class);
188     }
189     
190     /**
191      * 
192      * @return true if the property is annotated with 
193      */
194     public boolean isFetchLazy ()
195     {
196         // Basic...
197         final Basic basicAnnotation = getAnnotation(Basic.class);
198         final boolean isBasicLazy = FetchType.LAZY == ((null != basicAnnotation) ? basicAnnotation.fetch() : FetchType.EAGER);
199 
200         // ManyToOne...
201         final ManyToOne manyToOneAnnotation = getAnnotation(ManyToOne.class);
202         final boolean isNto1Lazy = FetchType.LAZY == ((null != manyToOneAnnotation) ? manyToOneAnnotation.fetch() : FetchType.LAZY); 
203 
204         // OneToOne...
205         final OneToOne oneToOneAnnotation = getAnnotation(OneToOne.class);
206         final boolean is1to1Lazy = FetchType.LAZY == ((null != oneToOneAnnotation) ? oneToOneAnnotation.fetch() : FetchType.EAGER); 
207 
208         // OneToMany...
209         final OneToMany oneToManyAnnotation = getAnnotation(OneToMany.class);
210         final boolean is1toNLazy = FetchType.LAZY == ((null != oneToManyAnnotation) ? oneToManyAnnotation.fetch() : FetchType.LAZY); 
211 
212         return isBasicLazy || isNto1Lazy || is1to1Lazy || is1toNLazy;
213     }
214 
215     /**
216      * Is the field a boolean (true/false, yes/no).
217      * @return true if the field class is a Boolean or boolean.
218      */
219     public boolean isBoolean ()
220     {
221         final Class<?> fieldType = this.field.getType();
222         return fieldType.equals(Boolean.class) || fieldType.equals(boolean.class);
223     }
224     
225     /**
226      * Is the field an enum.
227      * @return true if the field is an enum, false otherwise.
228      */
229     public boolean isEnum ()
230     {
231         return this.field.getType().isEnum();
232     }
233 
234     /**
235      * Is the property searchable from the EntityQuery.  In other words, would you want to
236      * search for matches on this property.
237      * TODO Rethink name (searchable).  Can this be expanded beyond String?
238      * @return true if the field is a String, false otherwise.
239      */
240     public boolean isSearchable ()
241     {
242         return this.field.getType().equals(String.class);
243     }
244     
245     /**
246      * Is the property listable from the EntityQuery.  In other words, would you want the
247      * field to appear in a one-line summary for the entity. 
248      * TODO Rethink name (listable).  Can this be derived differently?
249      */
250     public boolean isListable ()
251     {
252         final Class<?> fieldType = this.field.getType();
253 
254         return (fieldType.isPrimitive() || fieldType.getCanonicalName().startsWith("java.lang.")) 
255             && !isId() && !isVersion() && !isFetchLazy() && !isLob();
256     }
257 
258     /**
259      * @return The maximum length of the property.
260      */
261     public Integer getMaxLength ()
262     {
263         Integer maxLength = null;
264         final Column columnAnnotation = getAnnotation(Column.class);
265         if (this.field.getType().equals(String.class))
266         {
267             maxLength = (null != columnAnnotation && 0 != columnAnnotation.length())
268                     ? columnAnnotation.length() : 255;
269         }
270         else if (null != columnAnnotation && (0 != columnAnnotation.precision() || 0 != columnAnnotation.scale()))
271         {
272             maxLength = columnAnnotation.precision() + 1 + columnAnnotation.scale();
273         }
274         return maxLength;
275     }
276 
277     /**
278      * Input box size may be estimated based on the field type.
279      */
280     private static final Map<String,Integer> SIZE_MAP;
281     static
282     {
283         final Integer int4 = Integer.valueOf(4);
284         final Integer int6 = Integer.valueOf(6);
285         final Integer int12 = Integer.valueOf(12);
286         final Integer int20 = Integer.valueOf(20);
287 
288         SIZE_MAP = new TreeMap<String,Integer>();
289         SIZE_MAP.put(Byte.class.getName(), int4);
290         SIZE_MAP.put(Double.class.getName(), int20);
291         SIZE_MAP.put(double.class.getName(), int20);
292         SIZE_MAP.put(double.class.getName(), int20);
293         SIZE_MAP.put(Float.class.getName(), int12);
294         SIZE_MAP.put(float.class.getName(), int12);
295         SIZE_MAP.put(Integer.class.getName(), int12);
296         SIZE_MAP.put(int.class.getName(), int12);
297         SIZE_MAP.put(Long.class.getName(), int20);
298         SIZE_MAP.put(long.class.getName(), int20);
299         SIZE_MAP.put(Short.class.getName(), int6);
300         SIZE_MAP.put(short.class.getName(), int6);
301     }
302 
303     static final int DEFAULT_SIZE = 20;
304 
305     /**
306      * @return Reasonable estimate for input box size.
307      */
308     public int getSize ()
309     {
310         int size;
311         if (isAnnotationPresent(Temporal.class))
312         {
313             switch (getTemporalType())
314             {
315             case DATE:
316                 size = 10;
317                 break;
318             case TIME:
319                 size = 5;
320                 break;
321             case TIMESTAMP:
322                 size = 16;
323                 break;
324             default:
325                 assert false;
326                 size = 99;
327                 break;
328             }
329         }
330         else
331         {
332             final Integer sizeForClass = SIZE_MAP.get(this.field.getType().getName());
333             if (null != sizeForClass)
334             {
335                 size = sizeForClass.intValue();
336             }
337             else
338             {
339                 final Integer maxLength = getMaxLength();
340                 size = (null != maxLength) ? maxLength.intValue() : DEFAULT_SIZE;
341             }
342         }
343 
344         return size;
345     }
346 
347     /**
348      * Gets the resource bundle key for translating the field into a localized message. 
349      * @return Typically, package.class.field
350      */
351     public String getMessageKey ()
352     {
353         return this.field.getDeclaringClass().getName() + '.' + this.field.getName();
354     }
355 
356     /**
357      * Gets the Enumeration Type.
358      * @return EnumType for enum fields.  This is only applicable for enum properties. 
359      */
360     public EnumType getEnumType ()
361     {
362         assert isEnum();
363         final Enumerated enumeration = getAnnotation(Enumerated.class);
364         return (null != enumeration) ? enumeration.value() : EnumType.ORDINAL;
365     }
366 
367     /**
368      * Gets the Temporal Type.
369      * @return TemporalType if a @Temporal annotation is present. 
370      */
371     public TemporalType getTemporalType ()
372     {
373         final Temporal temporal = getAnnotation(Temporal.class);
374         return (null != temporal) ? temporal.value() : null;
375     }
376 
377     /**
378      * Gets the appropriate converter for the property. 
379      * @return Converter tag that can be embedded in a facelet,
380      * if applicable.  Otherwise a null is returned.
381      */
382     public String getConverter ()
383     {
384         String converter = null;
385 
386         final Class<?> fieldType = this.field.getType();
387         if (fieldType.isEnum())
388         {
389             converter = "<s:convertEnum/>";
390         }
391         else if (isAnnotationPresent(Temporal.class))
392         {
393             switch (getTemporalType())
394             {
395             case DATE:
396                 converter = "<s:convertDateTime type=\"date\" dateStyle=\"short\" pattern=\"MM/dd/yyyy\"/>";
397                 break;
398             case TIME:
399                 converter = "<s:convertDateTime type=\"time\"/>";
400                 break;
401             case TIMESTAMP:
402                 converter = "<s:convertDateTime type=\"both\" dateStyle=\"short\"/>";
403                 break;
404             default:
405                 assert false;
406                 break;
407             }
408         }
409         else if (fieldType.isPrimitive() || fieldType.getCanonicalName().startsWith("java.lang."))
410         {
411             assert null == converter;
412         }
413         else
414         {
415             converter = "<s:convertEntity/>";
416         }
417 
418         return converter;
419     }
420 }