|
The Java Specialists' Newsletter
Issue 097 2004-10-17
Category:
Language
Java version: Sun JDK 1.5.0-b64 Mapping Objects to XML Files using Java 5 Annotationsby Amotz Anner
Welcome to the 97th edition of The Java(tm) Specialists' Newsletter. We had a beautiful day
down here in Cape Town, South Africa. This morning I was
concerned because it was cloudy, but fortunately it cleared
up for our braai. In South Africa, a popular pass-time is to
make a fire with wood, then once this has burnt down, to
grill some lamb chops, sausage or chicken on the coals. We
only braai once the coals are completely burnt, to avoid
having flames leap up and burn the meat. The coals have to
be so hot that you cannot hold your hand at the level of the
meat for more than eight seconds. Some wood holds its
heat longer, and you must make sure that the flavour of the
meat is not adversely affected by the wood you choose.
The braai is more for the social interaction than for the food
and so it is common to start the fire only after the guests
have arrived. Another interesting tradition is to invite
guests for a braai, but ask them to bring their own meat and
drinks, and maybe a salad. We call that a bring-and-braai.
The company is very important, and although we sometimes
braai on our own, we usually prefer to have lots of happy
friendly faces around. For example, today we were 17!
So, if you visit South Africa, and tell me when you will be
in Cape Town, you will probably be invited to a "braai". We
will probably ask you to bring some of the food, and when you
arrive, I will still be chopping the wood, but besides that,
it is a lot of fun, and you should accept the invitation :)
I find it amazing
how quickly some people pick up a new technology. Amotz
Anner sent me an example of how you can use annotations to
write some very interesting code. Then Amotz did me a huge
favour and wrote this newsletter. Thank you very very much.
Amotz is the Founder of X.M.L. Systems Ltd
located on:
46, Jerusalem St. Flat 9B
Kfar-Saba 44369
Israel
Cell: +972 (54) 686-0707
That was not the only interesting communication with Amotz.
He discovered that in the JDK 1.5.0, if you compile code that
uses the ternary if-else operator, with one side of the
results null then it can deliver
wrong results if you immediately assign this to a
final local variable. This compiler
bug has been fixed and will be available in the next release.
For more details, please look at our posts on JavaLobby
and TheServerSide.
This bug does concern me. With most Java bugs that I have
seen, upgrading to a new JRE solves the problem. With this
bug, you need to upgrade AND recompile all your sources.
It is easy for this bug to make it into your production code
library, and unless you have a very good test procedure, you
might never pick it up. So, be warned!
Enough from me, over to Amotz...
Would you like to really understand Java concurrency? Join us for an
in-depth study of how threading works in Java. During the course,
you will learn how to write correct and fast multi-threaded Java code.
Please
click here if you would like to learn more. Mapping Objects to XML Files using Java 5 Annotations
The following is an example of using the new annotation
capability of Java 5.0 to extend the expressiveness of Java.
My code relies on XML to declare all sort of components, and
then have Java classes construct themselves from those
declarations. It is NOT a persistence framework, for the
following reason:
I consider the XML declaration to be primary, and the Java
class to be secondary, a mere tool to realize the declaratory
intent of the XML document. In contrast, in a persistence
framework, the Java class is primary and the XML document is
just a vehicle used to contain persistence data, and has no
standing in and of itself.
So my requirement is that the Java class adapts itself to the
XML declaration rather than the other way around. I also did
not want to use any external IDL-type definitions to match
Java classes to XML.
Prior to annotations, I was severely limited in my choices.
What I could, and did do, was to use reflection to look for
all public fields of a class whose names match those of an
XML attribute in the appropriate declaration, and initialize
those fields from the attribute value. This was a fragile
solution, since there was no clear indication as to the
special status of the names of those fields, and all to often
I broke my code by changing field names, thus breaking the
connection to the XML declaration.
Then came annotations and solved all my problems in one fell
swoop. With them I can:
- Clearly indicate which fields are initialized from an XML
declaration.
- Dissolve the field name - attribute name bond.
- Extend usage from just XML attributes to XML elements as
well.
- Supply centrally/locally defined default values.
So how is that magic achieved? In four easy steps, of course.
Step 1: Annotation is defined
First, an annotation is defined, in the same way as an
interface would be, as follows:
import java.lang.annotation.*;
/**
* Make annotation available at run time and only allow fields to
* be modified by this annotation.
*
* We have 3 properties, defaults for all
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FromXml {
/**
* Normally, the field's value is taken from an attribute with
* an identical name. xPath can be used to specify a different
* source, breaking the name linkage
*/
String xPath() default "";
/**
* A default value to be used if field source is not found in
* the XML (we call it "dflt" since "default" is reserved)
*/
String dflt() default "";
/**
* Flag to trim fetched data.
*/
boolean trim() default true;
}
Step 2: Fields are Annotated
Second, fields have to be annotated. Syntactically,
annotations are modifiers, so the result looks like:
// This is equivalent to old usage.
@FromXml public int a;
// This is the new usage.
@FromXml(xPath = "a/b/c", dflt = "blue") public String color;
Step 3: Call initializer
The third easy step is to call the initializer with a Class
and Element references:
XmlConstructor.constructFromXml(this, elem, false);
A more complete example is our ComponenSlider class that
manages a slider that can be configured using XML and
annotations. You need the dom4j jar file to get
this to compile.
import org.dom4j.Element;
import javax.swing.*;
public class ComponentSlider {
@FromXml private boolean inverted = false;
@FromXml private int min = Integer.MIN_VALUE;
@FromXml private int max = Integer.MIN_VALUE;
@FromXml private int minorTickInterval = Integer.MIN_VALUE;
@FromXml private int majorTickInterval = Integer.MIN_VALUE;
@FromXml private boolean snapToTick = false;
public ComponentSlider(JSlider slider, Element def) {
XmlConstructor.constructFromXml(this, def);
slider.setMinimum(min);
slider.setMaximum(max);
slider.setInverted(inverted);
if (minorTickInterval != Integer.MIN_VALUE) {
slider.setMinorTickSpacing(minorTickInterval);
slider.setPaintTicks(true);
slider.setSnapToTicks(snapToTick);
}
if (majorTickInterval != Integer.MIN_VALUE) {
slider.setMajorTickSpacing(majorTickInterval);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setSnapToTicks(snapToTick);
}
}
}
Step 4: Supply constructFromXml method (once)
And finally, but just once, the above method has to be
supplied. It looks like:
import org.dom4j.*;
import java.lang.reflect.Field;
public class XmlConstructor {
public static void constructFromXml(Object obj, Element elem) {
constructFromXml(obj, elem, false);
}
/**
* Set object's fields from values of XML declarations, using
* "@FromXml" annotation
*
* Scans all fields in an object for a annotated elements, and
* initialize them, according to the fields type.
*
* @param useSuper If super class is to be processed
* @param o The object to scan for fields
* @param element The element whose attributes are the data
* sources.
*/
public static void constructFromXml(Object o, Element element,
boolean useSuper) {
if (element == null) {
return;
}
Class aClass = getAppropriateClass(o, useSuper);
for (Field field : aClass.getDeclaredFields()) {
FromXml fromXml = field.getAnnotation(FromXml.class);
if (fromXml != null) {
field.setAccessible(true);
handleAnnotatedField(fromXml, element, field, o);
}
}
}
private static void handleAnnotatedField(FromXml fromXml,
Element element,
Field field, Object o) {
String value = getValue(fromXml.xPath(), element, field, fromXml);
if (!isEmpty(value)) {
if (fromXml.trim()) {
value = value.trim();
}
setField(field, o, value);
}
}
private static String getValue(String xPath, Element element,
Field field, FromXml fromXml) {
String value = null;
if (!isEmpty(xPath)) {
Node node = element.selectSingleNode(xPath);
if (node != null) {
value = node.getText();
}
} else {
// If no xPath, use name
// Get value of matching attribute
value = element.attributeValue(field.getName());
}
if (value == null) {
//Use default
value = fromXml.dflt();
}
return value;
}
private static void setField(Field field, Object o, String value) {
Class type = field.getType();
try {
// Now initialize field according to type
if (type.equals(int.class)) {
field.setInt(o, asHexInt(value));
} else if (type.equals(String.class)) {
field.set(o, value.intern());
} else if (type.equals(double.class)) {
field.setDouble(o, Double.parseDouble(value));
} else if (type.equals(boolean.class)) {
field.setBoolean(o, asBoolean(value));
} else if (type.equals(char.class)) {
field.setChar(o, value.charAt(0));
}
} catch (IllegalAccessException ex) {
// this should not happen, since we are setting the access
// to true
throw new RuntimeException(ex);
}
}
private static Class getAppropriateClass(Object o, boolean useSuper) {
Class aClass = o.getClass();
if (useSuper) {
aClass = aClass.getSuperclass();
}
return aClass;
}
private static boolean isEmpty(String test) {
return test == null || test.length() == 0;
}
/**
* Use hex conversion if string starts with "0x", else decimal
* conversion.
*/
private static int asHexInt(String value) {
if (value.toLowerCase().startsWith("0x")) {
return Integer.parseInt(value.substring(2), 16);
}
return Integer.parseInt(value);
}
private static boolean asBoolean(String option) {
if (!isEmpty(option)) {
String opt = option.toLowerCase();
return "yes".equals(opt) || "on".equals(opt)
|| "true".equals(opt) || "1".equals(opt);
}
return false;
}
}
Very simple, really.
Next, we create an XML file that contains the attributes for
the Slider class:
<?xml version="1.0" encoding="UTF-8"?>
<Slider xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xsi:noNamespaceSchemaLocation='file:/C:/XML%20SYS/XML%20Sys/XSD/Components.xsd'
min="20" max="180" minorTickInterval="2" majorTickInterval="10">
<xmlPath>checkup/weight</xmlPath>
</Slider>
And an example class that uses the ComponentSlider:
import org.dom4j.*;
import org.dom4j.io.SAXReader;
import javax.swing.*;
import java.io.*;
public class AnnotationDemo extends JFrame {
private JSlider slider = new JSlider();
public AnnotationDemo(Element sliderDef) {
new ComponentSlider(slider, sliderDef);
// HK: did you notice that you don't have to say:
// getContentPane().add(...) in JDK 1.5 anymore?
add(slider);
}
private static Element loadSliderXMLFile(String filename)
throws FileNotFoundException, DocumentException {
// The slider definition as XML
Element sliderDef = null;
// A reusable SAX parser
SAXReader xmlReader = new SAXReader();
xmlReader.setIgnoreComments(true);
xmlReader.setMergeAdjacentText(true);
xmlReader.setStripWhitespaceText(true);
File file = new File(filename);
if (file.exists() && file.canRead()) {
Document doc = xmlReader.read(new FileInputStream(file));
sliderDef = doc.getRootElement();
}
if (sliderDef == null) {
throw new IllegalArgumentException(
"Could not find XML declaration");
}
return sliderDef;
}
public static void main(String args[]) throws Exception {
Element sliderDef = loadSliderXMLFile(args[0]);
AnnotationDemo frame = new AnnotationDemo(sliderDef);
frame.setSize(500, 100);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null); // center the frame
frame.setVisible(true);
}
}
I have not done it yet, but I can now initialize
multi-dimensional arrays, lists, collections or what-not
by straightforward extensions of the above code. Obviously,
it made no sense before annotations, as XML attributes appear
at most once. But with elements, there is no such limitation,
and there is nothing to prevent database queries and/or other
trickery.
Left as an exercise to the reader, is another annotation
which would be used to associate "slider" directly with
"weightSlider.xml", making annotation usage consistent, well
documented and safe.
Have fun.
Amotz
P.S. I (Heinz) refactored Amotz's code a bit, so if you find
bugs or do not like the style of the code, blame me :)
Language Articles
Related Java Course
Discuss at The Java Specialist Club
|