/* ================================================================== * JavaBeanXmlSerializer.java - Sep 6, 2011 9:00:16 PM * * Copyright 2007-2011 SolarNetwork.net Dev Team * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== * $Id$ * ================================================================== */ package net.solarnetwork.util; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Deque; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * PropertySerializer that serializes JavaBean objects into XML strings. * * <p> * This class can also be used in a stand-alone manner, calling the public * methods exposed for generating XML from JavaBean objects. * </p> * * <p> * The configurable properties of this class are: * </p> * * <dl> * <dt>propertySerializerRegistrar</dt> * <dd>An optional registrar of PropertySerializer instances that can be used to * serialize specific objects into String values. This can be useful for * formatting Date objects into strings, for example.</dd> * </dl> * * @author matt * @version 1.0 */ public class JavaBeanXmlSerializer implements PropertySerializer { private static final ThreadLocal<SimpleDateFormat> SDF = new ThreadLocal<SimpleDateFormat>(); private final String rootElementName = "root"; private final boolean singleBeanAsRoot = true; private final boolean useModelTimeZoneForDates = true; private final String modelKey = null; private final Set<String> classNamesAllowedForNesting = null; private final PropertySerializerRegistrar propertySerializerRegistrar = null; private final Logger logger = LoggerFactory.getLogger(getClass()); @Override public Object serialize(Object data, String propertyName, Object propertyValue) { setupDateFormat(null); ByteArrayOutputStream out = new ByteArrayOutputStream(); XMLStreamWriter writer = startXml(out); try { outputObject(propertyValue, propertyName, writer); } catch ( XMLStreamException e ) { throw new RuntimeException(e); } finally { endXml(writer); } return out.toString(); } /** * Render a JavaBean object as XML serialized to a given OutputStream. * * @param bean * the object to serialize as XML * @param out * the OutputStream to write the XML to */ public void renderBean(Object bean, OutputStream out) { setupDateFormat(null); XMLStreamWriter writer = startXml(out); try { outputObject(bean, null, writer); } catch ( XMLStreamException e ) { throw new RuntimeException(e); } finally { endXml(writer); } } /** * Render a Map as XML serialized to a given OutputStream. * * @param model * the data to serialize as XML * @param out * the OutputStream to write the XML to */ public void renderMap(Map<String, ?> model, OutputStream out) { Map<String, Object> finalModel = setupDateFormat(model); XMLStreamWriter writer = startXml(out); try { Object singleBean = finalModel.size() == 1 && this.singleBeanAsRoot ? finalModel.values() .iterator().next() : null; if ( singleBean != null ) { outputObject(singleBean, finalModel.keySet().iterator().next().toString(), writer); } else { writeElement(this.rootElementName, null, writer, false); for ( Map.Entry<String, Object> me : finalModel.entrySet() ) { outputObject(me.getValue(), me.getKey(), writer); } // end root element writer.writeEndElement(); } } catch ( XMLStreamException e ) { throw new RuntimeException(e); } finally { endXml(writer); } } private XMLStreamWriter startXml(OutputStream out) { XMLStreamWriter writer = null; try { writer = XMLOutputFactory.newFactory().createXMLStreamWriter(out); } catch ( XMLStreamException e ) { throw new RuntimeException(e); } catch ( FactoryConfigurationError e ) { throw new RuntimeException(e); } return writer; } private void endXml(XMLStreamWriter writer) { try { writer.writeEndDocument(); writer.flush(); } catch ( XMLStreamException e ) { // ignore this } finally { SDF.remove(); } } /** * Create a {@link SimpleDateFormat} and cache on the {@link #SDF} * ThreadLocal to re-use for all dates within a single response. * * @param model * the model, to look for a TimeZone to format the dates in */ private Map<String, Object> setupDateFormat(Map<String, ?> model) { TimeZone tz = TimeZone.getTimeZone("GMT"); Map<String, Object> result = null; if ( model != null ) { result = new LinkedHashMap<String, Object>(); for ( Map.Entry<String, ?> me : model.entrySet() ) { Object o = me.getValue(); if ( useModelTimeZoneForDates && o instanceof TimeZone ) { tz = (TimeZone) o; } else if ( modelKey != null ) { if ( modelKey.equals(me.getKey()) ) { result.put(modelKey, o); } } else { result.put(me.getKey(), o); } } } SimpleDateFormat sdf = new SimpleDateFormat(); if ( tz.getRawOffset() == 0 ) { sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); } else { sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); } sdf.setTimeZone(tz); if ( logger.isTraceEnabled() ) { logger.trace("TZ offset " + tz.getRawOffset()); } SDF.set(sdf); return result; } private void outputObject(Object o, String name, XMLStreamWriter out) throws XMLStreamException { if ( o instanceof Collection ) { Collection<?> col = (Collection<?>) o; outputCollection(col, name, out); } else if ( o instanceof Map ) { Map<?, ?> map = (Map<?, ?>) o; outputMap(map, name, out); } else if ( o instanceof String || o instanceof Number ) { // for simple types, write as unified <value type="String" value="foo"/> // this happens often in collections / maps of simple data types Map<String, Object> params = new LinkedHashMap<String, Object>(2); params.put("type", org.springframework.util.ClassUtils.getShortName(o.getClass())); params.put("value", o); writeElement("value", params, out, true); } else { String elementName = (o == null ? name : org.springframework.util.ClassUtils.getShortName(o .getClass())); writeElement(elementName, o, out, true); } } private void outputMap(Map<?, ?> map, String name, XMLStreamWriter out) throws XMLStreamException { writeElement(name, null, out, false); // for each entry, write an <entry> element for ( Map.Entry<?, ?> me : map.entrySet() ) { String entryName = me.getKey().toString(); out.writeStartElement("entry"); out.writeAttribute("key", entryName); Object value = me.getValue(); if ( value instanceof Collection ) { // special collection case, we don't add nested element for ( Object o : (Collection<?>) value ) { outputObject(o, "value", out); } } else { outputObject(value, null, out); } out.writeEndElement(); } out.writeEndElement(); } private void outputCollection(Collection<?> col, String name, XMLStreamWriter out) throws XMLStreamException { writeElement(name, null, out, false); for ( Object o : col ) { outputObject(o, null, out); } out.writeEndElement(); } private void writeElement(String name, Map<?, ?> props, XMLStreamWriter out, boolean close) throws XMLStreamException { out.writeStartElement(name); Map<String, Object> nested = null; if ( props != null ) { for ( Map.Entry<?, ?> me : props.entrySet() ) { String key = me.getKey().toString(); Object val = me.getValue(); if ( propertySerializerRegistrar != null ) { val = propertySerializerRegistrar .serializeProperty(name, val.getClass(), props, val); } if ( val instanceof Date ) { SimpleDateFormat sdf = SDF.get(); // SimpleDateFormat has no way to create xs:dateTime with tz, // so use trick here to insert required colon for non GMT dates Date date = (Date) val; StringBuilder buf = new StringBuilder(sdf.format(date)); if ( buf.charAt(buf.length() - 1) != 'Z' ) { buf.insert(buf.length() - 2, ':'); } val = buf.toString(); } else if ( val instanceof Collection ) { if ( nested == null ) { nested = new LinkedHashMap<String, Object>(5); } nested.put(key, val); val = null; } else if ( val instanceof Map<?, ?> ) { if ( nested == null ) { nested = new LinkedHashMap<String, Object>(5); } nested.put(key, val); val = null; } else if ( classNamesAllowedForNesting != null && !(val instanceof Enum<?>) ) { for ( String prefix : classNamesAllowedForNesting ) { if ( val.getClass().getName().startsWith(prefix) ) { if ( nested == null ) { nested = new LinkedHashMap<String, Object>(5); } nested.put(key, val); val = null; break; } } } if ( val != null ) { String attVal = val.toString(); out.writeAttribute(key, attVal); } } } if ( nested != null ) { for ( Map.Entry<String, Object> me : nested.entrySet() ) { outputObject(me.getValue(), me.getKey(), out); } if ( close ) { out.writeEndElement(); } } } private void writeElement(String name, Object bean, XMLStreamWriter out, boolean close) throws XMLStreamException { if ( propertySerializerRegistrar != null && bean != null ) { // try whole-bean serialization first Object o = propertySerializerRegistrar.serializeProperty(name, bean.getClass(), bean, bean); if ( o != bean ) { if ( o != null ) { outputObject(o, name, out); } return; } } Map<String, Object> props = ClassUtils.getBeanProperties(bean, null, true); writeElement(name, props, out, close); } /** * Parse XML into a simple Map structure. * * @param in * the input stream to parse * @return a Map of the XML */ public Map<String, Object> parseXml(InputStream in) { Deque<Map<String, Object>> stack = new LinkedList<Map<String, Object>>(); Map<String, Object> result = null; XMLStreamReader reader = startParse(in); try { int eventType; boolean parsing = true; while ( parsing ) { eventType = reader.next(); switch (eventType) { case XMLStreamConstants.END_DOCUMENT: parsing = false; break; case XMLStreamConstants.START_ELEMENT: String name = reader.getLocalName(); if ( stack.isEmpty() ) { result = new LinkedHashMap<String, Object>(); stack.push(result); } else { Map<String, Object> el = new LinkedHashMap<String, Object>(); putMapValue(stack.peek(), name, el); stack.push(el); } parseElement(stack.peek(), reader); break; case XMLStreamConstants.END_ELEMENT: stack.pop(); break; } } } catch ( XMLStreamException e ) { throw new RuntimeException(e); } finally { endParse(reader); } return result; } private void parseElement(Map<String, Object> result, XMLStreamReader reader) { int attrCount = reader.getAttributeCount(); for ( int i = 0; i < attrCount; i++ ) { String name = reader.getAttributeLocalName(i); String val = reader.getAttributeValue(i); putMapValue(result, name, val); } } @SuppressWarnings("unchecked") private void putMapValue(Map<String, Object> result, String name, Object val) { if ( result.containsKey(name) ) { Object existingVal = result.get(name); if ( existingVal instanceof List ) { // add to existing list ((List<Object>) existingVal).add(val); } else { // replace existing value with list List<Object> list = new ArrayList<Object>(); list.add(existingVal); list.add(val); result.put(name, list); } } else { result.put(name, val); } } private XMLStreamReader startParse(InputStream in) { XMLStreamReader reader = null; try { reader = XMLInputFactory.newFactory().createXMLStreamReader(in); } catch ( XMLStreamException e ) { throw new RuntimeException(e); } catch ( FactoryConfigurationError e ) { throw new RuntimeException(e); } return reader; } private void endParse(XMLStreamReader reader) { try { reader.close(); } catch ( XMLStreamException e ) { // ignore this } } }