/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.web.xml; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.text.Format; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import net.wotonomy.foundation.internal.Introspector; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.foundation.xml.XMLEncoder; import org.dom4j.Element; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.dom4j.util.NonLazyElement; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** * An implementation of XMLEncoder that serializes objects * into XMLRPC format, which is found at http://xmlrpc.com/spec. * We extend that standard only in that we add a "class" * attribute to the "value" tag, so that a java-based decoder * can more closely reconstruct the original java data structure. * The class attribute can be safely ignored by clients. * This implementation is not thread-safe, so a new instances * should be created to accomodate multiple threads. */ public class XMLRPCEncoder implements XMLEncoder { public static final String METHODCALL = "methodCall"; public static final String METHODNAME = "methodName"; public static final String METHODRESPONSE = "methodResponse"; public static final String PARAMS = "params"; public static final String PARAM = "param"; public static final String FAULT = "fault"; public static final String FAULTCODE = "faultCode"; public static final String FAULTSTRING = "faultString"; public static final String VALUE = "value"; public static final String CLASS = "class"; public static final String STRUCT = "struct"; public static final String MEMBER = "member"; public static final String NAME = "name"; public static final String ARRAY = "array"; public static final String DATA = "data"; public static final String NIL = "nil"; public static final String INT = "int"; public static final String I4 = "i4"; public static final String BOOLEAN = "boolean"; public static final String STRING = "string"; public static final String DOUBLE = "double"; public static final String DATE = "dateTime.iso8601"; public static final String BASE64 = "base64"; public static final String TRUE = "1"; public static final String FALSE = "0"; public static final Format DATEFORMAT8601 = new SimpleDateFormat( "yyyyMMdd'T'HHmmss" ); /** * Encodes an object to the specified output stream as XML. * @param anObject The object to be serialized to XML format. * @param anOutputStream The output stream to which the object * will be written. */ public void encode( Object anObject, OutputStream anOutputStream ) { try { //XMLWriter writer = new UTF8XMLWriter( anOutputStream ); RPCXMLWriter writer = new RPCXMLWriter(anOutputStream,OutputFormat.createCompactFormat()); writeValueToXMLWriter( anObject, writer ); writer.flush(); } catch ( Exception exc ) { throw new WotonomyException( exc ); } } /** * Encodes a method request in XML-RPC format in a "methodCall" tag, * and writes the XML to the specified output stream. * This method only writes XML: the caller is responsible for * generating the appropriate header, if any, which should set * the content type as "text/xml" and the content length as appropriate. * The caller is also responsible for writing the xml version tag. * @param aMethodName The method name to appear in the "methodName" tag. * @param aParameterArray An array of objects, each of which will be * encoded as values enclosed in a "param" tag, all of which will be * enclosed in a "params" tag. * @param anOutputStream The stream to which the XML will be written. */ public void encodeRequest( String aMethodName, Object[] aParameterArray, OutputStream anOutputStream ) { try { RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat()); writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); writer.startElement( METHODCALL ); writer.startElement( METHODNAME ); writer.write( aMethodName ); writer.endElement( METHODNAME ); writer.startElement( PARAMS ); for ( int i = 0; i < aParameterArray.length; i++ ) { writer.startElement( PARAM ); writeValueToXMLWriter( aParameterArray[i], writer ); writer.endElement( PARAM ); } writer.endElement( PARAMS ); writer.endElement( METHODCALL ); writer.flush(); } catch ( Exception exc ) { throw new WotonomyException( exc ); } //TODO: should this return the content-length? } /** * Encodes a method response in XML-RPC format in a "methodResponse" tag, * and writes the XML to the specified output stream. * This method only writes XML: the caller is responsible for * generating the appropriate header, if any, which should set * the content type as "text/xml" and the content length as appropriate. * The caller is also responsible for writing the xml version tag. * @param aResult A object which will be * encoded as values enclosed in a "param" tag, all of which will be * enclosed in a "params" tag. * @param anOutputStream The stream to which the XML will be written. */ public void encodeResponse( Object aResult, OutputStream anOutputStream ) { try { RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); writer.startElement( METHODRESPONSE ); writer.startElement( PARAMS ); writer.startElement( PARAM ); writeValueToXMLWriter( aResult, writer ); writer.endElement( PARAM ); writer.endElement( PARAMS ); writer.endElement( METHODRESPONSE ); writer.flush(); } catch ( Exception exc ) { throw new WotonomyException( exc ); } //TODO: should this return the content-length? } /** * Encodes a fault response in XML-RPC format in a "methodResponse" tag, * and writes the XML to the specified output stream. * This method only writes XML: the caller is responsible for first * generating the appropriate header, if any, which should set * the content type as "text/xml" and the content length as appropriate. * The caller is also responsible for writing the xml version tag. * @param aFaultCode An application-defined error code. * @param aFaultString A human-readable error description. * @param anOutputStream The stream to which the XML will be written. */ public void encodeFault( int aFaultCode, String aFaultString, OutputStream anOutputStream ) { try { RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); writer.processingInstruction( "xml", "version=\"1.0\"" ); writer.startElement( METHODRESPONSE ); writer.startElement( FAULT ); writer.startElement( VALUE ); writer.startElement( STRUCT ); writer.startElement( MEMBER ); writer.startElement( NAME ); writer.write( FAULTCODE ); writer.endElement( NAME ); writer.startElement( VALUE ); writer.startElement( INT ); writer.write( new Integer( aFaultCode ).toString() ); writer.endElement( INT ); writer.endElement( VALUE ); writer.endElement( MEMBER ); writer.startElement( MEMBER ); writer.startElement( NAME ); writer.write( FAULTSTRING ); writer.endElement( NAME ); writer.startElement( VALUE ); writer.startElement( STRING ); writer.write( aFaultString ); writer.endElement( STRING ); writer.endElement( VALUE ); writer.endElement( MEMBER ); writer.endElement( STRUCT ); writer.endElement( VALUE ); writer.endElement( FAULT ); writer.endElement( METHODRESPONSE ); writer.flush(); } catch ( Exception exc ) { throw new WotonomyException( exc ); } //TODO: should this return the content-length? } /** * Performs the actual writing of the file to XML. */ private void writeValueToXMLWriter( Object anObject, RPCXMLWriter writer ) { try { if ( anObject == null ) { writer.startElement( VALUE ); // write nil for null Element nill = new NonLazyElement(NIL); writer.write(nill); } else if ( anObject instanceof Collection ) { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); // write items in the order we get them from the iterator writer.startElement( ARRAY ); writer.startElement( DATA ); Iterator it = ((Collection)anObject).iterator(); while ( it.hasNext() ) { writeValueToXMLWriter( it.next(), writer ); } writer.endElement( DATA ); writer.endElement( ARRAY ); } else if ( anObject instanceof Map ) { AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); // write items in the order we get them from the iterator //FIXME: The method-based properties are being ignored! Map.Entry entry; writer.startElement( STRUCT ); writer.startElement( MEMBER ); Iterator it = ((Map)anObject).entrySet().iterator(); while ( it.hasNext() ) { entry = (Map.Entry) it.next(); writer.startElement( NAME ); writeValueToXMLWriter( entry.getKey(), writer ); writer.endElement( NAME ); writeValueToXMLWriter( entry.getValue(), writer ); } writer.endElement( MEMBER ); writer.endElement( STRUCT ); } else // not a collection { // check for primitive types if ( anObject instanceof String ) { writer.startElement( VALUE ); writer.startElement( STRING ); writer.write( anObject.toString() ); writer.endElement( STRING ); } else if ( anObject instanceof StringBuffer ) { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); writer.startElement( STRING ); writer.write( anObject.toString() ); writer.endElement( STRING ); } else if ( anObject instanceof Number ) { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); if ( ( anObject instanceof Double ) || ( anObject instanceof Float ) ) { writer.startElement( DOUBLE ); writer.write( anObject.toString() ); writer.endElement( DOUBLE ); } else { writer.startElement( INT ); writer.write( anObject.toString() ); writer.endElement( INT ); } } else if ( anObject instanceof Date ) { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); writer.startElement( DATE ); writer.write( DATEFORMAT8601.format( anObject ) ); writer.endElement( DATE ); } else if ( anObject instanceof Boolean ) { writer.startElement( BOOLEAN ); if ( ((Boolean)anObject).booleanValue() ) { writer.write( "1" ); } else { writer.write( "0" ); } writer.endElement( BOOLEAN ); } else if ( anObject.getClass().isArray() ) { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); writer.startElement( ARRAY ); writer.startElement( DATA ); int length = Array.getLength( anObject ); for ( int i = 0; i < length; i++ ) { writeValueToXMLWriter( Array.get( anObject, i ), writer ); } writer.endElement( DATA ); writer.endElement( ARRAY ); } else // not primitive or collection, treat as struct { // write class so we can restore if possible AttributesImpl a = new AttributesImpl(); a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); writer.startElement( VALUE, a ); List readProperties = new ArrayList(); String[] read = Introspector.getReadPropertiesForObject( anObject ); for ( int i = 0; i < read.length; i++ ) { readProperties.add( read[i] ); } List properties = new ArrayList(); String[] write = Introspector.getWritePropertiesForObject( anObject ); for ( int i = 0; i < write.length; i++ ) { properties.add( write[i] ); } // only use readable properties properties.retainAll( readProperties ); // if ( properties.size() > 0 ) // { String key; Object value; Iterator it = properties.iterator(); writer.startElement( STRUCT ); while ( it.hasNext() ) { key = (String) it.next(); value = Introspector.get( anObject, key ); writer.startElement( MEMBER ); writer.startElement( NAME ); writer.write( key ); writer.endElement( NAME ); writeValueToXMLWriter( value, writer ); writer.endElement( MEMBER ); } writer.endElement( STRUCT ); /* } else // no properties - write a converted string { writer.startElement( STRING ); Object converted = ValueConverter.convertObjectToClass( anObject, String.class ); if ( converted != null ) { writer.write( converted.toString() ); } else { writer.write( anObject.toString() ); } writer.endElement( STRING ); } */ } } writer.endElement( VALUE ); } catch ( Exception exc ) { System.err.println( "XMLFileSoup.writeValueToXMLWriter: " + exc ); exc.printStackTrace(); } } /* public static void main( String[] argv ) { System.out.println( "" ); XMLRPCEncoder encoder = new XMLRPCEncoder(); encoder.encodeRequest( "systemObject.test", new Object[] { new net.wotonomy.test.TestObject(), new net.wotonomy.test.TestObject(), new net.wotonomy.test.TestObject() }, System.out ); System.out.println(); System.out.println(); encoder.encodeResponse( new net.wotonomy.test.TestObject(), System.out ); System.out.println(); System.out.println(); encoder.encodeFault( -1, "This is a fault.", System.out ); System.out.println(); System.out.println(); System.out.println( "" ); } */ private class RPCXMLWriter extends XMLWriter { public RPCXMLWriter(OutputStream arg0, OutputFormat arg1) throws UnsupportedEncodingException { super(arg0, arg1); } public void endElement(String localname) throws SAXException { super.endElement(null, localname, null); } public void startElement(String localname) throws SAXException { this.startElement(localname, null); } public void startElement(String localname, Attributes attributes) throws SAXException { this.startElement(null, localname, null, attributes); } } }