View Javadoc

1   /*
2    * Copyright 2006 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.oxm.xstream;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.InputStreamReader;
22  import java.io.OutputStream;
23  import java.io.OutputStreamWriter;
24  import java.io.Reader;
25  import java.io.Writer;
26  import java.util.Iterator;
27  import java.util.Map;
28  import javax.xml.stream.XMLEventReader;
29  import javax.xml.stream.XMLEventWriter;
30  import javax.xml.stream.XMLStreamException;
31  import javax.xml.stream.XMLStreamReader;
32  import javax.xml.stream.XMLStreamWriter;
33  
34  import com.thoughtworks.xstream.XStream;
35  import com.thoughtworks.xstream.converters.Converter;
36  import com.thoughtworks.xstream.converters.ConverterMatcher;
37  import com.thoughtworks.xstream.converters.SingleValueConverter;
38  import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
39  import com.thoughtworks.xstream.io.HierarchicalStreamReader;
40  import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
41  import com.thoughtworks.xstream.io.xml.CompactWriter;
42  import com.thoughtworks.xstream.io.xml.DomReader;
43  import com.thoughtworks.xstream.io.xml.DomWriter;
44  import com.thoughtworks.xstream.io.xml.QNameMap;
45  import com.thoughtworks.xstream.io.xml.SaxWriter;
46  import com.thoughtworks.xstream.io.xml.StaxReader;
47  import com.thoughtworks.xstream.io.xml.StaxWriter;
48  import com.thoughtworks.xstream.io.xml.XmlFriendlyReplacer;
49  import com.thoughtworks.xstream.io.xml.XppReader;
50  import org.w3c.dom.Document;
51  import org.w3c.dom.Element;
52  import org.w3c.dom.Node;
53  import org.xml.sax.ContentHandler;
54  import org.xml.sax.InputSource;
55  import org.xml.sax.XMLReader;
56  import org.xml.sax.ext.LexicalHandler;
57  
58  import org.springframework.beans.propertyeditors.ClassEditor;
59  import org.springframework.oxm.AbstractMarshaller;
60  import org.springframework.oxm.XmlMappingException;
61  import org.springframework.util.ObjectUtils;
62  import org.springframework.util.StringUtils;
63  import org.springframework.xml.stream.StaxEventContentHandler;
64  import org.springframework.xml.stream.XmlEventStreamReader;
65  
66  /**
67   * Implementation of the <code>Marshaller</code> interface for XStream. By default, XStream does not require any further
68   * configuration, though class aliases can be used to have more control over the behavior of XStream.
69   * <p/>
70   * Due to XStream's API, it is required to set the encoding used for writing to outputstreams. It defaults to
71   * <code>UTF-8</code>.
72   * <p/>
73   * <b>Note</b> that XStream is an XML serialization library, not a data binding library. Therefore, it has limited
74   * namespace support. As such, it is rather unsuitable for usage within Web services.
75   *
76   * @author Peter Meijer
77   * @author Arjen Poutsma
78   * @see #setEncoding(String)
79   * @see #DEFAULT_ENCODING
80   * @see #setAliases(Map)
81   * @see #setConverters(ConverterMatcher[])
82   * @since 1.0.0
83   */
84  public class XStreamMarshaller extends AbstractMarshaller {
85  
86      /** The default encoding used for stream access. */
87      public static final String DEFAULT_ENCODING = "UTF-8";
88  
89      private XStream xstream = new XStream();
90  
91      private String encoding;
92  
93      private Class[] supportedClasses;
94  
95      /** Specialized driver to be used with stream readers and writers */
96      private HierarchicalStreamDriver streamDriver;
97  
98      /**
99       * Returns the encoding to be used for stream access. If this property is not set, the default encoding is used.
100      *
101      * @see #DEFAULT_ENCODING
102      */
103     public String getEncoding() {
104         return encoding != null ? encoding : DEFAULT_ENCODING;
105     }
106 
107     /**
108      * Sets the encoding to be used for stream access. If this property is not set, the default encoding is used.
109      *
110      * @see #DEFAULT_ENCODING
111      */
112     public void setEncoding(String encoding) {
113         this.encoding = encoding;
114     }
115 
116     /** Returns the XStream instance used by this marshaller. */
117     public XStream getXStream() {
118         return xstream;
119     }
120 
121     /**
122      * Sets the XStream mode.
123      *
124      * @see XStream#XPATH_REFERENCES
125      * @see XStream#ID_REFERENCES
126      * @see XStream#NO_REFERENCES
127      */
128     public void setMode(int mode) {
129         getXStream().setMode(mode);
130     }
131 
132     /**
133      * Sets the classes supported by this marshaller. If this property is empty (the default), all classes are
134      * supported.
135      *
136      * @see #supports(Class)
137      */
138     public void setSupportedClasses(Class[] supportedClasses) {
139         this.supportedClasses = supportedClasses;
140     }
141 
142     /**
143      * Sets the <code>Converters</code> or <code>SingleValueConverters</code> to be registered with the
144      * <code>XStream</code> instance.
145      *
146      * @see Converter
147      * @see SingleValueConverter
148      */
149     public void setConverters(ConverterMatcher[] converters) {
150         for (int i = 0; i < converters.length; i++) {
151             if (converters[i] instanceof Converter) {
152                 getXStream().registerConverter((Converter) converters[i], i);
153             }
154             else if (converters[i] instanceof SingleValueConverter) {
155                 getXStream().registerConverter((SingleValueConverter) converters[i], i);
156             }
157             else {
158                 throw new IllegalArgumentException("Invalid ConverterMatcher [" + converters[i] + "]");
159             }
160         }
161     }
162 
163     /** Sets the XStream hierarchical stream driver to be used with stream readers and writers */
164     public void setStreamDriver(HierarchicalStreamDriver streamDriver) {
165         this.streamDriver = streamDriver;
166     }
167 
168     /**
169      * Set a alias/type map, consisting of string aliases mapped to <code>Class</code> instances (or Strings to be
170      * converted to <code>Class</code> instances).
171      *
172      * @see org.springframework.beans.propertyeditors.ClassEditor
173      */
174     public void setAliases(Map aliases) {
175         for (Iterator iterator = aliases.entrySet().iterator(); iterator.hasNext();) {
176             Map.Entry entry = (Map.Entry) iterator.next();
177             // Check whether we need to convert from String to Class.
178             Class type;
179             if (entry.getValue() instanceof Class) {
180                 type = (Class) entry.getValue();
181             }
182             else {
183                 ClassEditor editor = new ClassEditor();
184                 editor.setAsText(String.valueOf(entry.getValue()));
185                 type = (Class) editor.getValue();
186             }
187             addAlias((String) entry.getKey(), type);
188         }
189     }
190 
191     /**
192      * Adds an alias for the given type.
193      *
194      * @param name alias to be used for the type
195      * @param type the type to be aliased
196      */
197     public void addAlias(String name, Class type) {
198         getXStream().alias(name, type);
199     }
200 
201     /**
202      * Sets types to use XML attributes for.
203      *
204      * @see XStream#useAttributeFor(Class)
205      */
206     public void setUseAttributeForTypes(Class[] types) {
207         for (int i = 0; i < types.length; i++) {
208             getXStream().useAttributeFor(types[i]);
209         }
210     }
211 
212     /**
213      * Sets the types to use XML attributes for. The given map can contain either <code>&lt;String, Class&gt;</code>
214      * pairs, in which case {@link XStream#useAttributeFor(String,Class)} is called, or <code>&lt;Class,
215      * String&gt;</code> pairs, which results in {@link XStream#useAttributeFor(Class,String)}.
216      */
217     public void setUseAttributeFor(Map attributes) {
218         for (Iterator iterator = attributes.entrySet().iterator(); iterator.hasNext();) {
219             Map.Entry entry = (Map.Entry) iterator.next();
220             if (entry.getKey() instanceof String && entry.getValue() instanceof Class) {
221                 getXStream().useAttributeFor((String) entry.getKey(), (Class) entry.getValue());
222             }
223             else if (entry.getKey() instanceof Class && entry.getValue() instanceof String) {
224                 getXStream().useAttributeFor((Class) entry.getKey(), (String) entry.getValue());
225             }
226             else {
227                 throw new IllegalArgumentException("Invalid attribute key and value pair. " +
228                         "'useAttributesFor' property takes either a <String, Class> map or a <Class, String> map");
229             }
230         }
231     }
232 
233     /**
234      * Adds an implicit Collection for the given type.
235      *
236      * @see XStream#addImplicitCollection(Class, String)
237      */
238     public void addImplicitCollection(String name, Class type) {
239         getXStream().addImplicitCollection(type, name);
240     }
241 
242     /**
243      * Set a implicit colletion/type map, consisting of string implicit collection mapped to <code>Class</code>
244      * instances (or Strings to be converted to <code>Class</code> instances).
245      *
246      * @see XStream#addImplicitCollection(Class, String)
247      */
248     public void setImplicitCollection(Map implicitCollection) {
249         for (Iterator iterator = implicitCollection.entrySet().iterator(); iterator.hasNext();) {
250             Map.Entry entry = (Map.Entry) iterator.next();
251             // Check whether we need to convert from String to Class.
252             Class type;
253             if (entry.getValue() instanceof Class) {
254                 type = (Class) entry.getValue();
255             }
256             else {
257                 ClassEditor editor = new ClassEditor();
258                 editor.setAsText(String.valueOf(entry.getValue()));
259                 type = (Class) editor.getValue();
260             }
261             addImplicitCollection((String) entry.getKey(), type);
262         }
263     }
264 
265     /**
266      * Adds an omitted field for the given type.
267      *
268      * @param type      the type to be containing the field
269      * @param fieldName field to omitt
270      * @see XStream#omitField(Class, String)
271      */
272     public void addOmittedField(Class type, String fieldName) {
273         getXStream().omitField(type, fieldName);
274     }
275 
276     /**
277      * Sets a ommited field map, consisting of <code>Class</code> instances (or Strings to be converted to
278      * <code>Class</code> instances) mapped to comma separated field names.
279      *
280      * @see XStream#omitField(Class, String)
281      */
282     public void setOmittedFields(Map omittedFields) {
283         for (Iterator iterator = omittedFields.entrySet().iterator(); iterator.hasNext();) {
284             Map.Entry entry = (Map.Entry) iterator.next();
285             // Check whether we need to convert from String to Class.
286             Class type;
287             if (entry.getKey() instanceof Class) {
288                 type = (Class) entry.getKey();
289             }
290             else {
291                 ClassEditor editor = new ClassEditor();
292                 editor.setAsText(String.valueOf(entry.getKey()));
293                 type = (Class) editor.getValue();
294             }
295             // add each omitted field for the current type
296             String fieldsString = (String) entry.getValue();
297             String[] fields = StringUtils.commaDelimitedListToStringArray(fieldsString);
298             for (int i = 0; i < fields.length; i++) {
299                 addOmittedField(type, fields[i]);
300             }
301         }
302     }
303 
304     public boolean supports(Class clazz) {
305         if (ObjectUtils.isEmpty(supportedClasses)) {
306             return true;
307         }
308         else {
309             for (int i = 0; i < supportedClasses.length; i++) {
310                 if (supportedClasses[i].isAssignableFrom(clazz)) {
311                     return true;
312                 }
313             }
314             return false;
315         }
316     }
317 
318     /**
319      * Convert the given XStream exception to an appropriate exception from the <code>org.springframework.oxm</code>
320      * hierarchy.
321      * <p/>
322      * The default implementation delegates to <code>XStreamUtils</code>. Can be overridden in subclasses.
323      *
324      * @param ex          exception that occured
325      * @param marshalling indicates whether the exception occurs during marshalling (<code>true</code>), or
326      *                    unmarshalling (<code>false</code>)
327      * @return the corresponding <code>XmlMappingException</code> instance
328      * @see XStreamUtils#convertXStreamException(Exception,boolean)
329      */
330     public XmlMappingException convertXStreamException(Exception ex, boolean marshalling) {
331         return XStreamUtils.convertXStreamException(ex, marshalling);
332     }
333 
334     //
335     // Marshalling
336     //
337 
338     /**
339      * Marshals the given graph to the given XStream HierarchicalStreamWriter. Converts exceptions using
340      * <code>convertXStreamException</code>.
341      */
342     private void marshal(Object graph, HierarchicalStreamWriter streamWriter) {
343         try {
344             getXStream().marshal(graph, streamWriter);
345         }
346         catch (Exception ex) {
347             throw convertXStreamException(ex, true);
348         }
349     }
350 
351     protected void marshalDomNode(Object graph, Node node) throws XmlMappingException {
352         HierarchicalStreamWriter streamWriter;
353         if (node instanceof Document) {
354             streamWriter = new DomWriter((Document) node);
355         }
356         else if (node instanceof Element) {
357             streamWriter = new DomWriter((Element) node, node.getOwnerDocument(), new XmlFriendlyReplacer());
358         }
359         else {
360             throw new IllegalArgumentException("DOMResult contains neither Document nor Element");
361         }
362         marshal(graph, streamWriter);
363     }
364 
365     protected void marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter) throws XmlMappingException {
366         ContentHandler contentHandler = new StaxEventContentHandler(eventWriter);
367         marshalSaxHandlers(graph, contentHandler, null);
368     }
369 
370     protected void marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter) throws XmlMappingException {
371         try {
372             marshal(graph, new StaxWriter(new QNameMap(), streamWriter));
373         }
374         catch (XMLStreamException ex) {
375             throw convertXStreamException(ex, true);
376         }
377     }
378 
379     protected void marshalOutputStream(Object graph, OutputStream outputStream)
380             throws XmlMappingException, IOException {
381         marshalWriter(graph, new OutputStreamWriter(outputStream, getEncoding()));
382     }
383 
384     protected void marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)
385             throws XmlMappingException {
386         SaxWriter saxWriter = new SaxWriter();
387         saxWriter.setContentHandler(contentHandler);
388         marshal(graph, saxWriter);
389     }
390 
391     protected void marshalWriter(Object graph, Writer writer) throws XmlMappingException, IOException {
392         if (streamDriver != null) {
393             marshal(graph, streamDriver.createWriter(writer));
394         }
395         else {
396             marshal(graph, new CompactWriter(writer));
397         }
398     }
399 
400     //
401     // Unmarshalling
402     //
403 
404     private Object unmarshal(HierarchicalStreamReader streamReader) {
405         try {
406             return getXStream().unmarshal(streamReader);
407         }
408         catch (Exception ex) {
409             throw convertXStreamException(ex, false);
410         }
411     }
412 
413     protected Object unmarshalDomNode(Node node) throws XmlMappingException {
414         HierarchicalStreamReader streamReader;
415         if (node instanceof Document) {
416             streamReader = new DomReader((Document) node);
417         }
418         else if (node instanceof Element) {
419             streamReader = new DomReader((Element) node);
420         }
421         else {
422             throw new IllegalArgumentException("DOMSource contains neither Document nor Element");
423         }
424         return unmarshal(streamReader);
425     }
426 
427     protected Object unmarshalXmlEventReader(XMLEventReader eventReader) throws XmlMappingException {
428         try {
429             XMLStreamReader streamReader = new XmlEventStreamReader(eventReader);
430             return unmarshalXmlStreamReader(streamReader);
431         }
432         catch (XMLStreamException ex) {
433             throw convertXStreamException(ex, false);
434         }
435     }
436 
437     protected Object unmarshalXmlStreamReader(XMLStreamReader streamReader) throws XmlMappingException {
438         return unmarshal(new StaxReader(new QNameMap(), streamReader));
439     }
440 
441     protected Object unmarshalInputStream(InputStream inputStream) throws XmlMappingException, IOException {
442         return unmarshalReader(new InputStreamReader(inputStream, getEncoding()));
443     }
444 
445     protected Object unmarshalReader(Reader reader) throws XmlMappingException, IOException {
446         if (streamDriver != null) {
447             return unmarshal(streamDriver.createReader(reader));
448         }
449         else {
450             return unmarshal(new XppReader(reader));
451         }
452     }
453 
454     protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)
455             throws XmlMappingException, IOException {
456         throw new UnsupportedOperationException(
457                 "XStreamMarshaller does not support unmarshalling using SAX XMLReaders");
458     }
459 
460 
461 }