/* ================================================================== * SimpleXmlHttpMessageConverter.java - Dec 3, 2013 11:58:01 AM * * Copyright 2007-2013 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 * ================================================================== */ package net.solarnetwork.web.support; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TimeZone; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import net.solarnetwork.util.ClassUtils; import net.solarnetwork.util.PropertySerializerRegistrar; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; /** * {@link HttpMessageConverter} that marshals objects into XML documents. * * @author matt * @version 1.0 */ public class SimpleXmlHttpMessageConverter extends AbstractHttpMessageConverter<Object> { private static final ThreadLocal<SimpleDateFormat> SDF = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { TimeZone tz = TimeZone.getTimeZone("GMT"); SimpleDateFormat sdf = new SimpleDateFormat(); sdf.setTimeZone(tz); sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); return sdf; } }; /** Default content type. */ public static final String DEFAULT_XML_CONTENT_TYPE = "text/xml;charset=UTF-8"; private XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); private Set<String> classNamesAllowedForNesting = null; private PropertySerializerRegistrar propertySerializerRegistrar = null; /** * Default constructor. */ public SimpleXmlHttpMessageConverter() { super(MediaType.APPLICATION_XML, MediaType.TEXT_XML); } @Override protected boolean supports(Class<?> clazz) { return true; } @Override protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException("Mapping from XML is not supported yet."); } @Override protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { OutputStream out = outputMessage.getBody(); try { XMLStreamWriter writer = xmlOutputFactory.createXMLStreamWriter(out, "UTF-8"); writer.writeStartDocument("UTF-8", "1.0"); outputObject(t, t.getClass().getSimpleName(), writer); writer.flush(); } catch ( XMLStreamException e ) { throw new HttpMessageConversionException("Error creating XMLStreamWriter", e); } } private void outputObject(Object o, String name, XMLStreamWriter writer) throws IOException, XMLStreamException { if ( o instanceof Collection ) { Collection<?> col = (Collection<?>) o; outputCollection(col, name, writer); } else if ( o instanceof Map ) { Map<?, ?> map = (Map<?, ?>) o; outputMap(map, name, writer); } 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, writer, true); } else { String elementName = (o == null ? name : org.springframework.util.ClassUtils.getShortName(o .getClass())); writeElement(elementName, o, writer, true); } } private void outputMap(Map<?, ?> map, String name, XMLStreamWriter writer) throws IOException, XMLStreamException { writeElement(name, null, writer, false); // for each entry, write an <entry> element for ( Map.Entry<?, ?> me : map.entrySet() ) { String entryName = me.getKey().toString(); writer.writeStartElement("entry"); writer.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", writer); } } else { outputObject(value, null, writer); } writer.writeEndElement(); } writer.writeEndElement(); } private void outputCollection(Collection<?> col, String name, XMLStreamWriter writer) throws IOException, XMLStreamException { writeElement(name, null, writer, false); for ( Object o : col ) { outputObject(o, null, writer); } writer.writeEndElement(); } private void writeElement(String name, Map<?, ?> props, XMLStreamWriter writer, boolean close) throws IOException, XMLStreamException { writer.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(); Date date = (Date) val; val = sdf.format(date); } 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 ) { writer.writeAttribute(key, val.toString()); } } } if ( nested != null ) { for ( Map.Entry<String, Object> me : nested.entrySet() ) { outputObject(me.getValue(), me.getKey(), writer); } } if ( close ) { writer.writeEndElement(); } } private void writeElement(String name, Object bean, XMLStreamWriter writer, boolean close) throws IOException, 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, writer); } return; } } Map<String, Object> props = ClassUtils.getBeanProperties(bean, null, true); writeElement(name, props, writer, close); } public Set<String> getClassNamesAllowedForNesting() { return classNamesAllowedForNesting; } public void setClassNamesAllowedForNesting(Set<String> classNamesAllowedForNesting) { this.classNamesAllowedForNesting = classNamesAllowedForNesting; } public void setXmlOutputFactory(XMLOutputFactory xmlOutputFactory) { this.xmlOutputFactory = xmlOutputFactory; } public XMLOutputFactory getXmlOutputFactory() { return xmlOutputFactory; } public PropertySerializerRegistrar getPropertySerializerRegistrar() { return propertySerializerRegistrar; } public void setPropertySerializerRegistrar(PropertySerializerRegistrar propertySerializerRegistrar) { this.propertySerializerRegistrar = propertySerializerRegistrar; } }