/** Copyright (C) 2012 Delcyon, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.delcyon.capo.util; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.delcyon.capo.annotations.XmlMappedArrays; /** * @author jeremiah */ public class XMLSerializer { public static final String DEFAULT_STRING = "##default##"; public static int MAX_DEPTH = 0; private static final String CLASS_ATTRIBUTE = "class"; private String namespaceURI = null; private String prefix = null; public XMLSerializer() { } /** * this will export the class as an XML element owned by the owner document. * It will NOT append it to that document. This is DUE to the way the W3C DOM standard is written. * @param ownerDocument * @param object * @return * @throws Exception */ public static Element export(Document ownerDocument, Object object) throws Exception { XMLSerializer xmlSerializer = new XMLSerializer(); Element returnElement = ownerDocument.createElement(object.getClass().getSimpleName()); xmlSerializer.export(object, returnElement,0); return returnElement; } /** * Append the serialization of an object to an element * @param parentElement * @param object * @return * @throws Exception */ public static Element export(Element parentElement, Object object) throws Exception { XMLSerializer xmlSerializer = new XMLSerializer(); Element returnElement = parentElement.getOwnerDocument().createElement(object.getClass().getSimpleName()); xmlSerializer.export(object, returnElement,0); parentElement.appendChild(returnElement); return returnElement; } public void setNamespace(String prefix, String namespaceURI) { this.prefix = prefix; this.namespaceURI = namespaceURI; } /** * * @param rootObject * @param rootElement * @param currentDepth * @throws Exception */ public void export(Object rootObject, Element rootElement, int currentDepth) throws Exception { currentDepth++; Vector<Field> fieldVector = ReflectionUtility.getFieldVector(rootObject); //process mapped arrays XmlMappedArrays xmlMappedArrays = null; String[] mappedArrayKeyField = null; String[] mappedArrayValueField = null; Class searchClass = rootObject.getClass(); while(searchClass != null) { if (searchClass.isAnnotationPresent(XmlMappedArrays.class)) { xmlMappedArrays = (XmlMappedArrays) searchClass.getAnnotation(XmlMappedArrays.class); break; } searchClass = searchClass.getSuperclass(); } //end mapped array processing //start main field loop for (Field field : fieldVector) { if (Modifier.isTransient(field.getModifiers()) == false && Modifier.isStatic(field.getModifiers()) == false) { field.setAccessible(true); Object fieldValue = field.get(rootObject); String fieldName = field.getName(); if (fieldValue == null) { continue; } //can't deal with crazy $ char in class names, shouldn't matter any way, if (fieldName.contains("$") && Modifier.isFinal(field.getModifiers())) { continue; } //skip self referencing items if (fieldValue.equals(rootObject)) { continue; } //skip any fields used by mapped arrays, they should have been handled already if (xmlMappedArrays != null) { if (fieldName.equals(xmlMappedArrays.keys())) { mappedArrayKeyField = (String[]) field.get(rootObject); continue; } if (fieldName.equals(xmlMappedArrays.values())) { mappedArrayValueField = (String[]) field.get(rootObject); continue; } } if (field.isAnnotationPresent(XMLElement.class) == true) { XMLElement xmlElementAnnotation = field.getAnnotation(XMLElement.class); if (xmlElementAnnotation.name().equals(DEFAULT_STRING) == false) { fieldName = xmlElementAnnotation.name(); } Element newElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? fieldName : prefix+":"+fieldName); // store the content of a primitive as the text data in an // element if (ReflectionUtility.isPrimitive(fieldValue.getClass()) == true) { rootElement.appendChild(newElement); newElement.appendChild(newElement.getOwnerDocument().createTextNode(ReflectionUtility.getSerializedString(fieldValue))); } else { exportComplexType(fieldValue, rootElement, newElement, fieldName, currentDepth); } } else if (ReflectionUtility.isPrimitive(field.getType()) == true) { if (field.isAnnotationPresent(XMLAttribute.class) == true) { XMLAttribute xmlAttributeAnnotation = field.getAnnotation(XMLAttribute.class); if (xmlAttributeAnnotation.name().equals(DEFAULT_STRING) == false) { fieldName = xmlAttributeAnnotation.name(); } } if (fieldValue != null) { rootElement.setAttributeNS(namespaceURI,prefix == null ? fieldName : prefix+":"+fieldName, ReflectionUtility.getSerializedString(fieldValue)); } } else { Element newElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? fieldName : prefix+":"+fieldName); exportComplexType(fieldValue, rootElement, newElement, fieldName, currentDepth); } } } if (xmlMappedArrays != null && mappedArrayKeyField != null && mappedArrayValueField != null) { Element mappedArrayElement = (Element) rootElement.appendChild(rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? xmlMappedArrays.name() : prefix+":"+xmlMappedArrays.name())); for (int currentKey = 0 ; currentKey < mappedArrayKeyField.length; currentKey++) { if (mappedArrayValueField[currentKey] != null ) { mappedArrayElement.setAttributeNS(namespaceURI,prefix == null ? mappedArrayKeyField[currentKey] : prefix+":"+mappedArrayKeyField[currentKey], mappedArrayValueField[currentKey]); } } } } /** * @param fieldValue * the complex type to be exported * @param rootElement * the element to append it to once we figure out what to do with * it. * @param newElement * the element that will be appended * @param newElementName * the new element name * @param currentDepth * how big of a tree we've already built. * @throws Exception */ @SuppressWarnings("unchecked") private void exportComplexType(Object fieldValue, Element rootElement, Element newElement, String newElementName, int currentDepth) throws Exception { currentDepth++; if (currentDepth >= MAX_DEPTH && MAX_DEPTH > 0) { Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).log(Level.WARNING, "Reached Max Depth for XML serialization: "+MAX_DEPTH); return; } if (Element.class.isAssignableFrom(fieldValue.getClass())) { Element fieldValueElement = (Element) fieldValue; newElement.appendChild(rootElement.getOwnerDocument().importNode(fieldValueElement, true)); rootElement.appendChild(newElement); } else if (Collection.class.isAssignableFrom(fieldValue.getClass())) { exportCollection(newElementName, rootElement, (Collection) fieldValue, currentDepth); } else if (fieldValue.getClass().isArray() == true) { exportArray(newElementName, rootElement, fieldValue, currentDepth); } else if (Map.class.isAssignableFrom(fieldValue.getClass())) { exportMap(newElementName, rootElement, (Map) fieldValue, currentDepth); } else { if(newElement.getParentNode() == null) //check and see if this has already been added someplace, and if so, don't try to add it again. { rootElement.appendChild(newElement); } newElement.setAttributeNS(namespaceURI, prefix == null ? CLASS_ATTRIBUTE : prefix+":"+CLASS_ATTRIBUTE, fieldValue.getClass().getCanonicalName()); export(fieldValue, newElement, currentDepth); } } /** * This will export an array of any dimension * * @param arrayName * @param rootElement * @param arrayObject * @param currentDepth * @throws Exception */ private void exportArray(String arrayName, Element rootElement, Object arrayObject, int currentDepth) throws Exception { int length = Array.getLength(arrayObject); Vector<Object> objectVector = new Vector<Object>(length); for (int index = 0; index < length; index++) { objectVector.add(Array.get(arrayObject, index)); } exportCollection(arrayName, rootElement, objectVector, currentDepth); } @SuppressWarnings("unchecked") private void exportCollection(String collectionName, Element rootElement, Collection collection, int currentDepth) throws Exception { for (Object object : collection) { Element newElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? collectionName : prefix+":"+collectionName); rootElement.appendChild(newElement); if (object == null) { continue; } else if (ReflectionUtility.isPrimitive(object.getClass())) { newElement.appendChild(rootElement.getOwnerDocument().createTextNode(ReflectionUtility.getSerializedString(object))); } else { if (Collection.class.isAssignableFrom(object.getClass())) { exportCollection(collectionName, newElement, (Collection) object, currentDepth); } else { exportComplexType(object, rootElement, newElement, collectionName, currentDepth); } } } } /** * This will export any class that can be cast as a Map * * @param mapName * @param rootElement * @param map * @param currentDepth * @throws Exception */ @SuppressWarnings("unchecked") private void exportMap(String mapName, Element rootElement, Map map, int currentDepth) throws Exception { Set<Entry<Object, Object>> entrySet = map.entrySet(); for (Entry<Object, Object> entry : entrySet) { Element mapElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? mapName : prefix+":"+mapName); rootElement.appendChild(mapElement); Element keyElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? "key" : prefix+":"+"key"); Element valueElement = rootElement.getOwnerDocument().createElementNS(namespaceURI,prefix == null ? "value" : prefix+":"+"value"); Object value = entry.getValue(); Object key = entry.getKey(); if (ReflectionUtility.isPrimitive(key.getClass())) { mapElement.setAttributeNS(namespaceURI,prefix == null ? "key" : prefix+":"+"key", ReflectionUtility.getSerializedString(key)); } else { mapElement.appendChild(keyElement); exportComplexType(key, mapElement, keyElement, "key", currentDepth); } // TODO this will ralph on any case where the key doesn't resolve as (This might be fixed now) // a primitive or simple type if (value != null) { if (ReflectionUtility.isPrimitive(value.getClass())) { mapElement.setAttributeNS(namespaceURI,prefix == null ? "value" : prefix+":"+"value", ReflectionUtility.getSerializedString(value)); } else { mapElement.appendChild(valueElement); exportComplexType(value, mapElement, valueElement, "value", currentDepth); } } } } /** * TODO break this up for loop processing * @param rootElement * @param rootObject * @throws Exception */ @SuppressWarnings({ "unchecked", "deprecation" }) public void marshall(Element rootElement, Object rootObject) throws Exception { Vector<Field> fieldVector = ReflectionUtility.getFieldVector(rootObject); //process mapped arrays XmlMappedArrays xmlMappedArrays = null; Field mappedArrayKeyField = null; Field mappedArrayValueField = null; Class searchClass = rootObject.getClass(); while(searchClass != null) { if (searchClass.isAnnotationPresent(XmlMappedArrays.class)) { xmlMappedArrays = (XmlMappedArrays) searchClass.getAnnotation(XmlMappedArrays.class); break; } searchClass = searchClass.getSuperclass(); } //end mapped array processing for (Field field : fieldVector) { Class type = null; if (Modifier.isTransient(field.getModifiers()) == false && Modifier.isStatic(field.getModifiers()) == false) { field.setAccessible(true); //lets get the expected name for this field String fieldName = field.getName(); if (field.isAnnotationPresent(XMLAttribute.class) == true) { XMLAttribute xmlAnnotation = field.getAnnotation(XMLAttribute.class); if (xmlAnnotation.name().equals(DEFAULT_STRING) == false) { fieldName = xmlAnnotation.name(); } } else if (field.isAnnotationPresent(XMLElement.class) == true) { XMLElement xmlAnnotation = field.getAnnotation(XMLElement.class); if (xmlAnnotation.name().equals(DEFAULT_STRING) == false) { fieldName = xmlAnnotation.name(); } } //skip any fields used by mapped arrays, they should have been handled already if (xmlMappedArrays != null) { if (fieldName.equals(xmlMappedArrays.keys())) { mappedArrayKeyField = field; continue; } if (fieldName.equals(xmlMappedArrays.values())) { mappedArrayValueField = field; continue; } } //START PRIMITIVE PROCESSING //if this is a primitive type, then we can set the value right here if (ReflectionUtility.isPrimitive(field.getType())) { String fieldValueString = null; if (field.isAnnotationPresent(XMLElement.class) == true) { Vector<Element> elementVector = new Vector<Element>(); NodeList nodeList = rootElement.getChildNodes(); //this lest us ignore namespaces for (int index = 0; index < nodeList.getLength(); index++) { Node node = nodeList.item(index); if (node instanceof Element) { if (node.getLocalName().endsWith(fieldName)) { elementVector.add((Element) node); } } } if (elementVector.size() > 0) { fieldValueString = elementVector.firstElement().getTextContent(); } } else { //need a check here since getAttribute returns the empty string instead of null if (rootElement.hasAttribute(fieldName)) { fieldValueString = rootElement.getAttribute(fieldName); } } if (fieldValueString != null) { Object instanceObject = ReflectionUtility.getPrimitiveInstance(field.getType(), fieldValueString); if (instanceObject != null) { field.set(rootObject, instanceObject); } else { //if we got null, and the type is a primitive, leave it alone //but if it's an object, then set it to null; if (field.getType().isPrimitive() == false) { field.set(rootObject, instanceObject); } } } } else //this is a complex type { NodeList nodeList = rootElement.getChildNodes(); Vector<Element> elementVector = getChildElementVector(nodeList,fieldName); if (elementVector.size() > 0) { //check for Array, Collection, Map or Element if (Element.class.isAssignableFrom(field.getType())) { //TODO this will leave the element attached o the serialized document, need to detach some how //Element fieldValueElement = (Element) fieldValue; //rootElement.appendChild(rootElement.getOwnerDocument().importNode(fieldValueElement, true)); field.set(rootObject, elementVector.firstElement().getElementsByTagName("*").item(0)); } else if (Collection.class.isAssignableFrom(field.getType())) { Collection collection = (Collection) field.getType().newInstance(); field.set(rootObject, collection); //walk the nodes, and create an object foreach node //add the new object //mashall the new object Type collectionType = field.getGenericType(); marshallCollection(collection,collectionType,elementVector,fieldName); } else if (field.getType().isArray() == true) { Object arrayObject = Array.newInstance(field.getType().getComponentType(), elementVector.size()); field.set(rootObject, arrayObject); for (int elementIndex = 0; elementIndex < elementVector.size(); elementIndex++) { Element element = elementVector.get(elementIndex); if (ReflectionUtility.isPrimitive(field.getType().getComponentType()) == true) { Object instanceObject = ReflectionUtility.getPrimitiveInstance(field.getType().getComponentType(), element.getTextContent()); if (instanceObject != null) { Array.set(arrayObject, elementIndex, instanceObject); } } else { Object instanceObject = null; if(element.hasAttribute(CLASS_ATTRIBUTE)) { //skip anything we can't remarshall if (ReflectionUtility.hasDefaultContructor(Class.forName(element.getAttribute(CLASS_ATTRIBUTE)))) { instanceObject = ReflectionUtility.getComplexInstance(Class.forName(element.getAttribute(CLASS_ATTRIBUTE)));//, marnodeList.item(currentNode).getTextContent()); } else { instanceObject = ReflectionUtility.getMarshalWrapperInstance(element.getAttribute(CLASS_ATTRIBUTE)); if (instanceObject == null) { Logger.global.log(Level.WARNING, "Skipping "+element.getAttribute(CLASS_ATTRIBUTE)+" because it has no default constructor, try creating a MarshalWrapper"); } } } else { //skip anything we can't remarshall if (ReflectionUtility.hasDefaultContructor(field.getType().getComponentType())) { instanceObject = ReflectionUtility.getComplexInstance(field.getType().getComponentType());//, marnodeList.item(currentNode).getTextContent()); } else { instanceObject = ReflectionUtility.getMarshalWrapperInstance(field.getType().getComponentType().getCanonicalName()); if (instanceObject == null) { Logger.global.log(Level.WARNING, "Skipping "+field.getType().getComponentType()+" because it has no default constructor, try creating a MarshalWrapper"); } } } if (instanceObject != null) { Array.set(arrayObject, elementIndex, instanceObject); marshall(element, instanceObject); } } } } else if (Map.class.isAssignableFrom(field.getType())) { Map map = (Map) field.getType().newInstance(); field.set(rootObject, map); Type[] types = null; if (field.getGenericType() instanceof ParameterizedType) { types = ((ParameterizedType)field.getGenericType()).getActualTypeArguments(); if (types.length != 2) { //TODO we do not yet support complex generic types on maps throw new Exception("we do not yet support complex generic types on maps:"+field.getGenericType()); } else if (types[0] instanceof ParameterizedType || types[1] instanceof ParameterizedType) { //TODO we do not yet support deep generic types on maps throw new Exception("we do not yet support deep generic types on maps:"+field.getGenericType()); } } else { //TODO we do not support generic type in maps yet throw new Exception("we do not yet support generic types on maps:"+field.getGenericType()); } for (Element element : elementVector) { Object keyTypeObject = null; Object valueTypeObject = null; Element keyElement = null; Element valueElement = null; NodeList childNodeList = element.getChildNodes(); for (int currentChildNode = 0; currentChildNode < childNodeList.getLength(); currentChildNode++) { String childNodeName = childNodeList.item(currentChildNode).getNodeName(); if (childNodeName.equals("key")) { keyElement = (Element) childNodeList.item(currentChildNode); } else if (childNodeName.equals("value")) { valueElement = (Element) childNodeList.item(currentChildNode); } } if (keyElement != null) { if (ReflectionUtility.isPrimitive((Class) types[0]) == true) { keyTypeObject = ReflectionUtility.getPrimitiveInstance(types[0], keyElement.getTextContent()); } else { keyTypeObject = ReflectionUtility.getComplexInstance((Class) types[0]); marshall(keyElement, keyTypeObject); } } else if (element.hasAttributeNS(namespaceURI,prefix == null ? "key" : prefix+":"+"key")) { keyTypeObject = ReflectionUtility.getPrimitiveInstance(types[0], element.getAttributeNS(namespaceURI,prefix == null ? "key" : prefix+":"+"key")); } if (valueElement != null) { if (ReflectionUtility.isPrimitive((Class) types[1]) == true) { valueTypeObject = ReflectionUtility.getPrimitiveInstance(types[1], valueElement.getTextContent()); } else { if (valueElement.hasAttribute(CLASS_ATTRIBUTE)) { valueTypeObject = ReflectionUtility.getComplexInstance(Class.forName(valueElement.getAttribute(CLASS_ATTRIBUTE))); } else { valueTypeObject = ReflectionUtility.getComplexInstance((Class) types[1]); } marshall(valueElement, valueTypeObject); } } else if (element.hasAttributeNS(namespaceURI,prefix == null ? "value" : prefix+":"+"value")) { valueTypeObject = ReflectionUtility.getPrimitiveInstance(types[0], element.getAttributeNS(namespaceURI,prefix == null ? "value" : prefix+":"+"value")); } try { map.put(keyTypeObject, valueTypeObject); } catch (NullPointerException nullPointerException) { //not all maps support null as a key or value //but we might as well try and just skip it if there is an error } } } else { if (type == null) { type = field.getType(); } if (elementVector.firstElement().hasAttribute(CLASS_ATTRIBUTE)) { type = Class.forName(elementVector.firstElement().getAttribute(CLASS_ATTRIBUTE)); } Object instanceObject = ReflectionUtility.getComplexInstance(type); if (instanceObject != null) { field.set(rootObject, instanceObject); } marshall(elementVector.firstElement(), instanceObject); } } } } } if (xmlMappedArrays != null) { NodeList mappedArrayNodeList = rootElement.getElementsByTagNameNS(namespaceURI, prefix == null ? xmlMappedArrays.name() : prefix+":"+xmlMappedArrays.name()); if (mappedArrayNodeList.getLength() > 0) { for(int currentNode = 0; currentNode < mappedArrayNodeList.getLength(); currentNode++) { Element mappedArrayElement = (Element) mappedArrayNodeList.item(currentNode); if (mappedArrayElement.getParentNode().equals(rootElement)) { NamedNodeMap namedNodeMap = mappedArrayElement.getAttributes(); String[] keyField = new String[namedNodeMap.getLength()]; String[] valueField = new String[namedNodeMap.getLength()]; for (int currentKey = 0 ; currentKey < namedNodeMap.getLength(); currentKey++) { Attr attribute = (Attr) namedNodeMap.item(currentKey); keyField[currentKey] = attribute.getLocalName(); valueField[currentKey] = attribute.getValue(); } mappedArrayKeyField.set(rootObject, keyField); mappedArrayValueField.set(rootObject, valueField); break; } } } } } private static Vector<Element> getChildElementVector(NodeList nodeList, String fieldName) { Vector<Element> elementVector = new Vector<Element>(); //this lest us ignore namespaces for (int index = 0; index < nodeList.getLength(); index++) { Node node = nodeList.item(index); if (node instanceof Element) { String nodeName = node.getLocalName(); if (nodeName == null) { nodeName = node.getNodeName(); } if (nodeName.equals(fieldName)) { elementVector.add((Element) node); } } } return elementVector; } @SuppressWarnings("unchecked") private void marshallCollection(Collection collection, Type collectionType, Vector<Element> elementVector,String fieldName) throws Exception { for (Element element : elementVector) { if (collectionType instanceof ParameterizedType) { if (((ParameterizedType) collectionType).getActualTypeArguments().length != 1) { //TODO Don't know what to do with complex Collections yet throw new Exception("Don't know what to do with complex Collections yet"); } Type[] collectionTypes = ((ParameterizedType) collectionType).getActualTypeArguments(); if (collectionTypes[0] instanceof Class) { Object instanceObject = null; boolean isPrimitive = true; if (ReflectionUtility.isPrimitive((Class) collectionTypes[0])) { instanceObject = ReflectionUtility.getPrimitiveInstance(collectionTypes[0],element.getTextContent()); } else { isPrimitive = false; if (element.hasAttribute(CLASS_ATTRIBUTE)) { instanceObject = ReflectionUtility.getComplexInstance(Class.forName(element.getAttribute(CLASS_ATTRIBUTE))); } else { instanceObject = ReflectionUtility.getComplexInstance((Class) collectionTypes[0]); } } if (instanceObject != null) { collection.add(instanceObject); if (isPrimitive == false) { marshall(element, instanceObject); } } } //this is something else, something deeper else if (collectionTypes[0] instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) collectionTypes[0]; if (parameterizedType.getRawType() instanceof Class) { Object instanceObject = ((Class) parameterizedType.getRawType()).newInstance(); if (instanceObject != null) { collection.add(instanceObject); if (Collection.class.isAssignableFrom(instanceObject.getClass())) { Vector<Element> subElementVector = getChildElementVector(element.getChildNodes(), fieldName); marshallCollection((Collection) instanceObject, parameterizedType, subElementVector,fieldName); } else { //TODO Don't know what to do with compound generic collections who's parameterized types aren't classes or other generic collections throw new Exception("Don't know what to do with compound generic collections who's parameterized types aren't classes or other generic collections"); } } } } } else { //TODO Don't know what to do with untyped Collections yet throw new Exception("Don't know what to do with untyped Collections yet"); } } } }