package org.bifrost.xmlio;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import org.bifrost.util.text.StringHelper;
import org.bifrost.xmlio.config.Converter;
import org.bifrost.xmlio.config.ObjectMap;
import org.bifrost.xmlio.config.PropertyMap;
import org.bifrost.xmlio.config.XmlIOConfig;
/**
* <p>Serialize an xml representation of a graph of objects into a writer.</p>
* <p>
* Created: Feb 24, 2003<br/>
* Copyright: Copyright (c) 2003<br/>
* Assumptions: none<br/>
* Requires: nothing<br/>
* Required by: nothing<br/>
* Revision History:<br/>
* </p>
* <p>Example:</p>
* <pre>
* Config config = new Config();
* config.setNumber(123);
* config.setString("acb");
* XmlWriter writer = new XmlWriter("filename.xml");
* writer.setRootObject(config);
*
* Would produce an xml file that might look like:
* <Config>
* <number>123</number>
* <string>abc</string>
* </Config>
* </pre>
* <p>Conventions:<br/>
* <ul>
* <li>Xml elements representing a class will be in mixed case with the beginning of each
* word being upper case.</li>
* <li>Xml elements representing an attribute will be in mixed case with the exception of the
* very first letter which will be in lower case.</li>
* <li>Objects can contain other, non-primative objects which in turn will be reflected and
* have their attributes serialized in a hierachical, recursive fashion.</li>
* <li>Objects can have a List or Set of objects returned from a getter and that List or
* Set will be iterated through, serializing each of the List/Set elements. Note, however,
* that those elements MUST NOT be primative objects, ie you cannot have a getter called
* getString which returns a collection of String objects.</li>
* <li>An error in serialzing any object will abort writing the whole xml.</li>
* </ul>
* </p>
* @author Donald Kittle <donald@bifrost.org>
* @version 1.0
* @stereotype boundary
*/
public class XmlWriter
{
private final static String _VERSION =
"$Id: XmlWriter.java,v 1.21 2005/07/04 11:37:05 donald Exp $";
private Logger logger = Logger.getLogger(this.getClass().getName());
/**
* Exception when a null object is encountered.
*/
public static final String EX_NULL_OBJECT = "Cannot serialize a null object";
/**
* Exception when a primative object is found in a List or Set.
*/
public static final String EX_PRIMATIVE = "Cannot have a 'primative' inside a Collection";
/**
* Exception when there is an error writing to the Writer.
*/
public static final String EX_WRITING = "Error writing xml.";
/**
* Exception when there is an error calling a getter.
*/
public static final String EX_OBJECT_GRAPH = "Error reading object graph.";
/**
* Exception when an attribute is null.
*/
public static final String EX_NULL_ATTRIBUTE = "Cannot serialize a null attribute";
/**
* Exception when there is an error creating an output file.
*/
public static final String EX_FILE = "Error creating output file: ";
/**
* Exception when there is a problem flushing or closing the writer.
*/
public static final String EX_CLOSE = "Error closing writer: ";
/**
* Exception when a writer is null.
*/
public static final String EX_NULL_WRITER = "The Writer is null";
/**
* The root object read from the configuration.
*/
private Object rootObject = null;
/**
* The writer to output the xml to.
*/
private Writer writer = null;
/**
* Flag to control 'beautifying' the output.
*/
private boolean beautify = false;
/**
* A flag to indicate whether null values should be output or not.
*/
private boolean outputNulls = true;
/**
* A flag to indicate whether 'object' and property names should have dashes
* between the words (rather than camel-casing them.
*/
private boolean dashedNames = false;
/**
* Default string to use for line separators.
*/
private String breakString = System.getProperty("line.separator");
/**
* Default string to use for indentation.
*/
private String indentString = " ";
/**
* A flag indicating whether the fully qualified classname should be
* output as an element or just the classname (without the package).
* Defaults to just the classname without the package.
*/
private boolean includePackageName = false;
private XmlIOConfig config = XmlIOConfig.getInstance();
/**
* Constructor which initializes the XmlWriter with the supplied writer object.
* @param writer the writer to output to.
* @throws XmlException if there was a problem
*/
public XmlWriter(Writer writer) throws XmlException
{
if(writer == null)
throw new XmlException(EX_NULL_WRITER);
this.writer = writer;
} // end Constructor()
/**
* Constructor which creates a FileWriter from the supplied filename.
* @param filename the name of the file to write to
* @throws XmlException if there was a problem
*/
public XmlWriter(String filename) throws XmlException
{
FileWriter fileWriter;
try
{
fileWriter = new FileWriter(filename);
}
catch (IOException ioe)
{
throw new XmlException(EX_FILE + filename + ", " + ioe.toString());
}
this.writer = fileWriter;
} // end Constructor()
/**
* Gets the flag which controls 'beautifying' the output.
* @return boolean the flag controlling whether or not output should be 'beautified'
*/
public boolean getBeautify()
{
return beautify;
}
/**
* Gets the string which will be used to break each element/attribute line of the
* resulting xml if beatifying is true.
* @return String the string which will be used to break each element/attribute line
*/
public String getBreakString()
{
return breakString;
}
/**
* Gets the string which will be used to indent each element/attribute of the resulting
* xml if beatifying is true.
* @return String the string which will be used to indent each element/attribute
*/
public String getIndentString()
{
return indentString;
}
/**
* Get the flag indicating whether the fully qualified classname should be
* output as an element or just the classname (without the package).
* @return boolean whether a fully qualified classname should be output.
*/
public boolean getIncludePackageName()
{
return includePackageName;
}
/**
* Sets the flag to control 'beautifying' the output.
* @param beautify the flag controlling whether or not output should be 'beautified'
*/
public void setBeautify(boolean beautify)
{
this.beautify = beautify;
}
/**
* Sets the string which will be used to break each element/attribute line of the
* resulting xml if beatifying is true.
* @param breakString the string which will be used to break each element/attribute line
*/
public void setBreakString(String breakString)
{
this.breakString = breakString;
}
/**
* Sets the string which will be used to indent each element/attribute of the resulting
* xml if beatifying is true.
* @param indentString the string which will be used to indent each element/attribute
*/
public void setIndentString(String indentString)
{
this.indentString = indentString;
}
/**
* Set the flag indicating whether the fully qualified classname should be
* output as an element or just the classname (without the package).
* @param rootObject
* @throws XmlException
*/
public void setIncludePackageName(boolean includePackageName)
{
this.includePackageName = includePackageName;
}
/**
* Sets the root object to be serialized out and initiates the process of writing xml
* out to the writer. Any attributes that are not a primative nor an Object that
* represents a primative will be serialized as an object with 'primative' attributes.
* @param rootObject the root object of the graph that will be serialized to xml
* @throws XmlException if there was a problem
*/
public void setRootObject(Object rootObject) throws XmlException
{
if(writer == null)
throw new XmlException(EX_NULL_WRITER);
this.rootObject = rootObject;
if (breakString == null)
breakString = "";
if (indentString == null)
indentString = "";
writeObject(rootObject, writer, 0);
try
{
writer.flush();
writer.close();
}
catch (IOException ioe)
{
// Try to close again, in case it was flush that threw a fit...
try
{
writer.close();
}
catch (IOException e)
{
throw new XmlException(EX_CLOSE + e.toString());
}
}
} // end setRootObject()
/**
* Serializes an object out to the writer.
* Revisions:
* 2004-02-25: added support for NULL attribute values
* @param object the object to serialize to xml
* @param writer the Writer to output to
* @param indentLevel an integer specifying the number of 'indents' for this object
* @throws XmlException if there was a problem
*/
private void writeObject(Object object, Writer writer, int indentLevel)
throws XmlException
{
if (object == null)
throw new XmlException(EX_NULL_OBJECT);
String fqClassName = object.getClass().getName();
String className = StringHelper.classNameWithoutPackage(fqClassName);
// Look up the object via it's short name
ObjectMap oMap = config.getObjectMapByName(className);
// If not found, look for it via it's fully qualified name
if (oMap == null)
oMap = config.getObjectMapByName(fqClassName);
// If not found, create a mapping from the object's class
if (oMap == null)
oMap = config.addClassAsMappedObject(object.getClass());
if (oMap == null)
throw new XmlException ("Cannot find nor create definition for " + className);
className = oMap.getXmlName();
if (dashedNames == true)
className = StringHelper.capitalsToDashes(className);
logger.fine("Writing object " + oMap.getName() + " as <" + className + ">");
try
{
if (beautify == true)
outputIndent(indentLevel, writer);
writer.write("<");
if (includePackageName == true)
writer.write(fqClassName);
else
writer.write(className);
boolean openTagWritten = true;
boolean closeTagWritten = false;
writeProperties(writer, indentLevel, object, oMap, true);
writer.write(">");
if (beautify == true)
writer.write(breakString);
writeProperties(writer, indentLevel, object, oMap, false);
if (closeTagWritten == false)
{
if (beautify == true)
outputIndent(indentLevel, writer);
writer.write("</");
if (includePackageName == true)
writer.write(fqClassName);
else
writer.write(className);
writer.write(">");
if (beautify == true)
writer.write(breakString);
}
}
catch (IOException e)
{
throw new XmlException(EX_WRITING + e.toString());
}
catch (Exception e)
{
e.printStackTrace();
throw new XmlException(EX_OBJECT_GRAPH + e.toString());
}
} // end writeObject()
/**
* @param writer
* @param indentLevel
* @param object
* @param oMap
* @throws XmlException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private void writeProperties(Writer writer, int indentLevel,
Object object, ObjectMap oMap, boolean attributes)
throws XmlException, IllegalAccessException, InvocationTargetException
{
List properties = oMap.getPropertyMap();
for (Iterator i = properties.iterator(); i.hasNext();)
{
PropertyMap pMap = (PropertyMap)i.next();
// If there is a property map for this attribute at the 'global'
// level, use that one instead
if (config.getPropertyMapByName(pMap.getPropertyName()) != null)
pMap = config.getPropertyMapByName(pMap.getPropertyName());
if (pMap.getWriteAsAttribute() != attributes)
continue;
logger.fine("Writing object property " + pMap.getPropertyName() +
" as xml element " + pMap.getPropertyXmlName() + ".");
Method method = pMap.getGetter();
if (method == null)
{
String name = pMap.getPropertyName();
// If the setter method is undefined for this property map, then the
// property map was probably added to config programmatically or through
// a config file, rather than being generated from a class.
if (method == null && pMap != null)
{
// Guess what the setter might be
method = pMap.guessGetter(object);
// And cache the method if it's not a global property map
if (method != null)
pMap.setGetter(method);
}
if (pMap == null)
throw new XmlException("Lost property definition for " +
name);
if (method == null)
throw new XmlException("Cannot find getter method for " + name +
" in the object " + object.getClass().getName());
}
Object value = method.invoke(object, (Object[])null);
String type = "unknown";
Class typeClass = null;
if (value == null)
{
typeClass = pMap.getPropertyType();
type = typeClass.getName();
}
if (value != null)
{
type = value.getClass().getName();
typeClass = value.getClass();
// 'Correct' the type if we are dealing with a List or Set
if ((value instanceof List || value instanceof Set) &&
((Collection)value).size() > 0 &&
((Collection)value).iterator().hasNext() == true)
{
if (((Collection)value).iterator().next() != null)
{
type =
(((Collection)value).iterator().next()).getClass().getName();
typeClass = (((Collection)value).iterator().next()).getClass();
}
// Yikes, we have a collection with at least one element, but the
// first element is a NULL! We should just skip this whole
// property.
else
continue;
}
// 'Correct' the type if we are dealing with an array
else if (value.getClass().isArray() == true &&
((Object[])value).length > 0)
{
type = ((Object[])value)[0].getClass().getName();
typeClass = ((Object[])value)[0].getClass();
}
// If the properties 'actual' type is a primative, our getter will
// actually return the Object wrapper for the primative so we must
// update our PropertyMap to reflect this.
if (pMap.getPropertyType() == null ||
!type.equals(pMap.getPropertyType().getName()))
pMap.setPropertyType(typeClass);
type = pMap.getPropertyType().getName();
}
boolean isAttribute = (config.getConverter(type) != null);
if (pMap.getConverter() != null)
isAttribute = true;
if (isAttribute == false && attributes == true)
{
pMap.setWriteAsAttribute(false);
continue;
}
// If it's an object and it's null, don't write it out.
if (value == null && isAttribute == false)
continue;
// Are we supposed to output nulls or ignore them.
if (value == null && outputNulls == false)
continue;
// Is this null or a primative??
if (value == null ||
(isAttribute == true && value.getClass().isArray() == false) &&
!(value instanceof Collection))
{
writeAttribute(pMap, writer, indentLevel, value, attributes);
}
// Is this an array of primatives??
else if (isAttribute == true && value.getClass().isArray() == true)
{
Object[] array = (Object[])value;
for (int l = 0; l < array.length; l++)
{
value = array[l];
type = value.getClass().getName();
writeAttribute(pMap, writer, indentLevel, value);
}
value = array;
}
// Is this is a list of primatives (with at least one entry in the list)??
else if ((value instanceof Collection) && ((Collection)value).size() > 0 &&
((Collection)value).iterator().hasNext() == true &&
isAttribute == true)
{
Collection coll = (Collection)value;
for (Iterator inner = ((Collection)value).iterator(); inner.hasNext(); )
{
value = inner.next();
writeAttribute(pMap, writer, indentLevel, value);
}
value = coll;
}
// Is this a list of objects??
else if (value instanceof Collection)
{
for (Iterator inner = ((Collection)value).iterator(); inner.hasNext(); )
{
Object subObject = inner.next();
// if (isAttribute(subObject) == true)
// throw new XmlException(EX_PRIMATIVE);
writeObject(subObject, writer, indentLevel + 1);
}
}
// Is this an array of objects??
else if (value.getClass().isArray() == true)
{
Object[] array = (Object[])value;
for (int l = 0; l < array.length; l++)
{
value = array[l];
writeObject(value, writer, indentLevel + 1);
}
value = array;
}
// Else write an object...
else
writeObject(value, writer, indentLevel + 1);
} // end loop through all readible attributes
}
private void writeAttribute(PropertyMap pmap, Writer writer,
int indentLevel, Object value,
boolean writeAsAttribute)
throws XmlException
{
outputAttribute(pmap, value, writer, indentLevel + 1,
true, writeAsAttribute);
}
private void writeAttribute(PropertyMap pmap, Writer writer,
int indentLevel, Object value)
throws XmlException
{
outputAttribute(pmap, value, writer, indentLevel + 1);
}
private String getAttributeName(String methodName)
{
int index = 3;
if(methodName.startsWith("is"))
index = 2;
StringBuffer temp = new
StringBuffer(String.valueOf(Character.toLowerCase(methodName.charAt(index))));
index++;
if (methodName.length() > index)
temp.append(methodName.substring(index));
return temp.toString();
} // end writeAttribute()
/**
* Serializes an attribute out to the writer.
* @param name the name of the attribute to serialize to xml
* @param value the value of the attribute
* @param writer the Writer to output to
* @param indentLevel an integer specifying the number of 'indents' for this attribute
* @throws XmlException if there was a problem
*/
private void outputAttribute(PropertyMap pmap, Object value, Writer writer, int indentLevel)
throws XmlException
{
outputAttribute(pmap, value, writer, indentLevel, true, false);
} // end outputAttribute()
/**
* Serializes an attribute out to the writer.
* Revisions:
* 2004-02-25: added support for NULL attribute values
* @param name the name of the attribute to serialize to xml
* @param value the value of the attribute
* @param writer the Writer to output to
* @param indentLevel an integer specifying the number of 'indents' for this
* attribute
* @param writeOpenTag flag indicating whether the opening tag of the
* attribute should be output
* @throws XmlException if there was a problem
*/
private void outputAttribute(PropertyMap pmap, Object value,
Writer writer, int indentLevel,
boolean writeOpenTag)
throws XmlException
{
outputAttribute(pmap, value, writer, indentLevel, writeOpenTag,
false);
} // end outputAttribute
/**
* Serializes an attribute out to the writer.
* Revisions:
* 2004-02-25: added support for NULL attribute values
* @param name the name of the attribute to serialize to xml
* @param value the value of the attribute
* @param writer the Writer to output to
* @param indentLevel an integer specifying the number of 'indents' for this
* attribute
* @param writeAsAttribute flag indicating whether the the it should be
* output as an XML attribute or an XML element
* @throws XmlException if there was a problem
*/
private void outputAttribute(PropertyMap pmap, Object value,
Writer writer, int indentLevel,
boolean writeOpenTag, boolean writeAsAttribute)
throws XmlException
{
if (pmap == null || pmap.getPropertyXmlName() == null)
throw new XmlException(EX_NULL_ATTRIBUTE);
String name = pmap.getPropertyXmlName();
// Look up the alias for this attribute name and use it, if it exists
Converter converter = null;
if (pmap.getPropertyXmlName() != null)
name = pmap.getPropertyXmlName();
converter = pmap.getConverter();
if (converter == null)
{
converter = config.getConverter(pmap.getPropertyType().getName());
if (converter != null)
logger.finer("Using global converter.");
}
else
logger.finer("Using property specific converter.");
if (converter == null)
{
writeAsAttribute = false;
name = StringHelper.capitalizeFirst(name);
}
try
{
if (dashedNames == true)
name = StringHelper.capitalsToDashes(name);
if (writeOpenTag == true)
{
if (beautify == true && writeAsAttribute == false)
outputIndent(indentLevel, writer);
if (writeAsAttribute == false)
writer.write("<");
else
writer.write(" ");
writer.write(name);
if (writeAsAttribute == false)
writer.write(">");
else
writer.write("=\"");
}
if (value != null)
writer.write(printValue(converter, value));
else
writer.write("<null/>");
if (writeAsAttribute == false)
{
writer.write("</");
writer.write(name);
writer.write(">");
}
else
writer.write("\"");
if (beautify == true && writeAsAttribute == false)
writer.write(breakString);
}
catch (Exception e)
{
throw new XmlException(EX_WRITING + e.toString());
}
} // end outputAttribute()
private String printValue(Converter converter, Object value)
throws XmlException
{
if (converter == null)
throw new IllegalArgumentException("Converter cannot be null");
if (value == null)
throw new IllegalArgumentException("Value cannot be null");
return converter.print(value);
}
private void outputIndent(int indentLevel, Writer writer) throws XmlException
{
if (indentLevel == 0)
return;
StringBuffer indent = new StringBuffer(indentString);
for (int i = 0; i < indentLevel - 1; i++)
indent.append(indentString);
try
{
writer.write(indent.toString());
}
catch (IOException e)
{
throw new XmlException("Error writing xml." + e.toString());
}
} // end writeIndent
/**
* Returns true if an object is of type String.
* @return boolean true if one of those types, otherwise false
*/
private boolean isStringAttribute(Object value)
{
if (value == null)
return false;
String className = value.getClass().getName();
if (className.indexOf("java.lang.String") >= 0)
return true;
return false;
} // end isAttribute()
/**
* @return Returns the configuration.
*/
public XmlIOConfig getConfig()
{
return config;
}
/**
* @param config The new configuration.
*/
public void setConfig(XmlIOConfig config)
{
this.config = config;
}
public boolean getOutputNulls()
{
return outputNulls;
}
public void setOutputNulls(boolean outputNulls)
{
this.outputNulls = outputNulls;
}
public boolean getDashedNames()
{
return dashedNames;
}
public void setDashedNames(boolean dashedNames)
{
this.dashedNames = dashedNames;
}
} // end XmlWriter Class