An annotation-based persistence framework

Use J2SE 5.0 annotations to eliminate getters and setters

1 2 3 Page 3
Page 3 of 3
  1  package com.holub.persist;
   2  
   3  import java.lang.reflect.*;
   4  import java.io.*;
   5  import java.util.*;
   6  
   7  /** A very-simple persistent writer. Writes in XML format.
   8   *  Cyclic object graphs are handled by ignoring the
   9   *  cyclic field.  For example, consider a Company that has references
  10   *  to its Employees, which have references to the Company.
  11   *  Exporting the Company will export all the Employees, but
  12   *  none of these Employees will contain a <Company>...</Company>
  13   *  element. Exporting an Employee will cause the Company (and
  14   *  all of it's employees except the original one) to be exported.
  15   *  Note that this second situation is best avoided by not
  16   *  marking the Employee's "Company" field as @Persistent.
  17   */
  18  public class XmlExporter
  19  {
  20      private PrintWriter         out;
  21      private int                 indentLevel     = -1;
  22      private boolean             writeClassNames = false;
  23      private Collection<Object>  amVisiting      = new ArrayList<Object>();
  24  
  25      /** Create an XmlEporter.
  26       * 
  27       * @param out send the XML output to this writer.
  28       * @param writeClassNames if true, then synthesized element names
  29       *      will have a "className=" attribute.
  30       */
  31      public XmlExporter( PrintWriter out, boolean writeClassNames )
  32      {   this.out = out;
  33          this.writeClassNames = writeClassNames;
  34      }
  35      
  36      /** Convenenience constructor, class names are written.
  37       * @see #XmlExporter(PrintWriter,boolean)
  38       */
  39      public XmlExporter( PrintWriter out )
  40      {   this( out, true );
  41      }
  42  
  43      public void flush( Object obj ) throws IOException
  44      {   flush(obj, null, null );
  45      }
  46      /**
  47       * Flush out a single object that's represented as a class (not
  48       * a primitive). The fields of the object are flushed
  49       * recursively. The output takes the form:
  50       * <PRE>
  51       * <eleName className="..." name="..." >
  52       *      ...
  53       * </eleName>
  54       * 
  55       * The element name typically defaults to the class name. However,
  56       * if the object is annotated and has a name= attribute (or
  57       * an unnamed value), then that attribute specifies the
  58       * element name. The className attribute is generated if
  59       * this XmlExporter was created with constructor's 
  60       * writeClassNames argument set to true.
  61       * It holds the fully-qualified class name.
  62       * 
  63       * @param obj           The object to export. If the object is a Collection or
  64       *                      Map, then the elements are exported as if they had
  65       *                      been passed to flush(Object) one at a time.
  66       * 
  67       * @param preferedElementName   if not null, and the object is not annotated, then
  68       *                      use this string instead of the class name for the
  69       *                      element name.
  70       * @param objName       if not null, a name= attribute is
  71       *                      included in the generated element, and this
  72       *                      parameter determines the value. 
  73       * @throws IOException
  74       */
  75      public void flush( Object obj, String nameAttribute, String fallbackElementName ) throws IOException
  76      {   ++indentLevel;
  77      
  78          if( amVisiting.contains(obj) )  // Cyclic object graph.
  79              return;                     // Silently ignore. cycles.
  80          
  81          amVisiting.add(obj);
  82          
  83          if( obj instanceof Map )
  84          {   for( Iterator i = ((Map)obj).keySet().iterator(); i.hasNext(); )
  85              {   Object element = i.next();
  86                  flush( ((Map)obj).get(element), element.toString(), null );
  87              }
  88          }
  89          else if( obj instanceof Collection )
  90          {   for( Iterator i = ((Collection)obj).iterator(); i.hasNext(); )
  91                  flush( i.next() );
  92          }
  93          else
  94          {   Exportable annotation =
  95                      obj.getClass().getAnnotation( Exportable.class );
  96          
  97              if( fallbackElementName == null )
  98                  fallbackElementName = extractNameFromClass( obj );
  99              
 100              String elementName = fallbackElementName;
 101              
 102              if( annotation != null )
 103              {   
 104                  if( annotation.description().length() > 0 )
 105                      out.println("<!-- " + annotation.description() + " -->");
 106                  
 107                  elementName = annotation.value();
 108                  if( elementName.length() == 0 )     // If it's not specified in the value= attribute,
 109                      elementName = annotation.name();    //      check the name= attribute.
 110                  if( elementName.length() == 0 )     // It's not in the name= attribute either.
 111                      elementName = fallbackElementName;
 112              }
 113              
 114              out.println(
 115                      indent()
 116                      + "<"
 117                      + elementName 
 118                      + ( !writeClassNames ? " " :
 119                               (" className=\"" + obj.getClass().getName()+"\" ") )
 120                      + (nameAttribute == null ? " " :
 121                               (" name=\"" + nameAttribute +"\" ") )
 122                      + ">" );
 123              
 124              if( annotation != null )    // If the object is exportable,
 125                  flushFields( obj );     // then process its fields.
 126              else    
 127                  out.println( indent() + "\t" + obj.toString() );
 128              
 129              out.println( indent() + "</" + elementName + ">");
 130          }
 131          
 132          amVisiting.remove(obj);
 133          --indentLevel;
 134      }
 135      
 136      private void flushFields( Object obj ) throws IOException
 137      {   try
 138          {   Field[] fields = obj.getClass().getDeclaredFields();
 139              for( Field f : fields )
 140              {   
 141                  Persistent annotation = f.getAnnotation( Persistent.class );
 142                  if( annotation == null )
 143                      continue;
 144                  
 145                  f.setAccessible(true); // Make private fields accessible.
 146                  
 147                  String value = null;
 148                  Class  type = f.getType();
 149                  if(type == byte.class  ) value=Byte.toString    ( f.getByte(obj) );
 150                  if(type == short.class ) value=Short.toString   ( f.getShort(obj) );
 151                  if(type == char.class  ) value=Character.toString( f.getChar(obj) );
 152                  if(type == int.class   ) value=Integer.toString ( f.getInt(obj) );
 153                  if(type == long.class  ) value=Long.toString    ( f.getLong(obj) );
 154                  if(type == float.class ) value=Float.toString   ( f.getFloat(obj) );
 155                  if(type == double.class) value=Double.toString  ( f.getDouble(obj) );
 156                  if(type == String.class) value= (String)        ( f.get(obj) );
 157                  
 158                  // If an element name is specified in the annotation, use it.
 159                  // Otherwise, use the field name as the element name.
 160                  String name = annotation.value(); 
 161                  if( name.length() == 0 )
 162                      name = f.getName();
 163                  
 164                  if(value != null)   // Then it's a primitive type or a String.
 165                  {   out.println (indent() + "\t<" + name + ">");
 166                      out.println( indent() + "\t\t" + value );
 167                      out.println( indent() + "\t</" + name + ">");
 168                  }
 169                  else if(  f.get(obj) instanceof Collection 
 170                          || f.get(obj) instanceof Map )
 171                  {   out.println (indent() + "\t<" + name + ">");
 172                      flush( f.get(obj), f.getName(), null );
 173                      out.println( indent() + "\t</" + name + ">");
 174                  }
 175                  else
 176                  {   
 177                      // The following if statement will kick out a type-safety warning
 178                      // from the compiler. Since we don't actually know the type (so
 179                      // can't cast it appropriately), there's no way to eliminate
 180                      // this warning.
 181                  
 182                      if( type.getAnnotation(Exportable.class) != null )
 183                          flush( f.get(obj), f.getName(), null );
 184                      else
 185                          flush( f.get(obj), null, name );
 186                  }
 187              }
 188          }
 189          catch( IllegalAccessException e )   // Shouldn't happen
 190          {   assert false : "Unexpected exception:\n" + e ;
 191          }
 192      }
 193      
 194      /** Get the class name from the prefix. If the fully-qualified name
 195       *  contains a $, assume it's an inner class and the class name is
 196       *  everything to the right of the rightmost $. Otherwise, if the
 197       *  fully qualified name has a dot, then the class name is everything
 198       *  to the right of the rightmost dot. Otherwise, the name is the
 199       *  string returned from getClass().getName().
 200       *  
 201       * @param obj
 202       * @return
 203       */
 204      private String extractNameFromClass( Object obj )
 205      {   String name = obj.getClass().getName();
 206          int index;
 207          if( (index = name.lastIndexOf('$')) != -1 )
 208              return name.substring( index + 1 );
 209          
 210          if( (index = name.lastIndexOf('.')) != -1 )
 211              return name.substring( index + 1 );
 212          
 213          return name;
 214      }
 215      private static final String indents[] = new String[]
 216         {
 217              /* 00 */    "",
 218              /* 01 */    "\t",
 219              /* 02 */    "\t\t",
 220              /* 03 */    "\t\t\t",
 221              /* 04 */    "\t\t\t\t",
 222              /* 05 */    "\t\t\t\t\t",
 223              /* 06 */    "\t\t\t\t\t\t",
 224              /* 07 */    "\t\t\t\t\t\t\t",
 225              /* 08 */    "\t\t\t\t\t\t\t\t",
 226              /* 09 */    "\t\t\t\t\t\t\t\t\t",
 227              /* 10 */    "\t\t\t\t\t\t\t\t\t\t",
 228              /* 11 */    "\t\t\t\t\t\t\t\t\t\t\t",
 229              /* 12 */    "\t\t\t\t\t\t\t\t\t\t\t\t",
 230              /* 13 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t",
 231              /* 14 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
 232              /* 15 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
 233         };
 234                                                         
 235      private final String indent()
 236      {   return indents[ indentLevel %16 ];
 237      }
 238  }
 239  

The first interesting method is flush() (on Line 75). This method is the top-level method called to flush an entire object to the output stream in XML format. The flush() method is also called recursively to flush any fields that aren't primitive types or Strings. The method's first few lines set an indent level (so the XML will look pretty). Cycles in the object graph are handled by checking if the current object is in the amVisiting Collection. If it is, then this instance of flush() is called recursively, and some previous call is in the process of visiting the object passed to the current call. The method just returns in this situation.

The next bit of code (starting on Line 83) handles the case of the current object being a Map or Collection. The individual elements of the aggregate are each processed in turn, as if they had been passed to flush() one at a time. Map and Collections are handled differently only in that a name=... attribute that holds the key's value is passed to the recursive flush() call.

Finally, we get to the code that handles the annotation. You get an annotation from the class object by asking for it (on Line 94):

 Exportable annotation = obj.getClass().getAnnotation( Exportable.class );

Note that I've asked for a particular annotation by specifying its class to getAnnotation(), and the return type is automatically cast into the correct interface type using the magic of generics. In the Javadoc, this method's signature is hideously confusing if you're not used to the generics syntax:

 public <A extends Annotation> A getAnnotation(Class<A> annotationClass)

This declaration says that Class A (the return type) must extend Annotation (which all annotations do by magic) and that same type is the generic type of the argument to the method. In practice, all this means is that you don't have to cast the return value, but...geeez!...I hate this syntax.

You use the object returned from getAnnotation() as if it were a reference to an interface. For example, given the following annotated source code:

 @Exportable(name="Fred")
//...

I can get the string value associated with the name= argument in the following code:

 Exportable annotation = obj.getClass().getAnnotation( Exportable.class );
//...
annotation.name(); // Returns "Fred"

Also note how the code that starts on Line 107 of Listing 5 explicitly makes the value() argument supply a default value for the name= argument. The code checks for a default value first, then uses the name if the value isn't there. This defaulting-to-the-unnamed-argument process must be done manually in your code. There's no way to specify the behavior in the annotation's @interface declaration.

The

flushFields()

method (on Line 136 of Listing 5) works much like the

flush()

method, at least with respect to annotation processing. I get the fields using

Class

's

getDeclaredFields()

method. I tell the system to ignore the access privilege with the

setAccessible(true)

call, so I can process the

private

fields. The

flushFields()

method then outputs elements representing primitive-type or

String

arguments. It calls

flush()

recursively to print objects of any class other than

String

, including

Collection

or

Map

fields.

Conclusion

This article presents a pretty simple example of annotation, but it effectively demonstrates how to use an annotation at runtime to eliminate the getter/setter-based tagging idiom. All of the introspection-related classes (like Class, Method, and Field) have a getAnnotation() method that you can use to access the annotations. As I said at the beginning of this article, this is just one part of the puzzle, because annotations are often best used at the compiler, not the runtime level. I'll talk about how to do that in a future article.

Allen Holub has worked in the computer industry since 1979 on everything from operating systems, to compilers, to Web applications. He is a consultant, providing mentoring and training in object-oriented design and Java development, technical due diligence, design reviews, and he writes programs on occasion. He served as a chief technology officer at NetReliance and sits on the board of advisors for Ascenium and Ontometrics. Holub has authored nine books (including Holub on Patterns: Learning Design Patterns by Looking at Code, Taming Java Threads, and Compiler Design in C) and more than 100 magazine articles. He has been a contributing editor for JavaWorld since 1998 and for SD Times since 2004.

Learn more about this topic

1 2 3 Page 3
Page 3 of 3