View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.web.xml;
20  
21  import java.io.OutputStream;
22  import java.io.UnsupportedEncodingException;
23  import java.lang.reflect.Array;
24  import java.text.Format;
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  
33  import net.wotonomy.foundation.internal.Introspector;
34  import net.wotonomy.foundation.internal.WotonomyException;
35  import net.wotonomy.foundation.xml.XMLEncoder;
36  
37  import org.dom4j.Element;
38  import org.dom4j.io.OutputFormat;
39  import org.dom4j.io.XMLWriter;
40  import org.dom4j.util.NonLazyElement;
41  import org.xml.sax.Attributes;
42  import org.xml.sax.SAXException;
43  import org.xml.sax.helpers.AttributesImpl;
44  
45  /***
46  * An implementation of XMLEncoder that serializes objects  
47  * into XMLRPC format, which is found at http://xmlrpc.com/spec.
48  * We extend that standard only in that we add a "class" 
49  * attribute to the "value" tag, so that a java-based decoder
50  * can more closely reconstruct the original java data structure.
51  * The class attribute can be safely ignored by clients.
52  * This implementation is not thread-safe, so a new instances
53  * should be created to accomodate multiple threads.
54  */
55  public class XMLRPCEncoder implements XMLEncoder
56  {
57      public static final String METHODCALL = "methodCall";
58      public static final String METHODNAME = "methodName";
59      public static final String METHODRESPONSE = "methodResponse";
60      public static final String PARAMS = "params";
61      public static final String PARAM = "param";
62      public static final String FAULT = "fault";
63      public static final String FAULTCODE = "faultCode";
64      public static final String FAULTSTRING = "faultString";
65  
66      public static final String VALUE = "value";
67      public static final String CLASS = "class";
68      
69      public static final String STRUCT = "struct";
70      public static final String MEMBER = "member";
71      public static final String NAME = "name";
72  
73      public static final String ARRAY = "array";
74      public static final String DATA = "data";
75      
76      public static final String NIL = "nil";
77      public static final String INT = "int";
78      public static final String I4 = "i4";
79      public static final String BOOLEAN = "boolean";
80      public static final String STRING = "string";
81      public static final String DOUBLE = "double";
82      public static final String DATE = "dateTime.iso8601";
83      public static final String BASE64 = "base64";
84      
85      public static final String TRUE = "1";
86      public static final String FALSE = "0";
87      
88      public static final Format DATEFORMAT8601 = 
89          new SimpleDateFormat( "yyyyMMdd'T'HHmmss" );
90  
91      /***
92      * Encodes an object to the specified output stream as XML.
93      * @param anObject The object to be serialized to XML format.
94      * @param anOutputStream The output stream to which the object
95      * will be written.  
96      */ 
97      public void encode( Object anObject, OutputStream anOutputStream )
98      {
99          try
100         {
101         
102             //XMLWriter writer = new UTF8XMLWriter( anOutputStream );
103             RPCXMLWriter writer = new RPCXMLWriter(anOutputStream,OutputFormat.createCompactFormat());
104         	writeValueToXMLWriter( anObject, writer );
105             writer.flush();
106         }
107         catch ( Exception exc )
108         {
109             throw new WotonomyException( exc );
110         }
111     }
112     
113     /***
114     * Encodes a method request in XML-RPC format in a "methodCall" tag,
115     * and writes the XML to the specified output stream.
116     * This method only writes XML: the caller is responsible for 
117     * generating the appropriate header, if any, which should set
118     * the content type as "text/xml" and the content length as appropriate.
119     * The caller is also responsible for writing the xml version tag.
120     * @param aMethodName The method name to appear in the "methodName" tag.
121     * @param aParameterArray An array of objects, each of which will be
122     * encoded as values enclosed in a "param" tag, all of which will be
123     * enclosed in a "params" tag.
124     * @param anOutputStream The stream to which the XML will be written.
125     */
126     public void encodeRequest( 
127         String aMethodName, Object[] aParameterArray, 
128         OutputStream anOutputStream )
129     {
130         try
131         {
132             RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat());
133             writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" );
134             writer.startElement( METHODCALL );
135             writer.startElement( METHODNAME );
136             writer.write( aMethodName );
137             writer.endElement( METHODNAME );
138             writer.startElement( PARAMS );
139             for ( int i = 0; i < aParameterArray.length; i++ )
140             {
141                 writer.startElement( PARAM );
142                 writeValueToXMLWriter( aParameterArray[i], writer );
143                 writer.endElement( PARAM );
144             }
145             writer.endElement( PARAMS );
146             writer.endElement( METHODCALL );
147             writer.flush();
148         }
149         catch ( Exception exc )
150         {
151             throw new WotonomyException( exc );
152         }
153         
154         //TODO: should this return the content-length?
155     }
156     
157     /***
158     * Encodes a method response in XML-RPC format in a "methodResponse" tag,
159     * and writes the XML to the specified output stream.
160     * This method only writes XML: the caller is responsible for 
161     * generating the appropriate header, if any, which should set
162     * the content type as "text/xml" and the content length as appropriate.
163     * The caller is also responsible for writing the xml version tag.
164     * @param aResult A object which will be
165     * encoded as values enclosed in a "param" tag, all of which will be
166     * enclosed in a "params" tag.  
167     * @param anOutputStream The stream to which the XML will be written.
168     */
169     public void encodeResponse( 
170         Object aResult, OutputStream anOutputStream )
171     {
172         try
173         {
174             RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() );
175             writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" );
176             writer.startElement( METHODRESPONSE );
177             writer.startElement( PARAMS );
178             writer.startElement( PARAM );
179             writeValueToXMLWriter( aResult, writer );                
180             writer.endElement( PARAM );
181             writer.endElement( PARAMS );
182             writer.endElement( METHODRESPONSE );
183             writer.flush();
184         }
185         catch ( Exception exc )
186         {
187             throw new WotonomyException( exc );
188         }
189         
190         //TODO: should this return the content-length?
191     }
192     
193     /***
194     * Encodes a fault response in XML-RPC format in a "methodResponse" tag,
195     * and writes the XML to the specified output stream.
196     * This method only writes XML: the caller is responsible for first
197     * generating the appropriate header, if any, which should set
198     * the content type as "text/xml" and the content length as appropriate.
199     * The caller is also responsible for writing the xml version tag.
200     * @param aFaultCode An application-defined error code.
201     * @param aFaultString A human-readable error description.
202     * @param anOutputStream The stream to which the XML will be written.
203     */
204     public void encodeFault( 
205         int aFaultCode, String aFaultString, OutputStream anOutputStream )
206     {
207         try
208         {
209             RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() );
210             writer.processingInstruction( "xml", "version=\"1.0\"" );
211             writer.startElement( METHODRESPONSE );
212             writer.startElement( FAULT );
213             writer.startElement( VALUE );
214             writer.startElement( STRUCT );
215 
216             writer.startElement( MEMBER );
217             writer.startElement( NAME );
218             writer.write( FAULTCODE );
219             writer.endElement( NAME );
220             writer.startElement( VALUE );
221             writer.startElement( INT );
222             writer.write( new Integer( aFaultCode ).toString() );
223             writer.endElement( INT );
224             writer.endElement( VALUE );
225             writer.endElement( MEMBER );
226 
227             writer.startElement( MEMBER );
228             writer.startElement( NAME );
229             writer.write( FAULTSTRING );
230             writer.endElement( NAME );
231             writer.startElement( VALUE );
232             writer.startElement( STRING );
233             writer.write( aFaultString );
234             writer.endElement( STRING );
235             writer.endElement( VALUE );
236             writer.endElement( MEMBER );
237 
238             writer.endElement( STRUCT );
239             writer.endElement( VALUE );
240             writer.endElement( FAULT );
241             writer.endElement( METHODRESPONSE );
242             writer.flush();
243         }
244         catch ( Exception exc )
245         {
246             throw new WotonomyException( exc );
247         }
248         
249         //TODO: should this return the content-length?
250     }
251     
252     /***
253     * Performs the actual writing of the file to XML.
254     */
255     private void writeValueToXMLWriter( 
256         Object anObject, RPCXMLWriter writer )
257     {
258         try
259         {
260 
261         
262         if ( anObject == null )
263         {
264         	writer.startElement( VALUE );
265         	// write nil for null
266         	Element nill = new NonLazyElement(NIL);
267         	writer.write(nill);
268         }
269         else
270         if ( anObject instanceof Collection )
271         {
272             // write class so we can restore if possible
273         	AttributesImpl a = new AttributesImpl();
274             a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
275             
276             writer.startElement( VALUE, a );
277 
278             // write items in the order we get them from the iterator
279             
280             writer.startElement( ARRAY );
281             writer.startElement( DATA );
282             Iterator it = ((Collection)anObject).iterator();
283             while ( it.hasNext() )
284             {
285                 writeValueToXMLWriter( it.next(), writer );
286             }
287             writer.endElement( DATA );
288             writer.endElement( ARRAY );
289         
290         }
291         else
292         if ( anObject instanceof Map )
293         {
294         	AttributesImpl a = new AttributesImpl();
295             a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
296             
297             writer.startElement( VALUE, a );
298 
299             // write items in the order we get them from the iterator
300             //FIXME: The method-based properties are being ignored!
301             
302             Map.Entry entry;
303             writer.startElement( STRUCT );
304             writer.startElement( MEMBER );
305             Iterator it = ((Map)anObject).entrySet().iterator();
306             while ( it.hasNext() )
307             {
308                 entry = (Map.Entry) it.next();
309                 writer.startElement( NAME );
310                 writeValueToXMLWriter( entry.getKey(), writer );
311                 writer.endElement( NAME );
312                 writeValueToXMLWriter( entry.getValue(), writer );
313             }
314             writer.endElement( MEMBER );
315             writer.endElement( STRUCT );
316         }
317         else // not a collection 
318         {
319             // check for primitive types
320             if ( anObject instanceof String )
321             {                
322                 writer.startElement( VALUE );
323                 
324                 writer.startElement( STRING );
325                 writer.write( anObject.toString() );
326                 writer.endElement( STRING );
327             }
328             else
329             if ( anObject instanceof StringBuffer )
330             {
331                 // write class so we can restore if possible
332             	AttributesImpl a = new AttributesImpl();
333                 a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
334                 
335                 writer.startElement( VALUE, a );
336 
337                 writer.startElement( STRING );
338                 writer.write( anObject.toString() );
339                 writer.endElement( STRING );
340             }
341             else
342             if ( anObject instanceof Number )
343             {
344                 // write class so we can restore if possible
345             	AttributesImpl a = new AttributesImpl();
346                 a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
347                 
348                 writer.startElement( VALUE, a );
349 
350                 if ( ( anObject instanceof Double ) 
351                 || ( anObject instanceof Float ) )
352                 {
353                     writer.startElement( DOUBLE );
354                     writer.write( anObject.toString() );
355                     writer.endElement( DOUBLE );
356                 }
357                 else
358                 {
359                     writer.startElement( INT );
360                     writer.write( anObject.toString() );
361                     writer.endElement( INT );
362                 }
363             }
364             else
365             if ( anObject instanceof Date )
366             {
367                 // write class so we can restore if possible
368             	AttributesImpl a = new AttributesImpl();
369                 a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
370                 
371                 writer.startElement( VALUE, a );
372 
373                 writer.startElement( DATE );
374                 writer.write( DATEFORMAT8601.format( anObject ) );
375                 writer.endElement( DATE );
376             }
377             else
378             if ( anObject instanceof Boolean )
379             {
380                 writer.startElement( BOOLEAN );
381                 if ( ((Boolean)anObject).booleanValue() )
382                 {
383                     writer.write( "1" );
384                 }
385                 else
386                 {
387                     writer.write( "0" );
388                 }
389                 writer.endElement( BOOLEAN );
390             }
391             else
392             if ( anObject.getClass().isArray() )
393             {
394                 // write class so we can restore if possible
395             	AttributesImpl a = new AttributesImpl();
396                 a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
397                 
398                 writer.startElement( VALUE, a );
399     
400                 writer.startElement( ARRAY );
401                 writer.startElement( DATA );
402     
403                 int length = Array.getLength( anObject );
404                 for ( int i = 0; i < length; i++ )
405                 {
406                     writeValueToXMLWriter( Array.get( anObject, i ), writer );  
407                 }
408     
409                 writer.endElement( DATA );
410                 writer.endElement( ARRAY );
411             }
412             else // not primitive or collection, treat as struct
413             {
414                 // write class so we can restore if possible
415             	AttributesImpl a = new AttributesImpl();
416                 a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() );
417                 
418                 writer.startElement( VALUE, a );
419 
420                 List readProperties = new ArrayList();
421                 String[] read = Introspector.getReadPropertiesForObject( anObject );
422                 for ( int i = 0; i < read.length; i++ )
423                 {
424                     readProperties.add( read[i] );
425                 }
426         
427                 List properties = new ArrayList();
428                 String[] write = Introspector.getWritePropertiesForObject( anObject );
429                 for ( int i = 0; i < write.length; i++ )
430                 {
431                     properties.add( write[i] );
432                 }
433                 
434                 // only use readable properties
435                 properties.retainAll( readProperties );
436                 
437 //                if ( properties.size() > 0 )
438 //                {
439                     String key;
440                     Object value;
441                     Iterator it = properties.iterator();
442                     writer.startElement( STRUCT );
443                     while ( it.hasNext() )
444                     {
445                         key = (String) it.next();
446                         value = Introspector.get( anObject, key );
447                     
448                         writer.startElement( MEMBER );
449                             writer.startElement( NAME );
450                                 writer.write( key ); 
451                             writer.endElement( NAME );
452                                 writeValueToXMLWriter( value, writer );
453                         writer.endElement( MEMBER );
454                     }
455                     writer.endElement( STRUCT );
456 /*                    
457                 }
458                 else // no properties - write a converted string
459                 {
460                     writer.startElement( STRING );
461                     Object converted =
462                         ValueConverter.convertObjectToClass( anObject, String.class );
463                     if ( converted != null )
464                     {
465                         writer.write( converted.toString() );
466                     }
467                     else
468                     {
469                         writer.write( anObject.toString() );
470                     }
471                     writer.endElement( STRING );
472                 }
473 */                
474             }
475         }
476         
477         writer.endElement( VALUE );
478         } 
479         catch ( Exception exc )
480         {
481             System.err.println( "XMLFileSoup.writeValueToXMLWriter: " + exc );
482             exc.printStackTrace();
483         }
484         
485     }
486 /*    
487     public static void main( String[] argv )
488     {
489         System.out.println( "<test>" );
490         XMLRPCEncoder encoder = new XMLRPCEncoder();
491         encoder.encodeRequest( "systemObject.test", new Object[] { 
492             new net.wotonomy.test.TestObject(), 
493             new net.wotonomy.test.TestObject(), 
494             new net.wotonomy.test.TestObject() }, System.out );
495         System.out.println();
496         System.out.println();
497         encoder.encodeResponse( new net.wotonomy.test.TestObject(), System.out );
498         System.out.println();
499         System.out.println();
500         encoder.encodeFault( -1, "This is a fault.", System.out );
501         System.out.println();
502         System.out.println();
503         System.out.println( "</test>" );
504     }
505 */        
506     
507     private class RPCXMLWriter extends XMLWriter {
508 
509 		public RPCXMLWriter(OutputStream arg0, OutputFormat arg1) throws UnsupportedEncodingException {
510 			super(arg0, arg1);
511 		}
512 
513 		public void endElement(String localname) throws SAXException {	
514 			super.endElement(null, localname, null);
515 		}
516 
517 		public void startElement(String localname) throws SAXException {
518 			this.startElement(localname, null);
519 		}
520 		public void startElement(String localname, Attributes attributes) throws SAXException {
521 			this.startElement(null, localname, null, attributes);
522 		}
523 
524     }
525     
526 }