package com.skaringa.javaxml.serializers;
import java.io.PrintStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.xml.sax.Attributes;
import com.skaringa.javaxml.DeserializerException;
import com.skaringa.javaxml.PropertyKeys;
import com.skaringa.javaxml.SerializerException;
import com.skaringa.javaxml.handler.AttrImpl;
import com.skaringa.javaxml.handler.DocumentOutputHandlerInterface;
import com.skaringa.javaxml.handler.sax.ObjectDeserializerHolder;
import com.skaringa.javaxml.impl.NSConstants;
import com.skaringa.javaxml.impl.PropertyHelper;
import com.skaringa.util.Log;
/**
* Implementation of ComponentSerializer for complex objects.
*/
public final class ObjectSerializer extends AbstractSerializer {
private String _xmlTypeName;
private String _javaClassName;
private static final String INNER_CLASS_TAG_XML = "._i.";
private static final String INNER_CLASS_TAG_JAVA = "$";
/**
* Construct an ObjectSerializer for a concrete type.
*
* @param xmlTypeName
* The XML type name.
*/
ObjectSerializer(String xmlTypeName) {
_xmlTypeName = fixXMLTypeNameForInnerClass(xmlTypeName).intern();
_javaClassName = fixJavaTypeNameForInnerClass(_xmlTypeName).intern();
}
/**
* Correct the xml type name of inner classes because '$' isn't a valid XML
* NCNameChar.
*
* @param javaTypeName
* The Java type name.
* @return The XML type name with all occurences of '$' replaced by '._i.'.
*/
public static String fixXMLTypeNameForInnerClass(String javaTypeName) {
return subst(javaTypeName, INNER_CLASS_TAG_JAVA, INNER_CLASS_TAG_XML);
}
/**
* Correct the java type name of inner classes. It does the reverse of
* {@link ObjectSerializer#fixXMLTypeNameForInnerClass(String)}
*
* @param xmlTypeName
* The XML type name.
* @return The Java type name with all occurences of '._i.' replaced by '$'.
*/
public static String fixJavaTypeNameForInnerClass(String xmlTypeName) {
return subst(xmlTypeName, INNER_CLASS_TAG_XML, INNER_CLASS_TAG_JAVA);
}
/**
* @see ComponentSerializer#serialize(Object, Class, String, Map, Map,
* DocumentOutputHandlerInterface)
*/
public void serialize(Object obj, Class type, String name, Map propertyMap,
Map objectIdMap, DocumentOutputHandlerInterface output)
throws SerializerException {
boolean omitXsi = PropertyHelper.parseBoolean(propertyMap,
PropertyKeys.OMIT_XSI_TYPE);
boolean omitNil = PropertyHelper.parseBoolean(propertyMap,
PropertyKeys.OMIT_XSI_NIL);
boolean omitId = PropertyHelper.parseBoolean(propertyMap,
PropertyKeys.OMIT_ID);
boolean isIdRef = false;
AttrImpl attrs = new AttrImpl();
if (obj != null) {
if (omitId) {
// need to call always writeReplace
// if object ids and references can't be written
obj = writeReplaceObject(obj);
} else {
// check if this object was already serialized
Integer id = (Integer) objectIdMap.get(obj);
if (id != null) {
// yes, provide only the reference of the object
// that was already serialized
isIdRef = true;
} else {
// this is the first time this objects is going to be serialized
// provide an id for this object
id = new Integer(objectIdMap.size());
objectIdMap.put(obj, id);
obj = writeReplaceObject(obj);
}
attrs.addAttribute(isIdRef ? "reference" : "id", new StringBuffer("i")
.append(id).toString());
}
}
if (!omitXsi) {
attrs.addAttribute(NSConstants.SCHEMA_INSTANCE_NS_NAME,
NSConstants.SCHEMA_INSTANCE_NS_PREFIX, "type", _xmlTypeName);
}
if (obj == null && !omitNil) {
attrs.addAttribute(NSConstants.SCHEMA_INSTANCE_NS_NAME,
NSConstants.SCHEMA_INSTANCE_NS_PREFIX, "nil", "true");
}
output.startElement(name, attrs);
if (obj != null && !isIdRef) {
inspectObject(obj, obj.getClass(), propertyMap, objectIdMap, output);
}
output.endElement(name);
}
/**
* @see ComponentSerializer#getXMLTypeName()
*/
public String getXMLTypeName() {
return _xmlTypeName;
}
/**
* @see ComponentSerializer#writeXMLTypeDefinition(Class, Map,
* DocumentOutputHandlerInterface)
*/
public void writeXMLTypeDefinition(Class type, Map propertyMap,
DocumentOutputHandlerInterface output) throws SerializerException {
AttrImpl attrs = new AttrImpl();
attrs.addAttribute("name", _xmlTypeName);
output.startElement("xsd:complexType", attrs);
processExtensionType(type, propertyMap, output);
output.endElement("xsd:complexType");
}
/**
* @see ComponentSerializer#startDeserialize(String, Attributes, Object,
* Stack, ClassLoader)
*/
public Object startDeserialize(String name, Attributes attrs, Object parent,
Stack objHolderStack, ClassLoader classLoader)
throws DeserializerException {
Object obj = null;
String nullAttr = attrs.getValue("xsi:nil");
try {
if (nullAttr == null || nullAttr.equals("false")) {
Class c = Class.forName(_javaClassName, true, classLoader);
Class outer = c.getDeclaringClass();
if (outer == null || Modifier.isStatic(c.getModifiers())) {
// top level or static inner class
Constructor ctor = c.getDeclaredConstructor((Class[]) null);
ctor.setAccessible(true);
obj = ctor.newInstance((Object[]) null);
} else {
// non-static inner class
Constructor ctor = c.getDeclaredConstructor(new Class[] { outer });
ctor.setAccessible(true);
Object declaringObject = findFirstObject(outer, objHolderStack);
obj = ctor.newInstance(new Object[] { declaringObject });
}
}
} catch (InstantiationException e) {
throw new DeserializerException("can't instantiate class: "
+ _javaClassName);
} catch (ClassNotFoundException e) {
throw new DeserializerException("class not found: " + _javaClassName);
} catch (IllegalAccessException e) {
throw new DeserializerException("can't access default ctor of class: "
+ _javaClassName);
} catch (NoSuchMethodException e) {
throw new DeserializerException("no default ctor found for class: "
+ _javaClassName);
} catch (java.lang.reflect.InvocationTargetException e) {
throw new DeserializerException("exception in initializer: "
+ e.getMessage());
} catch (java.lang.IllegalArgumentException e) {
throw new DeserializerException(
"IllegalArgument exception deserializing " + _javaClassName
+ ", parent " + parent.getClass().getName() + ": "
+ e.getMessage());
}
return obj;
}
/**
* @see ComponentSerializer#endDeserialize(Object, String)
*/
public Object endDeserialize(Object obj, String text)
throws DeserializerException {
if (obj != null) {
obj = readResolveObject(obj);
}
return obj;
}
/**
* @see ComponentSerializer#setMember(Object, String, Object)
*/
public void setMember(Object parent, String name, Object value)
throws DeserializerException, NoSuchFieldException {
if (name == null) {
throw new DeserializerException("member of object must have an name");
}
try {
Field field = getField(parent, name);
int modifiers = field.getModifiers();
if (!Modifier.isFinal(modifiers)) { // avoid setting of final fields
if (Log.isDebugEnabled()) {
Log.debug("set " + name + " = " + value);
}
// set the accessibility to public
field.setAccessible(true);
// set the field
field.set(parent, value);
}
} catch (IllegalAccessException e) {
throw new DeserializerException("Setting " + name + " to "
+ value.getClass().getName() + "(" + value + ")"
+ " threw IllegalAccessException");
} catch (IllegalArgumentException e) {
throw new DeserializerException("Setting " + name + " to "
+ value.getClass().getName() + "(" + value + ")"
+ " threw IllegalArgumentException");
}
}
/**
* Inspect and serialize an object.
*
* @param obj
* The object to inspect.
* @param c
* The class of the object.
* @param propertyMap
* The output properties.
* @param objectIdMap
* The objects and their ids that were already serialized.
* @param output
* The handler used to write the output.
* @throws SerializerException
* If the inspection of the object failes. If the result can't be
* written to the output.
*/
private void inspectObject(Object obj, Class c, Map propertyMap,
Map objectIdMap, DocumentOutputHandlerInterface output)
throws SerializerException {
// inspect members of super class first
if (!c.equals(java.lang.Object.class)) {
inspectObject(obj, c.getSuperclass(), propertyMap, objectIdMap, output);
}
// get all fields declared in class
Field[] fields = getFieldsToSerialize(c, getFieldComparator(c, propertyMap));
// set the accessibility to public
AccessibleObject.setAccessible(fields, true);
// inspect each field
for (int i = 0; i < fields.length; ++i) {
String fieldName = fields[i].getName();
Object member;
try {
// get the value of the field
member = fields[i].get(obj);
} catch (IllegalAccessException e) {
throw new SerializerException("can't access element " + fieldName
+ " of class " + c.getName());
}
Class fieldType = (member == null) ? fields[i].getType() : member
.getClass();
// serialize member
ComponentSerializer ser = SerializerRegistry.getInstance().getSerializer(
fieldType);
ser.serialize(member, fieldType, fieldName, propertyMap, objectIdMap,
output);
}
}
/**
* Inspect and serialize an object to JSON.
* @param obj The object to inspect.
* @param c The class of the object.
* @param propertyMap The output properties.
* @param output The handler used to write the output.
* @throws SerializerException If the inspection of the object failes.
* If the result can't be written to the output.
*/
private int inspectObject(
Object obj,
Class c,
Map propertyMap,
PrintStream output)
throws SerializerException {
int fieldCountParent = 0;
// inspect members of super class first
if (!c.equals(java.lang.Object.class)) {
fieldCountParent = inspectObject(obj, c.getSuperclass(), propertyMap, output);
}
// get all fields declared in class
Field[] fields =
getFieldsToSerialize(c, getFieldComparator(c, propertyMap));
// set the accessibility to public
AccessibleObject.setAccessible(fields, true);
if (fields.length > 0 && fieldCountParent > 0) {
output.print(",");
}
// inspect each field
for (int i = 0; i < fields.length; ++i) {
String fieldName = fields[i].getName();
Object member;
try {
// get the value of the field
member = fields[i].get(obj);
}
catch (IllegalAccessException e) {
throw new SerializerException(
"can't access element " + fieldName + " of class " + c.getName());
}
Class fieldType =
(member == null) ? fields[i].getType() : member.getClass();
output.print('"');
printEncodedStr(fieldName, output);
output.print("\":");
// serialize member
ComponentSerializer ser =
SerializerRegistry.getInstance().getSerializer(fieldType);
ser.toJson(
member,
fieldType,
propertyMap,
output);
if (i < fields.length - 1) {
output.print(",");
}
}
return fields.length;
}
/**
* Inspect the super types of a class and write according XML extension
* definitions.
*
* @param type
* The type to inspect.
* @param propertyMap
* A map (string-to-object) of properties.
* @param output
* The handler used to write the output.
* @throws SerializerException
* If the definition can't be written.
*/
private void processExtensionType(Class type, Map propertyMap,
DocumentOutputHandlerInterface output) throws SerializerException {
Class superclass = type.getSuperclass();
if (superclass != null) {
output.startElement("xsd:complexContent");
ComponentSerializer ser = SerializerRegistry.getInstance().getSerializer(
superclass);
AttrImpl attrs = new AttrImpl();
attrs.addAttribute("base", ser.getXMLTypeName());
output.startElement("xsd:extension", attrs);
processFieldTypes(type, propertyMap, output);
output.endElement("xsd:extension");
output.endElement("xsd:complexContent");
} else {
// it must be java.lang.Object!
// provide the attributes id and reference
AttrImpl attrs = new AttrImpl();
attrs.addAttribute("name", "id");
attrs.addAttribute("type", "xsd:ID");
output.startElement("xsd:attribute", attrs);
output.endElement("xsd:attribute");
attrs = new AttrImpl();
attrs.addAttribute("name", "reference");
attrs.addAttribute("type", "xsd:IDREF");
output.startElement("xsd:attribute", attrs);
output.endElement("xsd:attribute");
}
}
/**
* Inspect all fields of a class and write the according element definitions.
*
* @param type
* The class to inspect.
* @param propertyMap
* A map (string-to-object) of properties.
* @param output
* The handler used to write the output.
* @throws SerializerException
* If the definition can't be written.
*/
private void processFieldTypes(Class type, Map propertyMap,
DocumentOutputHandlerInterface output) throws SerializerException {
// get all fields declared in type
Field[] fields = getFieldsToSerialize(type, getFieldComparator(type,
propertyMap));
if (fields.length > 0) {
output.startElement("xsd:sequence");
// inspect each field
for (int i = 0; i < fields.length; ++i) {
String fieldName = fields[i].getName();
Class fieldType = fields[i].getType();
// serialize member
ComponentSerializer ser = SerializerRegistry.getInstance()
.getSerializer(fieldType);
AttrImpl attrs = new AttrImpl();
attrs.addAttribute("name", fieldName);
attrs.addAttribute("type", ser.getXMLTypeName());
attrs.addAttribute("nillable", "true");
attrs.addAttribute("minOccurs", "0");
output.startElement("xsd:element", attrs);
output.endElement("xsd:element");
}
output.endElement("xsd:sequence");
}
}
/**
* Call the skaReadResolve method of obj if it has one.
*
* @param obj
* The object to pass to skaReadResolve.
* @return The object returned by skaReadResolve or obj itself if no
* skaReadResolve method exists.
* @throws DeserializerException
* If the invocation of skaReadResolve produces an exception.
*/
private Object readResolveObject(Object obj) throws DeserializerException {
final String methodName = "skaReadResolve";
try {
Method activate = obj.getClass().getMethod(methodName, new Class[0]);
if (Log.isDebugEnabled()) {
Log
.debug("about to call: " + methodName + " of class: "
+ _xmlTypeName);
}
return activate.invoke(obj, new Object[0]);
} catch (NoSuchMethodException e) {
return obj;
} catch (IllegalAccessException e) {
throw new DeserializerException("can't access method " + methodName
+ " of class: " + _xmlTypeName);
} catch (InvocationTargetException e) {
throw new DeserializerException("exception in " + methodName + ": "
+ e.getMessage());
}
}
/**
* Call the skaWriteReplace method of obj if one exists.
*
* @param obj
* The object to pass to skaWriteReplace.
* @return The object returned by skaWriteReplace or obj itself if no
* skaWriteReplace method exists.
* @throws SerializerException
* If the invocation of skaWriteReplace produces an exception.
*/
private Object writeReplaceObject(Object obj) throws SerializerException {
final String methodName = "skaWriteReplace";
try {
Method activate = obj.getClass().getMethod(methodName, new Class[0]);
if (Log.isDebugEnabled()) {
Log
.debug("about to call: " + methodName + " of class: "
+ _xmlTypeName);
}
return activate.invoke(obj, new Object[0]);
} catch (NoSuchMethodException e) {
return obj;
} catch (IllegalAccessException e) {
throw new SerializerException("can't access method " + methodName
+ " of class: " + _xmlTypeName);
} catch (InvocationTargetException e) {
throw new SerializerException("exception in " + methodName + ": "
+ e.getMessage());
}
}
/**
* Get a comparator to order the fields in a certain order.
*
* @param type
* The class which fields should be ordered.
* @param propertyMap
* The properties of the transformer.
* @return The comparator used to compare fields.
* @throws SerializerException
* If the field skaFieldOrder exists but has the wrong signature.
*/
private Comparator getFieldComparator(Class type, Map propertyMap)
throws SerializerException {
Comparator comparator = getSkaFieldOrderComparator(type);
if (comparator == null) {
comparator = getLexicalFieldOrderComparator(propertyMap);
}
return comparator;
}
/**
* Check if a class has a static field String[] skaFieldOrder. If yes, then
* construct a comparator from it.
*
* @param type
* The class to check.
* @return The comparator or null if no such field exists.
* @throws SerializerException
* If the field exists but has the wrong type and/or modifiers.
*/
private Comparator getSkaFieldOrderComparator(Class type)
throws SerializerException {
final String fieldName = "skaFieldOrder";
try {
Field field = type.getDeclaredField(fieldName);
field.setAccessible(true);
if (!String[].class.equals(field.getType())) {
throw new SerializerException(fieldName + " of class: "
+ type.getName() + " must be of type String[]");
}
return new SkaFieldOrderComparator((String[]) field.get(null));
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new SerializerException("can't access field " + fieldName
+ " of class: " + type.getName());
} catch (NullPointerException e) {
throw new SerializerException(fieldName + " of class: " + type.getName()
+ " must be static");
}
}
/**
* Check if the property SORT_FIELDS is set in the property map. If yes, then
* construct a LexicographicalFieldOrderComparator.
*
* @param propertyMap
* The property map.
* @return The comparator or null if the property is not set.
*/
private Comparator getLexicalFieldOrderComparator(Map propertyMap) {
Comparator comp = null;
if (PropertyHelper.parseBoolean(propertyMap, PropertyKeys.SORT_FIELDS)) {
comp = new LexicographicalFieldOrderComparator();
}
return comp;
}
/**
* Find the first object of type clazz in objStack.
*
* @param clazz
* The type to search.
* @param objHolderStack
* The stack of holder objects.
* @return The first object of type clazz in objStack.
* @throws DeserializerException
* If no object of type clazz is in objStack.
*/
private static Object findFirstObject(Class clazz, Stack objHolderStack)
throws DeserializerException {
for (int i = objHolderStack.size() - 1; i >= 0; --i) {
Object obj = ((ObjectDeserializerHolder) objHolderStack.elementAt(i))
.getObj();
if (clazz.isInstance(obj)) {
return obj;
}
}
throw new DeserializerException("Object of class " + clazz.getName()
+ " not found in object hierarchy.");
}
/**
* @see ComponentSerializer#addUsedClasses(Class, Set)
*/
public void addUsedClasses(Class base, Set usedClasses)
throws SerializerException {
if (usedClasses.contains(base)) {
return; // already evaluated
}
SerializerRegistry reg = SerializerRegistry.getInstance();
// the class itself
usedClasses.add(base);
// fields
Field[] fields = AbstractSerializer.getFieldsToSerialize(base, null);
for (int i = 0; i < fields.length; i++) {
Class type = fields[i].getType();
ComponentSerializer ser = reg.getSerializer(type);
ser.addUsedClasses(type, usedClasses);
}
// base classes
base = base.getSuperclass();
while (base != null) {
ComponentSerializer ser = reg.getSerializer(base);
ser.addUsedClasses(base, usedClasses);
base = base.getSuperclass();
}
}
/**
* @see CollectionSerializer#toJson(Object, Class, Map, PrintStream)
*/
public void toJson(Object obj, Class type, Map propertyMap,
PrintStream output) throws SerializerException {
if (obj != null) {
// need to call always writeReplace
obj = writeReplaceObject(obj);
}
if (obj != null) {
output.print("{");
inspectObject(obj, obj.getClass(), propertyMap, output);
output.print("}");
} else {
output.print("null");
}
}
}