/* 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); } } }