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 String
s. 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 Collection
s 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.
Learn more about this topic
- The code for this article is available from
http://www.holub.com/software/ - A good description of annotations by Narayanan Jayaratchagan"Declarative Programming in Java" (ONJava.com, April 2004)
http://www.onjava.com/lpt/a/4768 - The problems of getters and setters are discussed in depth in Allen Holub's book Holub on Patterns (Apress, September, 2004; ISBN159059388X)
http://www.holub.com/goodies/patterns - Allen Holub's previous JavaWorld article "Why Getter and Setter Methods are Evil" (September 2003) also describes the problem, though in less depth
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html - Allen Holub's "More on Getters and Setters" (JavaWorld, January 2004) describes how to use the Builder design pattern to eliminate the getters and setters to build user interfaces
http://www.javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html - The Java "introspection" (sometimes called "reflection") APIs that you use to examine a class definition at runtime are described by Chuck McManis in "Take an In-Depth Look at the Java Reflection API" (JavaWorld, September 1997)
http://www.javaworld.com/javaworld/jw-09-1997/jw-09-indepth.html - For an introduction to annotations in J2SE 5.0, read Part 3 of Tarak Modi's JavaWorld series "Taming Tiger" (July 2004)
http://www.javaworld.com/javaworld/jw-07-2004/jw-0719-tiger3.html - Browse through all of Allen Holub's Java Toolbox columns
http://www.javaworld.com/columns/jw-toolbox-index.shtml - For more articles on J2SE, browse the Java 2 Platform, Standard Edition section of JavaWorld's Topical Index
http://www.javaworld.com/channel_content/jw-j2se-index.shtml?j2se1 - For more articles on persistence, browse the Persistence section of JavaWorld's Topical Index
http://www.javaworld.com/channel_content/jw-persistence-index.shtml - Browse the User Interface Design section of JavaWorld's Topical Index
http://www.javaworld.com/channel_content/jw-ui-index.shtml - For more articles on XML, browse the Java and XML section of JavaWorld's Topical Index
http://www.javaworld.com/channel_content/jw-xml-index.shtml