View Javadoc

1   /*
2   Copyright (C) 2000 - 2007 Grid Systems, S.A.
3   
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License, version 2, as
6   published by the Free Software Foundation.
7   
8   This program is distributed in the hope that it will be useful,
9   but WITHOUT ANY WARRANTY; without even the implied warranty of
10  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  GNU General Public License for more details.
12  
13  You should have received a copy of the GNU General Public License
14  along with this program; if not, write to the Free Software
15  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16  */
17  package com.gridsystems;
18  
19  import java.io.IOException;
20  import java.io.ObjectStreamField;
21  import java.io.Writer;
22  import java.lang.reflect.Array;
23  import java.lang.reflect.Field;
24  import java.lang.reflect.Modifier;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.Comparator;
30  import java.util.LinkedHashMap;
31  import java.util.LinkedHashSet;
32  import java.util.Map;
33  import java.util.SortedMap;
34  import java.util.SortedSet;
35  import java.util.TreeMap;
36  import java.util.Vector;
37  
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  
41  /**
42   * XML Serializer.
43   * <p>
44   * The main use of this class is to obtain a human-readable representation of
45   * a set of objects, for debugging, tests, etc.
46   * 
47   * <h3>Usage</h3>
48   *
49   * <pre>
50   *   java.io.Writer writer = ...;
51   *   GridXMLSerializer ser = new GridXMLSerializer(writer);
52   *   try {
53   *     ser.begin();
54   *     ser.write(obj1);
55   *     ser.write(obj2);
56   *     ...
57   *     ser.end();
58   *   } finally {
59   *     writer.close();
60   *   }
61   * </pre>
62   *
63   * For each object, the serialization works as follows:
64   *
65   * <ol>
66   *   <li>If a static field is found with the following structure:
67   *       <pre>
68   *         private static final ObjectStreamField[] xmlPersistentFields = {
69   *           new ObjectStreamField("myField", myClass.class)
70   *         };
71   *       </pre>
72   *       Only the specified fields in this array will be serialized.</li>
73   *   <li>Otherwise, all non-static fields will be serialized, including
74   *       those from ancestor classes.</li>
75   * </ol>
76   *
77   * @author Gaston Freire Amoedo
78   * @author Rodrigo Ruiz
79   * @version 2.0
80   */
81  public class GridXMLSerializer {
82  
83    /**
84     * Class logger.
85     */
86    private static Log log = LogFactory.getLog(GridXMLSerializer.class);
87  
88    /**
89     * XML Indentation prefix.
90     */
91    private static final String SPACER = "  ";
92  
93    /**
94     * Target writer for output.
95     */
96    private Writer outWriter = null;
97  
98    /**
99     * Creates a new instance.
100    *
101    * @param output Writer to write to
102    */
103   public GridXMLSerializer(Writer output) {
104     outWriter = output;
105 
106     log.debug("New GridXMLSerializer created");
107   }
108 
109 
110   /**
111    * Starts serialization, generating the appropriate XML headers.
112    *
113    * @throws IOException If an I/O error occurs writing to the writer
114    */
115   public void begin() throws IOException {
116     outWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
117     outWriter.write("<main>\n");
118 
119     log.debug("Serialization started");
120   }
121 
122 
123   /**
124    * Termina la serializacion, generando el cierre de las cabeceras XML correspondientes
125    * Despues de esta operacion se recomienda hacer flush() y close() del writer.
126    *
127    * @throws IOException If an I/O error occurs writing to the writer
128    */
129   public void end() throws IOException {
130     outWriter.write("</main>\n");
131 
132     log.debug("Serialization ended");
133   }
134 
135   /**
136    * Serializa en XML el objeto pasado por parametro.
137    *
138    * @param obj objeto a serializar en XML
139    * @param excluded para excluir arboles xml que contengan esta palabra,
140    *                ya sea en el nodo raiz como en alguno de sus nodos
141    *                hijos (con un nivel de profundidad como maximo)
142    *
143    * @throws IOException If an I/O error occurs writing to the writer
144   */
145   public void write(Object obj, String excluded) throws IOException {
146     outWriter.write(serialize(obj, SPACER, excluded));
147   }
148 
149   /**
150    * Serializa en XML el objeto pasado por parametro.
151    *
152    * @param obj objeto a serializar en XML
153    *
154    * @throws IOException If an I/O error occurs writing to the writer
155    */
156   public void write(Object obj) throws IOException {
157     outWriter.write(serialize(obj, SPACER, null));
158   }
159 
160 
161   /**
162    * Obtiene las super clases de una dada (excepto java.lang.Object).
163    *
164    * @param origin clase origen (hija)
165    * @return array de clases, con origin en [0]
166    */
167   private static Class[] getSuperClasses(Class origin) {
168 
169     log.debug("getting super classes of " + origin.getName());
170 
171     if (origin == Object.class) {
172       // origin no tiene superclases
173       return new Class[0];
174     }
175     // Cuenta el numero de superclases (excepto Object)
176     Class aux = origin;
177     int classDepth = 0;
178     do {
179       classDepth++;
180       aux = aux.getSuperclass();
181     } while (aux != Object.class);
182     // Almacena las superclases en un array
183     Class[] superClasses = new Class[classDepth];
184     aux = origin;
185     int i = 0;
186     do {
187       superClasses[i++] = aux;
188       aux = aux.getSuperclass();
189     } while (aux != Object.class);
190 
191     log.debug("super classes of " + origin.getName() + " got successfully");
192 
193     return superClasses;
194   }
195 
196 
197   /**
198    * Devuelve todos los campos del objeto a convertir a XML.
199    * Si la clase del objeto tiene una variable
200    *   private static final ObjectStreamField[] xmlPersistentFields
201    * toma de ella la lista de campos que deben convertirse a XML
202    * En caso contrario, toma todos los campos excepto los estaticos
203    *
204    * @param obj objeto del que queremos obtener la lista de campos a convertir a XML
205    * @return array con los campos obtenidos
206    */
207   private static Field[] getFields(Object obj) {
208     Class origin = obj.getClass();
209     Field[] fieldsArray;
210     Vector<Field> fieldsVector = new Vector<Field>();
211     ObjectStreamField[] xmlFields = null;
212     Class[] superClasses = getSuperClasses(origin);
213     boolean includeField;
214 
215     log.debug("getting serializable fields from " + origin.getName());
216 
217     // Busca una declaracion de campos persistentes para xml
218     try {
219       Field specialField = origin.getDeclaredField("xmlPersistentFields");
220       specialField.setAccessible(true);
221       xmlFields = (ObjectStreamField[]) specialField.get(obj);
222     } catch (NoSuchFieldException e) {
223       // El campo no existe, todos los campos se convierten a XML
224     } catch (IllegalAccessException e) {
225       // No se producira por el Field.setAccessible
226     }
227     // Recorre la cadena de clases obteniendo sus campos
228     for (int i = 0; i < superClasses.length; i++) {
229       fieldsArray = superClasses[i].getDeclaredFields();
230       // Filtra fieldsArray para que contenga solo los campos deseados
231       for (int j = 0; j < fieldsArray.length; j++) {
232         // Determina si hay que convertir el campo a XML
233         if (xmlFields == null) {
234           includeField = !Modifier.isStatic(fieldsArray[j].getModifiers());
235         } else {
236           includeField = false;
237           for (int k = 0; k < xmlFields.length; k++) {
238             if (fieldsArray[j].getName().equals(xmlFields[k].getName())) {
239               includeField = true;
240             }
241           }
242         }
243         if (includeField) {
244           fieldsVector.add(fieldsArray[j]);
245         }
246       }
247     }
248     // Junta todos los campos obtenidos
249     fieldsArray = new Field[fieldsVector.size()];
250     fieldsVector.copyInto(fieldsArray);
251     //Java 1.2 security MAGIC! para evitar IllegalAccessException
252     Arrays.sort(fieldsArray, new Comparator<Field>() {
253       public int compare(Field f1, Field f2) {
254         String n1 = (f1 == null) ? null : f1.getName();
255         String n2 = (f2 == null) ? null : f2.getName();
256         if (n1 == null) {
257           return -1;
258         } else if (n2 == null) {
259           return 1;
260         } else {
261           return n1.compareTo(n2);
262         }
263       }
264     });
265     Field.setAccessible(fieldsArray, true);
266 
267     log.debug("serializable fields from " + origin.getName() + " got successfully");
268 
269     return fieldsArray;
270   }
271 
272   /**
273    * @param c Class to check
274    * @return true if the class is a basic type, false otherwise
275    */
276   private static boolean isBasicType(Class c) {
277     if (Number.class.isAssignableFrom(c) || CharSequence.class.isAssignableFrom(c)) {
278       return true;
279     } else if (c == Boolean.class || c == Character.class) {
280       return true;
281     }
282     return false;
283   }
284 
285   /**
286    * Indirect recursion serialization through <tt>serializeField</tt>.
287    *
288    * @param obj Object to serialize
289    * @param indent XML indentation prefix
290    * @param excluded XML trees containing this word are excluded (with a maximum
291    *                 depth level of one)
292    * @return cadena con el objeto serializado en XML
293    */
294   private static String serialize(Object obj, String indent, String excluded) {
295     StringBuffer output = new StringBuffer();
296     Class objectClass = obj.getClass();
297 
298     log.debug("starting serialization of object from class " + objectClass.getName());
299 
300     output.append(indent);
301     output.append("<object class=\"");
302     // Si la clase es array debe indicar el tipo de sus componentes
303     if (objectClass.isArray()) {
304       output.append("Array\" componentType=\"");
305       if (objectClass.getComponentType().isArray()) {
306         output.append("Array");
307       } else {
308         output.append(objectClass.getComponentType().getName());
309       }
310     } else {
311       output.append(objectClass.getName());
312     }
313     output.append("\">");
314 
315     // El comportamiento varia en funcion de la clase del objeto
316     if (isBasicType(objectClass)) {
317       // El objeto es un wrapper, un String o una clase numerica
318       output.append(encodeXML(obj.toString()));
319       output.append("</object>\n");
320     } else {
321       // Hay que "desglosar" el objeto
322       output.append("\n");
323       if (objectClass.isArray()) {
324         serializeArray(obj, indent, excluded, output, objectClass);
325       } else {
326         //superClasses = getSuperClasses(objectClass);
327         //rootClass = superClasses[superClasses.length - 1];
328         if (obj instanceof Collection) {
329           serializeCollection((Collection)obj, indent, excluded, output);
330         } else if (obj instanceof Map) {
331           serializeMap((Map)obj, indent, excluded, output);
332         } else {
333           serializeObject(obj, indent, excluded, output);
334         }
335       }
336       output.append(indent);
337       output.append("</object>\n");
338     }
339 
340     log.debug("serialization of object from class " + objectClass.getName()
341                + " successfully");
342 
343     return output.toString();
344   }
345 
346 
347   /**
348    * @param obj object to serialize
349    * @param indent indentation
350    * @param excluded excluded elements
351    * @param output StringBuffer to write to
352    * @param objectClass object class
353    */
354   private static void serializeArray(Object obj, String indent, String excluded,
355                                      StringBuffer output, Class objectClass) {
356     Object item;
357     boolean exclude = false;
358     // El objeto es un array: hay que mostrar sus componentes
359     // Determina si el tipo de los componentes es primitivo (caso especial)
360     log.debug("serializing array...");
361     if (objectClass.getComponentType().isPrimitive()) {
362       // Comprobamos que ningun componente del array sea la cadena a excluir
363       for (int i = 0; i < Array.getLength(obj); i++) {
364         item = Array.get(obj, i);
365         if (item.toString().equalsIgnoreCase(excluded)) {
366           exclude = true;
367           break;
368         }
369       }
370       if (!exclude) {
371         for (int i = 0; i < Array.getLength(obj); i++) {
372           item = Array.get(obj, i);
373           output.append(indent + SPACER);
374           output.append("<arrayItem index=\"");
375           output.append(i);
376           output.append("\">");
377           output.append(encodeXML(item.toString()));
378           output.append("</arrayItem>\n");
379         }
380       }
381     } else {
382       for (int i = 0; i < Array.getLength(obj); i++) {
383         item = Array.get(obj, i);
384         output.append(indent + SPACER);
385         output.append("<arrayItem index=\"");
386         output.append(i);
387         output.append("\">\n");
388         if (item != null) {
389           output.append(serialize(item, indent + SPACER + SPACER, excluded));
390         }
391         output.append(indent + SPACER);
392         output.append("</arrayItem>\n");
393       }
394     }
395     log.debug("...array serialized");
396   }
397 
398 
399   /**
400    * @param obj the object to serialize
401    * @param indent indentation
402    * @param excluded excluded elements
403    * @param output StringBuffer to write to
404    */
405   private static void serializeObject(Object obj, String indent, String excluded,
406                                       StringBuffer output) {
407 
408     log.debug("serializing " + obj.getClass().getName() + "...");
409 
410     Field[] fields = getFields(obj);
411     // Check the instance for exclusion
412     for (int i = 0; i < fields.length; i++) {
413       if (fields[i].getType().isPrimitive() && fields[i].toString().equals(excluded)) {
414         log.debug("...object not serialized. Excluded because it has a "
415             + excluded + " component");
416         return;
417       }
418     }
419 
420     for (int i = 0; i < fields.length; i++) {
421       output.append(serializeField(obj, fields[i], indent + SPACER, excluded));
422     }
423   }
424 
425 
426   /**
427    * @param map the map to serialize
428    * @param indent indentation
429    * @param excluded excluded elements
430    * @param output StringBuffer to write to
431    */
432   private static void serializeMap(Map map, String indent, String excluded,
433                                    StringBuffer output) {
434     Object item;
435     // El objeto es un "mapeo": hay que mostrar la relacion clave-valor
436     log.debug("serializing map...");
437 
438     if (excluded != null) {
439       if (map.containsKey(excluded) || map.containsValue(excluded)) {
440         log.debug("...map not serialized. Excluded because it has a "
441             + excluded + " component");
442         return;
443       }
444     }
445 
446     // Store the keys in a sorted set to facilitate later comparisons
447     Map sorted = sort(map);
448 
449     final String spacer = indent + "      ";
450 
451     for (Object key : sorted.keySet()) {
452       Map myMap = (Map) map;
453       item = myMap.get(key);
454       output.append(indent).append("  <mapItem>\n");
455       output.append(indent).append("    <key>\n");
456       output.append(serialize(key, spacer, excluded));
457       output.append(indent).append("    </key>\n");
458       if (item != null) {
459         output.append(indent).append("    <value>\n");
460         output.append(serialize(item, spacer, excluded));
461         output.append(indent).append("    </value>\n");
462       }
463       output.append(indent).append("  </mapItem>\n");
464     }
465     log.debug("...map serialized");
466   }
467 
468 
469   /**
470    * @param col the Collection to serialize
471    * @param indent the indentation
472    * @param excluded excluded elements
473    * @param output the StringBuffer to write to
474    */
475   private static void serializeCollection(Collection col,
476     String indent, String excluded, StringBuffer output) {
477 
478     // El objeto es una coleccion: hay que mostrar sus elementos, si no
479     // contiene ningun elemento de tipo String con valor igual al excluido
480     log.debug("serializing collection...");
481 
482     // Filter instances containing 'excluded'
483     if (excluded != null && col.contains(excluded)) {
484       log.debug("...collection not serialized. Excluded because it has a "
485           + excluded + " component");
486       return;
487     }
488 
489     // Sort "unsorted" collections to allow easier comparisons
490     Collection sorted = sort(col);
491     for (Object item : sorted) {
492       if (item != null) {
493         output.append(indent).append(SPACER).append("<collectionItem>\n");
494         output.append(serialize(item, indent + SPACER + SPACER, excluded));
495         output.append(indent).append(SPACER).append("</collectionItem>\n");
496       }
497     }
498     log.debug("...collection serialized");
499   }
500 
501 
502   /**
503    * Serializa un campo de un objeto.
504    * Realiza la recursion si no es un tipo primitivo
505    *
506    * @param obj Objeto propietario del campo
507    * @param classField campo a serializar
508    * @param indent cadena de indentacion a emplear
509    * @param excluded para excluir arboles xml que contengan esta palabra,
510    *                ya sea en el nodo raiz como en alguno de sus nodos
511    *                hijos (con un nivel de profundidad como maximo)
512    * @return cadena con el campo serializado
513    */
514   private static String serializeField(Object obj, Field classField,
515     String indent, String excluded) {
516 
517 
518     log.debug("starting serialization of field " + classField.getName()
519               + " from class " + obj.getClass().getName());
520 
521     StringBuffer output = new StringBuffer();
522     try {
523       Object field = classField.get(obj);
524 
525       output.append(indent).append("<field name=\"");
526       output.append(classField.getName());
527       if (field != null && classField.getType().isPrimitive()) {
528         output.append("\" type=\"");
529         output.append(classField.getType().getName());
530       }
531       output.append("\">");
532 
533       if (field == null) {
534         output.append("</field>\n");
535       } else if (classField.getType().isPrimitive()) {
536         output.append(encodeXML(field.toString()) + "</field>\n");
537       } else {
538         output.append("\n");
539         output.append(serialize(field, indent + SPACER, excluded));
540         output.append(indent).append("</field>\n");
541       }
542 
543       log.debug("serialization of field " + classField.getName()
544                 + " from class " + obj.getClass().getName() + " successful");
545     } catch (IllegalAccessException e) {
546       log.error("Field " + obj.getClass().getName()
547         + "#" + classField.getName() + " not available.");
548     }
549     return output.toString();
550   }
551 
552 
553 
554   /**
555    * Codifica los caracteres susceptibles de confundir el codigo xml.
556    *
557    * @param in Cadena con los caracteres a sustituir
558    *
559    * @return a String containing the encoded data
560    */
561   public static String encodeXML(String in) {
562     StringBuffer out = new StringBuffer(in.length());
563     for (int i = 0; i < in.length(); i++) {
564       char c = in.charAt(i);
565       switch (c) {
566         case '&':
567           out.append("&amp;");
568           break;
569         case '<':
570           out.append("&lt;");
571           break;
572         case '>':
573           out.append("&gt;");
574           break;
575         case '\'':
576           out.append("&apos;");
577           break;
578         case '"':
579           out.append("&quot;");
580           break;
581         default:
582           out.append(c);
583       }
584     }
585     return out.toString();
586   }
587 
588   /**
589    * Tries to sort the elements in a list, assuming they are Comparable.
590    *
591    * @param c The elements to sort.
592    *
593    * @return The ordered list (if no exception was thrown
594    * when trying to compare the elements) or the original one (if an exception
595    * did made the sorting impossible.
596    */
597   @SuppressWarnings("unchecked")
598   private static Collection sort(Collection c) {
599     try {
600       if (c instanceof SortedSet || c instanceof LinkedHashSet) {
601         return c;
602       } else {
603         ArrayList list = new ArrayList(c);
604         Collections.sort(list);
605         return list;
606       }
607     } catch (Throwable t) {
608       // We do not want to throw any kind of error from here.
609       return c;
610     }
611   }
612 
613   /**
614    * Sorts a map, if it is possible.
615    * <p>
616    * If the map is "sorted" it returns map itself. If not, it tries to
617    * create a TreeMap and fill it with the contents of map.
618    * <p>
619    * If an error occurs, the original map instance is returned
620    *
621    * @param map The map to be sorted
622    * @return A sorted instance with the same contents as map
623    */
624   @SuppressWarnings("unchecked")
625   private static Map sort(Map map) {
626     try {
627       if (map instanceof SortedMap || map instanceof LinkedHashMap) {
628         return map;
629       } else {
630         return new TreeMap(map);
631       }
632     } catch (Exception e) {
633       return map;
634     }
635   }
636 }