001: /*
002: * Koala Bean Markup Language - Copyright (C) 1999 Dyade
003: *
004: * Permission is hereby granted, free of charge, to any person obtaining a
005: * copy of this software and associated documentation files
006: * (the "Software"), to deal in the Software without restriction, including
007: * without limitation the rights to use, copy, modify, merge, publish,
008: * distribute, sublicense, and/or sell copies of the Software, and to permit
009: * persons to whom the Software is furnished to do so, subject to the
010: * following conditions:
011: * The above copyright notice and this permission notice shall be included
012: * in all copies or substantial portions of the Software.
013: *
014: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
015: * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
016: * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
017: * IN NO EVENT SHALL Dyade BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
019: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
020: * DEALINGS IN THE SOFTWARE.
021: *
022: * Except as contained in this notice, the name of Dyade shall not be
023: * used in advertising or otherwise to promote the sale, use or other
024: * dealings in this Software without prior written authorization from
025: * Dyade.
026: *
027: * $Id: KBMLSerializer.java,v 1.21 2000/08/01 13:26:20 tkormann Exp $
028: * Author: Thierry.Kormann@sophia.inria.fr
029: */
030:
031: package fr.dyade.koala.xml.kbml;
032:
033: import java.io.*;
034: import java.util.Hashtable;
035: import java.beans.*;
036: import java.lang.reflect.*;
037:
038: /**
039: * The class enables to create an XML document from JavaBeans.
040: *
041: * Any number of beans can be written. In order to have a valid XML
042: * document, the beans must be enclosed by the KBML tag. Here is an
043: * exemple to write 2 beans:
044: *
045: * <pre><code>
046: * KBMLSerializer bxo = new KBMLSerializer(new FileOutputStream("test.kbml"));
047: * bxo.writeXMLDeclaration();
048: * bxo.writeDocumentTypeDefinition();
049: * bxo.writeKBMLStartTag();
050: * bxo.writeBean(bean1);
051: * bxo.writeBean(bean2);
052: * bxo.writeKBMLEndTag();
053: * bxo.flush();
054: * bxo.close();
055: * </code></pre>
056: *
057: * @author Thierry.Kormann@sophia.inria.fr
058: */
059: public class KBMLSerializer {
060:
061: /**
062: * This flag bit indicates that the default values must be written.
063: */
064: public static final int WRITE_DEFAULT_VALUES = 1;
065:
066: /** The Hashtable where keys are Object and values are ID. */
067: protected Hashtable beansCache = new Hashtable();
068: /** An internal cache for the default bean instance. */
069: private Hashtable cache = new Hashtable();
070: /** The unique ID ref counter. */
071: private int id = 0;
072: /** The underlying output stream used to store the beans. */
073: protected OutputStream ostream;
074: /** The underlying writer used to store the beans. */
075: protected Writer writer;
076: /** The error handler used to notify no-fatal errors. */
077: protected ErrorHandler handler;
078: /** The options used by this serializer. */
079: protected int serializationOptions;
080:
081: /**
082: * Constructs a new serializer with the specified output stream.
083: * @param ostream the underlying output stream, to be saved for later use
084: */
085: public KBMLSerializer(OutputStream ostream) {
086: this (ostream, new KBMLSerializerDefaultErrorHandler());
087: }
088:
089: /**
090: * Constructs a new serializer with the specified output stream
091: * and error handler.
092: * @param ostream the underlying output stream, to be saved for later use
093: * @param handler the handler to track the no-fatal error
094: */
095: public KBMLSerializer(OutputStream ostream, ErrorHandler handler) {
096: this .ostream = ostream;
097: this .handler = handler;
098: initializePropertyEditorManager();
099: }
100:
101: /**
102: * Constructs a new serializer with the specified writer.
103: * @param writer the underlying writer, to be saved for later use
104: */
105: public KBMLSerializer(Writer writer) {
106: this (writer, new KBMLSerializerDefaultErrorHandler());
107: }
108:
109: /**
110: * Constructs a new serializer with the specified writer
111: * and error handler.
112: * @param writer the underlying writer, to be saved for later use
113: * @param handler the handler to track the no-fatal error
114: */
115: public KBMLSerializer(Writer writer, ErrorHandler handler) {
116: this .writer = writer;
117: this .handler = handler;
118: initializePropertyEditorManager();
119: }
120:
121: /**
122: * Enables or disables options that can modify the way this
123: * serializer writes beans, properties and values.
124: * @param serializationOptions determines how this serializer will
125: * write beans, properties or values
126: * @see #WRITE_DEFAULT_VALUES
127: * @since KBML 2.3
128: */
129: public void setSerializationOptions(int serializationOptions) {
130: this .serializationOptions = serializationOptions;
131: }
132:
133: /**
134: * Writes a comment.
135: * @param comment The comment string. Special characters will be quoted.
136: * @exception IOException if an I/O error occurs
137: */
138: public void writeComment(String comment) throws IOException {
139: write("<!-- " + Util.toXML(comment) + " -->");
140: }
141:
142: /**
143: * Writes the XML declaration. The declaration is
144: * <code><?xml version='1.0' encoding='UTF-8'?></code>
145: * @exception IOException if an I/O error occurs
146: * @since KBML 2.2
147: */
148: public void writeXMLDeclaration() throws IOException {
149: write(XML_DECLARATION);
150: }
151:
152: /**
153: * Writes the default document type definition. The DTD used is
154: * the one located at http://www.inria.fr/koala/kbml/kbml20.dtd
155: * @exception IOException if an I/O error occurs
156: * @since KBML 2.2
157: */
158: public void writeDocumentTypeDefinition() throws IOException {
159: writeDocumentTypeDefinition(DOCTYPE);
160: }
161:
162: /**
163: * Writes the specified document type definition.
164: * @param doctype the document type definition to write
165: * @exception IOException if an I/O error occurs
166: * @since KBML 2.2
167: */
168: public void writeDocumentTypeDefinition(String doctype)
169: throws IOException {
170: write(doctype);
171: }
172:
173: /**
174: * Writes the KBML start tag.
175: * @exception IOException if an I/O error occurs
176: * @exception IllegalStateException if KBML start tag was already written.
177: */
178: public void writeKBMLStartTag() throws IOException {
179: write(START_ROOT_ELEMENT);
180: }
181:
182: /**
183: * Writes the KBML end tag.
184: * @exception IOException if an I/O error occurs
185: * @exception IllegalStateException if KBML start tag was not
186: * written, or KBML end tag was already written.
187: */
188: public void writeKBMLEndTag() throws IOException {
189: write(END_ROOT_ELEMENT);
190: if (ostream != null) {
191: ostream.write('\n');
192: } else {
193: writer.write('\n');
194: }
195: }
196:
197: /**
198: * Writes the specified bean object.
199: * @param bean the bean object to write
200: * @exception IOException if an I/O error occurs
201: * @exception IllegalStateException if not inside the KBML tag.
202: */
203: public void writeBean(Object bean) throws IOException {
204: // check null
205: if (bean == null) {
206: writeNullElement();
207: return;
208: }
209: // check if the bean has already been written
210: if (beansCache.containsKey(bean)) {
211: writeBeanRefElement((String) beansCache.get(bean));
212: return;
213: }
214: String id = newID();
215: beansCache.put(bean, id);
216: Class beanClass = bean.getClass();
217: // write the start bean tag with its class and id attributes
218: writeBeanStartElement(getBeanClassName(bean), id);
219: // write the properties
220: BeanInfo info;
221: try {
222: info = Introspector.getBeanInfo(beanClass);
223: } catch (IntrospectionException ex) {
224: handler.introspector(bean, ex);
225: writeNullElement();
226: return;
227: }
228: PropertyDescriptor[] pds = info.getPropertyDescriptors();
229: Object defaultBean = null;
230: try {
231: defaultBean = getDefaultBeanInstance(bean);
232: } catch (ClassNotFoundException ex) {
233: // nothing to do, the class has already been found
234: } catch (IOException ex) {
235: handler.instanciateBean(bean, ex);
236: writeBeanEndElement();
237: return;
238: } catch (InstantiationException ex) {
239: handler.instanciateBean(bean, ex);
240: writeBeanEndElement();
241: return;
242: } catch (IllegalAccessException ex) {
243: handler.instanciateBean(bean, ex);
244: writeBeanEndElement();
245: return;
246: } catch (NoSuchMethodError err) {
247: handler.instanciateBean(bean, err);
248: writeBeanEndElement();
249: return;
250: }
251: for (int i = 0; i < pds.length; ++i) {
252: PropertyDescriptor pd = pds[i];
253: // ensure the property descriptor exists
254: if (pd == null) {
255: handler.propertyDescriptor(bean, pd);
256: continue;
257: }
258: // discard bad patterns
259: if (pd instanceof IndexedPropertyDescriptor) {
260: IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
261: if ((ipd.getReadMethod() == null)
262: || ((ipd.getWriteMethod() == null) && (ipd
263: .getIndexedWriteMethod() == null))) {
264: continue;
265: }
266: } else {
267: if ((pd.getReadMethod() == null)
268: || (pd.getWriteMethod() == null)) {
269: continue;
270: }
271: }
272: // find the property value
273: Object value;
274: try {
275: value = pd.getReadMethod().invoke(bean, null);
276: } catch (IllegalAccessException ex) {
277: handler.readMethod(bean, pd, ex);
278: continue;
279: } catch (InvocationTargetException ex) {
280: handler.readMethod(bean, pd, ex);
281: continue;
282: }
283: // check if the property must be written.
284: if ((serializationOptions & WRITE_DEFAULT_VALUES) == 0) {
285: Method readMethod = pd.getReadMethod();
286: Object defaultValue = null;
287: try {
288: defaultValue = readMethod.invoke(defaultBean, null);
289: } catch (IllegalAccessException ex) {
290: // nothing to do, previously catched when asking the value
291: } catch (InvocationTargetException ex) {
292: // nothing to do, previously catched when asking the value
293: }
294: if ((value == null && defaultValue == null)
295: || ((value != null && value
296: .equals(defaultValue)))) {
297: continue;
298: }
299: }
300: // write the property element
301: writePropertyStartElement(pd.getName());
302: if (value != null && value.getClass().isArray()) {
303: // handle indexed property
304: if (beansCache.containsKey(value)) {
305: String arrayID = (String) beansCache.get(value);
306: writeValueArrayRefElement(arrayID);
307: } else {
308: String arrayID = newID();
309: writeValueArrayStartElement(arrayID);
310: beansCache.put(value, arrayID);
311: int length = Array.getLength(value);
312: Class subType = value.getClass().getComponentType();
313: for (int j = 0; j < length; ++j) {
314: Object subValue = Array.get(value, j);
315: writeValue(pd, subValue, subType);
316: }
317: writeValueArrayEndElement();
318: }
319: } else {
320: // handle no indexed property
321: writeValue(pd, value, pd.getPropertyType());
322: }
323: writePropertyEndElement();
324: }
325: // close the bean element
326: writeBeanEndElement();
327: }
328:
329: void writeValue(PropertyDescriptor pd, Object value,
330: Class expectedClass) throws IOException {
331: // check null value
332: if (value == null) {
333: writeNullElement();
334: return;
335: }
336: Class valueClass = value.getClass();
337: // check for a property editor
338: Class editorClass = pd.getPropertyEditorClass();
339: PropertyEditor editor = null;
340: if (editorClass == null) {
341: editor = PropertyEditorManager.findEditor(valueClass);
342: } else {
343: try {
344: editor = (PropertyEditor) editorClass.newInstance();
345: } catch (IllegalAccessException ex) {
346: handler.propertyEditor(pd, ex);
347: writeNullElement();
348: return;
349: } catch (InstantiationException ex) {
350: handler.propertyEditor(pd, ex);
351: writeNullElement();
352: return;
353: } catch (NoSuchMethodError err) {
354: handler.propertyEditor(pd, err);
355: writeNullElement();
356: return;
357: }
358: }
359: if (editor != null) {
360: // check if the value has already been written
361: if (beansCache.containsKey(value)) {
362: String valueID = (String) beansCache.get(value);
363: writeValueRefElement(valueID);
364: } else {
365: // write a value element
366: editor.setValue(value);
367: String content = editor.getAsText();
368: if (content == null) {
369: handler.propertyEditor(pd, editor);
370: writeNullElement();
371: return;
372: }
373: if (expectedClass.isPrimitive()) {
374: // no attribute for primitive types
375: writeValueStartElement();
376: } else {
377: String valueID = newID();
378: if (valueClass != expectedClass) {
379: // pathologic case: with a property editor, if
380: // the value is a subclasse of the property
381: // type, the real type is lost so we have to
382: // add a class attribute
383: writeValueStartElement(valueClass, valueID);
384: } else {
385: writeValueStartElement(valueID);
386: }
387: // add the bean to the cache
388: beansCache.put(value, valueID);
389: }
390: writeContent(content);
391: writeValueEndElement();
392: }
393: } else {
394: // check if the bean has already been written
395: if (beansCache.containsKey(value)) {
396: String valueID = (String) beansCache.get(value);
397: writeBeanRefElement(valueID);
398: } else {
399: // recurse : call the writeBean method
400: writeBean(value);
401: }
402: }
403: }
404:
405: /**
406: * Returns the class attribute that will be written in the XML
407: * document for the given bean. Override to let custom a class
408: * loader returns a coded class name (like an URL).
409: * @param bean the bean
410: * @return the string representation of the specified bean class
411: * @since KBML 2.2
412: */
413: protected String getBeanClassName(Object bean) {
414: return bean.getClass().getName();
415: }
416:
417: /**
418: * Flushes the output stream.
419: * @exception IOException if an I/O error occurs
420: */
421: public void flush() throws IOException {
422: if (ostream != null) {
423: ostream.flush();
424: } else {
425: writer.flush();
426: }
427: }
428:
429: /**
430: * Closes the output stream.
431: * @exception IOException if an I/O error occurs.
432: * @exception IllegalStateException if KBML tag is opened and not closed.
433: */
434: public void close() throws IOException {
435: cache.clear();
436: if (ostream != null) {
437: ostream.close();
438: } else {
439: writer.close();
440: }
441: }
442:
443: /**
444: * Returns the internal cache used by the serializer. Use the
445: * bean as a key to get its associated ID.
446: */
447: public Hashtable getBeansCache() {
448: return beansCache;
449: }
450:
451: /**
452: * Initializes the PropertyEditorManager with some new PropertyEditors.
453: * @see Util#initializePropertyEditorManager
454: */
455: protected void initializePropertyEditorManager() {
456: Util.initializePropertyEditorManager();
457: }
458:
459: // write a valueArray start-tag with the specified ID
460: void writeValueArrayStartElement(String id) throws IOException {
461: write("<valueArray id=\"" + id + "\">");
462: }
463:
464: // write a valueArray end-tag
465: void writeValueArrayEndElement() throws IOException {
466: write("</valueArray>");
467: }
468:
469: // write a valueArray empty-element tag that references the specified ID
470: void writeValueArrayRefElement(String id) throws IOException {
471: write("<valueArray source=\"" + id + "\"/>");
472: }
473:
474: // write a element content
475: void writeContent(String s) throws IOException {
476: write(Util.toXML(s));
477: }
478:
479: // write a value start-tag
480: void writeValueStartElement() throws IOException {
481: write("<value>");
482: }
483:
484: // write a value start-tag with the specified ID
485: void writeValueStartElement(String id) throws IOException {
486: write("<value id=\"" + id + "\">");
487: }
488:
489: // write a value start-tag with the specified class and ID
490: void writeValueStartElement(Class _class, String id)
491: throws IOException {
492: write("<value class=\"" + _class.getName() + "\" id=\"" + id
493: + "\">");
494: }
495:
496: // write a value end-tag
497: void writeValueEndElement() throws IOException {
498: write("</value>");
499: }
500:
501: // write a value empty-element tag that references the specified ID
502: void writeValueRefElement(String id) throws IOException {
503: write("<value source=\"" + id + "\"/>");
504: }
505:
506: // write a property start-tag with the specified name
507: void writePropertyStartElement(String name) throws IOException {
508: write("<property name=\"" + name + "\">");
509: }
510:
511: // write a property end-tag
512: void writePropertyEndElement() throws IOException {
513: write("</property>");
514: }
515:
516: // write a bean start-tag with the specified class and ID
517: void writeBeanStartElement(String className, String id)
518: throws IOException {
519: write("<bean class=\"" + className + "\" id=\"" + id + "\">");
520: }
521:
522: // write a bean end-tag
523: void writeBeanEndElement() throws IOException {
524: write("</bean>");
525: }
526:
527: // write a null empty-element tag
528: void writeNullElement() throws IOException {
529: write("<null/>");
530: }
531:
532: // write a bean empty-element tag that references the specified ID
533: void writeBeanRefElement(String id) throws IOException {
534: write("<bean source=\"" + id + "\"/>");
535: }
536:
537: // returns a new unique ID.
538: String newID() {
539: return ID_PREFIX + (id++);
540: }
541:
542: /**
543: * Returns a new instance that corresponds to the specified bean
544: * object. This method is invoked each time the serializer needs a
545: * default bean instance.
546: * @param bean the bean used to get a new instance
547: *
548: * @exception IOException if the bean can't be instantiate by the
549: * <code>Beans.instanciate(ClassLoader, String)</code> method
550: *
551: * @exception ClassNotFoundException if the bean can't be
552: * found (check your CLASSPATH)
553: *
554: * @exception InstantiationException if the bean can't be
555: * instanciated (it's an interface or abstract class)
556: *
557: * @exception IllegalAccessException The bean class was
558: * found, but you do not have permission to load it.
559: *
560: * @exception NoSuchMethodError The zero-argument constructor of
561: * the bean was not found.
562: *
563: * @return a new bean instance used as default template
564: */
565: private Object getDefaultBeanInstance(Object bean)
566: throws ClassNotFoundException, InstantiationException,
567: IllegalAccessException, NoSuchMethodError, IOException {
568: Object defaultBeanInstance = null;
569: if ((serializationOptions & WRITE_DEFAULT_VALUES) == 0) {
570: defaultBeanInstance = cache.get(bean.getClass());
571: if (defaultBeanInstance == null) {
572: ClassLoader cl = bean.getClass().getClassLoader();
573: String className = bean.getClass().getName();
574: defaultBeanInstance = Beans.instantiate(cl, className);
575: cache.put(bean.getClass(), defaultBeanInstance);
576: }
577: }
578: return defaultBeanInstance;
579: }
580:
581: // write the specified string to the output stream.
582: void write(String s) throws IOException {
583: if (ostream != null) {
584: ostream.write(s.getBytes());
585: } else {
586: writer.write(s);
587: }
588: }
589:
590: /** The KBML version. */
591: public static final String VERSION = "2.3";
592: /** The prefix used to generate id */
593: static String ID_PREFIX = "KBML_";
594: /** The KBML copyright. */
595: static final String COPYRIGHT = "(c) Dyade 2000";
596:
597: static final String XML_DECLARATION = "<?xml version='1.0' encoding='UTF-8'?>";
598: static final String DOCTYPE = "<!DOCTYPE kbml SYSTEM \"http://www.inria.fr/koala/kbml/kbml20.dtd\">";
599: static final String START_ROOT_ELEMENT = "<kbml version=\""
600: + VERSION + "\" copyright=\"" + COPYRIGHT + "\">";
601: static final String END_ROOT_ELEMENT = "</kbml>";
602:
603: /**
604: * The handler interface for receiving no-fatal errors that may
605: * occur during the serialization process. If an application needs
606: * to customize error handling, it must implement this interface
607: * and then instanciate the serializer with correct arguments.
608: *
609: * <p>KBML serializer calls this handler instead of throwing an
610: * exception, it is up to the application to throw an exception
611: * when needed.
612: *
613: * @author Thierry.Kormann@sophia.inria.fr
614: */
615: public interface ErrorHandler {
616:
617: // called by writeBean
618:
619: /**
620: * Invoked when the default bean can not be instanciated.
621: * @param bean the bean
622: * @param ex the exception thrown
623: */
624: void instanciateBean(Object bean, IOException ex);
625:
626: /**
627: * Invoked when the default bean can not be instanciated.
628: * @param bean the bean
629: * @param ex the exception thrown
630: */
631: void instanciateBean(Object bean, IllegalAccessException ex);
632:
633: /**
634: * Invoked when the default bean can not be instanciated.
635: * @param bean the bean
636: * @param ex the exception thrown
637: */
638: void instanciateBean(Object bean, InstantiationException ex);
639:
640: /**
641: * Invoked when the default bean can not be instanciated.
642: * @param bean the bean
643: * @param err the error thrown
644: */
645: void instanciateBean(Object bean, NoSuchMethodError err);
646:
647: /**
648: * Invoked when a property value can not be gotten.
649: * @param bean the bean
650: * @param pd the property descriptor
651: * @param ex the exception thrown
652: */
653: void readMethod(Object bean, PropertyDescriptor pd,
654: IllegalAccessException ex);
655:
656: /**
657: * Invoked when a property value can not be gotten.
658: * @param bean the bean
659: * @param pd the property descriptor
660: * @param ex the exception thrown
661: */
662: void readMethod(Object bean, PropertyDescriptor pd,
663: InvocationTargetException ex);
664:
665: /**
666: * Invoked when the introspector can not get the BeanInfo.
667: * @param bean the bean
668: * @param ex the exception thrown
669: */
670: void introspector(Object bean, IntrospectionException ex);
671:
672: /**
673: * Invoked when no property descriptor is available a specific property.
674: * @param bean the bean
675: * @param pd the property descriptor
676: */
677: void propertyDescriptor(Object bean, PropertyDescriptor pd);
678:
679: // called by writeValue
680:
681: /**
682: * Invoked when a property editor can not be instanciated.
683: * @param pd the property descriptor
684: * @param ex the exception thrown
685: */
686: void propertyEditor(PropertyDescriptor pd,
687: IllegalAccessException ex);
688:
689: /**
690: * Invoked when a property editor can not be instanciated.
691: * @param pd the property descriptor
692: * @param ex the exception thrown
693: */
694: void propertyEditor(PropertyDescriptor pd,
695: InstantiationException ex);
696:
697: /**
698: * Invoked when a property editor can not be instanciated.
699: * @param pd the property descriptor
700: * @param err the error thrown
701: */
702: void propertyEditor(PropertyDescriptor pd, NoSuchMethodError err);
703:
704: /**
705: * Invoked when a property editor returns <code>null</code> for
706: * a specified string value.
707: * @param pd the property descriptor
708: * @param pe the property editor
709: */
710: void propertyEditor(PropertyDescriptor pd, PropertyEditor pe);
711: }
712: }
|