1 /*
2  * Copyright 2002-2012 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.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import javax.xml.stream.XMLEventReader;
30 import javax.xml.stream.XMLEventWriter;
31 import javax.xml.stream.XMLStreamException;
32 import javax.xml.stream.XMLStreamReader;
33 import javax.xml.stream.XMLStreamWriter;
34 
35 import com.thoughtworks.xstream.XStream;
36 import com.thoughtworks.xstream.converters.ConversionException;
37 import com.thoughtworks.xstream.converters.Converter;
38 import com.thoughtworks.xstream.converters.ConverterMatcher;
39 import com.thoughtworks.xstream.converters.SingleValueConverter;
40 import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
41 import com.thoughtworks.xstream.io.HierarchicalStreamReader;
42 import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
43 import com.thoughtworks.xstream.io.StreamException;
44 import com.thoughtworks.xstream.io.xml.CompactWriter;
45 import com.thoughtworks.xstream.io.xml.DomReader;
46 import com.thoughtworks.xstream.io.xml.DomWriter;
47 import com.thoughtworks.xstream.io.xml.QNameMap;
48 import com.thoughtworks.xstream.io.xml.SaxWriter;
49 import com.thoughtworks.xstream.io.xml.StaxReader;
50 import com.thoughtworks.xstream.io.xml.StaxWriter;
51 import com.thoughtworks.xstream.io.xml.XmlFriendlyReplacer;
52 import com.thoughtworks.xstream.io.xml.XppReader;
53 import com.thoughtworks.xstream.mapper.CannotResolveClassException;
54 import org.w3c.dom.Document;
55 import org.w3c.dom.Element;
56 import org.w3c.dom.Node;
57 import org.xml.sax.ContentHandler;
58 import org.xml.sax.InputSource;
59 import org.xml.sax.XMLReader;
60 import org.xml.sax.ext.LexicalHandler;
61 
62 import org.springframework.beans.factory.BeanClassLoaderAware;
63 import org.springframework.beans.factory.InitializingBean;
64 import org.springframework.oxm.MarshallingFailureException;
65 import org.springframework.oxm.UncategorizedMappingException;
66 import org.springframework.oxm.UnmarshallingFailureException;
67 import org.springframework.oxm.XmlMappingException;
68 import org.springframework.oxm.support.AbstractMarshaller;
69 import org.springframework.util.Assert;
70 import org.springframework.util.ClassUtils;
71 import org.springframework.util.ObjectUtils;
72 import org.springframework.util.StringUtils;
73 import org.springframework.util.xml.StaxUtils;
74 
75 /**
76  * Implementation of the <code>Marshaller</code> interface for XStream.
77  *
78  * <p>By default, XStream does not require any further configuration,
79  * though class aliases can be used to have more control over the behavior of XStream.
80  *
81  * <p>Due to XStream's API, it is required to set the encoding used for writing to OutputStreams.
82  * It defaults to <code>UTF-8</code>.
83  *
84  * <p><b>NOTE:</b> XStream is an XML serialization library, not a data binding library.
85  * Therefore, it has limited namespace support. As such, it is rather unsuitable for
86  * usage within Web services.
87  *
88  * @author Peter Meijer
89  * @author Arjen Poutsma
90  * @since 3.0
91  * @see #setAliases
92  * @see #setConverters
93  * @see #setEncoding
94  */
95 public class XStreamMarshaller extends AbstractMarshaller implements InitializingBean, BeanClassLoaderAware {
96 
97 	/**
98 	 * The default encoding used for stream access: UTF-8.
99 	 */
100 	public static final String DEFAULT_ENCODING = "UTF-8";
101 
102 
103 	private final XStream xstream = new XStream();
104 
105 	private HierarchicalStreamDriver streamDriver;
106 
107 	private String encoding = DEFAULT_ENCODING;
108 
109 	private Class[] supportedClasses;
110 
111 	private ClassLoader classLoader;
112 
113 
114 	/**
115 	 * Returns the XStream instance used by this marshaller.
116 	 */
getXStream()117 	public XStream getXStream() {
118 		return this.xstream;
119 	}
120 
121 	/**
122 	 * Set the XStream mode.
123 	 * @see XStream#XPATH_REFERENCES
124 	 * @see XStream#ID_REFERENCES
125 	 * @see XStream#NO_REFERENCES
126 	 */
setMode(int mode)127 	public void setMode(int mode) {
128 		this.getXStream().setMode(mode);
129 	}
130 
131 	/**
132 	 * Set the <code>Converters</code> or <code>SingleValueConverters</code> to be registered
133 	 * with the <code>XStream</code> instance.
134 	 * @see Converter
135 	 * @see SingleValueConverter
136 	 */
setConverters(ConverterMatcher[] converters)137 	public void setConverters(ConverterMatcher[] converters) {
138 		for (int i = 0; i < converters.length; i++) {
139 			if (converters[i] instanceof Converter) {
140 				this.getXStream().registerConverter((Converter) converters[i], i);
141 			}
142 			else if (converters[i] instanceof SingleValueConverter) {
143 				this.getXStream().registerConverter((SingleValueConverter) converters[i], i);
144 			}
145 			else {
146 				throw new IllegalArgumentException("Invalid ConverterMatcher [" + converters[i] + "]");
147 			}
148 		}
149 	}
150 
151 	/**
152 	 * Sets an alias/type map, consisting of string aliases mapped to classes. Keys are aliases; values are either
153 	 * {@code Class} instances, or String class names.
154 	 * @see XStream#alias(String, Class)
155 	 */
setAliases(Map<String, ?> aliases)156 	public void setAliases(Map<String, ?> aliases) throws ClassNotFoundException {
157 		Map<String, Class<?>> classMap = toClassMap(aliases);
158 
159 		for (Map.Entry<String, Class<?>> entry : classMap.entrySet()) {
160 			this.getXStream().alias(entry.getKey(), entry.getValue());
161 		}
162 	}
163 
164 	/**
165 	 * Sets the aliases by type map, consisting of string aliases mapped to classes. Any class that is assignable to
166 	 * this type will be aliased to the same name. Keys are aliases; values are either
167 	 * {@code Class} instances, or String class names.
168 	 * @see XStream#aliasType(String, Class)
169 	 */
setAliasesByType(Map<String, ?> aliases)170 	public void setAliasesByType(Map<String, ?> aliases) throws ClassNotFoundException {
171 		Map<String, Class<?>> classMap = toClassMap(aliases);
172 
173 		for (Map.Entry<String, Class<?>> entry : classMap.entrySet()) {
174 			this.getXStream().aliasType(entry.getKey(), entry.getValue());
175 		}
176 	}
177 
toClassMap(Map<String, ?> map)178 	private Map<String, Class<?>> toClassMap(Map<String, ?> map) throws ClassNotFoundException {
179 		Map<String, Class<?>> result = new LinkedHashMap<String, Class<?>>(map.size());
180 
181 		for (Map.Entry<String, ?> entry : map.entrySet()) {
182 			String key = entry.getKey();
183 			Object value = entry.getValue();
184 			Class type;
185 			if (value instanceof Class) {
186 				type = (Class) value;
187 			}
188 			else if (value instanceof String) {
189 				String s = (String) value;
190 				type = ClassUtils.forName(s, classLoader);
191 			}
192 			else {
193 				throw new IllegalArgumentException("Unknown value [" + value + "], expected String or Class");
194 			}
195 			result.put(key, type);
196 		}
197 		return result;
198 	}
199 
200 	/**
201 	 * Sets a field alias/type map, consiting of field names
202 	 * @param aliases
203 	 * @throws ClassNotFoundException
204 	 * @throws NoSuchFieldException
205 	 * @see XStream#aliasField(String, Class, String)
206 	 */
setFieldAliases(Map<String, String> aliases)207 	public void setFieldAliases(Map<String, String> aliases) throws ClassNotFoundException, NoSuchFieldException {
208 		for (Map.Entry<String, String> entry : aliases.entrySet()) {
209 			String alias = entry.getValue();
210 			String field = entry.getKey();
211 			int idx = field.lastIndexOf('.');
212 			if (idx != -1) {
213 				String className = field.substring(0, idx);
214 				Class clazz = ClassUtils.forName(className, classLoader);
215 				String fieldName = field.substring(idx + 1);
216 				this.getXStream().aliasField(alias, clazz, fieldName);
217 			} else {
218 				throw new IllegalArgumentException("Field name [" + field + "] does not contain '.'");
219 			}
220 		}
221 	}
222 
223 	/**
224 	 * Set types to use XML attributes for.
225 	 * @see XStream#useAttributeFor(Class)
226 	 */
setUseAttributeForTypes(Class[] types)227 	public void setUseAttributeForTypes(Class[] types) {
228 		for (Class type : types) {
229 			this.getXStream().useAttributeFor(type);
230 		}
231 	}
232 
233 	/**
234 	 * Set the types to use XML attributes for. The given map can contain
235 	 * either {@code <String, Class>} pairs, in which case
236 	 * {@link XStream#useAttributeFor(String, Class)} is called.
237 	 * Alternatively, the map can contain {@code <Class, String>}
238 	 * or {@code <Class, List<String>>} pairs, which results in
239 	 * {@link XStream#useAttributeFor(Class, String)} calls.
240 	 */
setUseAttributeFor(Map<?, ?> attributes)241 	public void setUseAttributeFor(Map<?, ?> attributes) {
242 		for (Map.Entry<?, ?> entry : attributes.entrySet()) {
243 			if (entry.getKey() instanceof String) {
244 				if (entry.getValue() instanceof Class) {
245 					this.getXStream().useAttributeFor((String) entry.getKey(), (Class) entry.getValue());
246 				}
247 				else {
248 					throw new IllegalArgumentException(
249 							"Invalid argument 'attributes'. 'useAttributesFor' property takes map of <String, Class>," +
250 									" when using a map key of type String");
251 				}
252 			}
253 			else if (entry.getKey() instanceof Class) {
254 				Class<?> key = (Class<?>) entry.getKey();
255 				if (entry.getValue() instanceof String) {
256 					this.getXStream().useAttributeFor(key, (String) entry.getValue());
257 				}
258 				else if (entry.getValue() instanceof List) {
259 					List list = (List) entry.getValue();
260 
261 					for (Object o : list) {
262 						if (o instanceof String) {
263 							this.getXStream().useAttributeFor(key, (String) o);
264 						}
265 					}
266 				}
267 				else {
268 					throw new IllegalArgumentException("Invalid argument 'attributes'. " +
269 							"'useAttributesFor' property takes either <Class, String> or <Class, List<String>> map," +
270 							" when using a map key of type Class");
271 				}
272 			}
273 			else {
274 				throw new IllegalArgumentException("Invalid argument 'attributes. " +
275 						"'useAttributesFor' property takes either a map key of type String or Class");
276 			}
277 		}
278 	}
279 
280 	/**
281 	 * Specify implicit collection fields, as a Map consisting of <code>Class</code> instances
282 	 * mapped to comma separated collection field names.
283 	 *@see XStream#addImplicitCollection(Class, String)
284 	 */
setImplicitCollections(Map<Class<?>, String> implicitCollections)285 	public void setImplicitCollections(Map<Class<?>, String> implicitCollections) {
286 		for (Map.Entry<Class<?>, String> entry : implicitCollections.entrySet()) {
287 			String[] collectionFields = StringUtils.commaDelimitedListToStringArray(entry.getValue());
288 			for (String collectionField : collectionFields) {
289 				this.getXStream().addImplicitCollection(entry.getKey(), collectionField);
290 			}
291 		}
292 	}
293 
294 	/**
295 	 * Specify omitted fields, as a Map consisting of <code>Class</code> instances
296 	 * mapped to comma separated field names.
297 	 * @see XStream#omitField(Class, String)
298 	 */
setOmittedFields(Map<Class<?>, String> omittedFields)299 	public void setOmittedFields(Map<Class<?>, String> omittedFields) {
300 		for (Map.Entry<Class<?>, String> entry : omittedFields.entrySet()) {
301 			String[] fields = StringUtils.commaDelimitedListToStringArray(entry.getValue());
302 			for (String field : fields) {
303 				this.getXStream().omitField(entry.getKey(), field);
304 			}
305 		}
306 	}
307 
308 	/**
309 	 * Set the classes for which mappings will be read from class-level JDK 1.5+ annotation metadata.
310 	 * @see XStream#processAnnotations(Class)
311 	 */
setAnnotatedClass(Class<?> annotatedClass)312 	public void setAnnotatedClass(Class<?> annotatedClass) {
313 		Assert.notNull(annotatedClass, "'annotatedClass' must not be null");
314 		this.getXStream().processAnnotations(annotatedClass);
315 	}
316 
317 	/**
318 	 * Set annotated classes for which aliases will be read from class-level JDK 1.5+ annotation metadata.
319 	 * @see XStream#processAnnotations(Class[])
320 	 */
setAnnotatedClasses(Class<?>[] annotatedClasses)321 	public void setAnnotatedClasses(Class<?>[] annotatedClasses) {
322 		Assert.notEmpty(annotatedClasses, "'annotatedClasses' must not be empty");
323 		this.getXStream().processAnnotations(annotatedClasses);
324 	}
325 
326 	/**
327 	 * Set the autodetection mode of XStream.
328 	 * <p><strong>Note</strong> that auto-detection implies that the XStream is configured while
329 	 * it is processing the XML streams, and thus introduces a potential concurrency problem.
330 	 * @see XStream#autodetectAnnotations(boolean)
331 	 */
setAutodetectAnnotations(boolean autodetectAnnotations)332 	public void setAutodetectAnnotations(boolean autodetectAnnotations) {
333 		this.getXStream().autodetectAnnotations(autodetectAnnotations);
334 	}
335 
336 	/**
337 	 * Set the XStream hierarchical stream driver to be used with stream readers and writers.
338 	 */
setStreamDriver(HierarchicalStreamDriver streamDriver)339 	public void setStreamDriver(HierarchicalStreamDriver streamDriver) {
340 		this.streamDriver = streamDriver;
341 	}
342 
343 	/**
344 	 * Set the encoding to be used for stream access.
345 	 * @see #DEFAULT_ENCODING
346 	 */
setEncoding(String encoding)347 	public void setEncoding(String encoding) {
348 		this.encoding = encoding;
349 	}
350 
351 	/**
352 	 * Set the classes supported by this marshaller.
353 	 * <p>If this property is empty (the default), all classes are supported.
354 	 * @see #supports(Class)
355 	 */
setSupportedClasses(Class[] supportedClasses)356 	public void setSupportedClasses(Class[] supportedClasses) {
357 		this.supportedClasses = supportedClasses;
358 	}
359 
setBeanClassLoader(ClassLoader classLoader)360 	public void setBeanClassLoader(ClassLoader classLoader) {
361 		this.classLoader = classLoader;
362 	}
363 
364 
afterPropertiesSet()365 	public final void afterPropertiesSet() throws Exception {
366 		customizeXStream(getXStream());
367 	}
368 
369 	/**
370 	 * Template to allow for customizing of the given {@link XStream}.
371 	 * <p>The default implementation is empty.
372 	 * @param xstream the {@code XStream} instance
373 	 */
customizeXStream(XStream xstream)374 	protected void customizeXStream(XStream xstream) {
375 	}
376 
377 
supports(Class clazz)378 	public boolean supports(Class clazz) {
379 		if (ObjectUtils.isEmpty(this.supportedClasses)) {
380 			return true;
381 		}
382 		else {
383 			for (Class supportedClass : this.supportedClasses) {
384 				if (supportedClass.isAssignableFrom(clazz)) {
385 					return true;
386 				}
387 			}
388 			return false;
389 		}
390 	}
391 
392 
393 	// Marshalling
394 
395 	@Override
marshalDomNode(Object graph, Node node)396 	protected void marshalDomNode(Object graph, Node node) throws XmlMappingException {
397 		HierarchicalStreamWriter streamWriter;
398 		if (node instanceof Document) {
399 			streamWriter = new DomWriter((Document) node);
400 		}
401 		else if (node instanceof Element) {
402 			streamWriter = new DomWriter((Element) node, node.getOwnerDocument(), new XmlFriendlyReplacer());
403 		}
404 		else {
405 			throw new IllegalArgumentException("DOMResult contains neither Document nor Element");
406 		}
407 		marshal(graph, streamWriter);
408 	}
409 
410 	@Override
marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter)411 	protected void marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter) throws XmlMappingException {
412 		ContentHandler contentHandler = StaxUtils.createContentHandler(eventWriter);
413 		marshalSaxHandlers(graph, contentHandler, null);
414 	}
415 
416 	@Override
marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter)417 	protected void marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter) throws XmlMappingException {
418 		try {
419 			marshal(graph, new StaxWriter(new QNameMap(), streamWriter));
420 		}
421 		catch (XMLStreamException ex) {
422 			throw convertXStreamException(ex, true);
423 		}
424 	}
425 
426 	@Override
marshalOutputStream(Object graph, OutputStream outputStream)427 	protected void marshalOutputStream(Object graph, OutputStream outputStream) throws XmlMappingException, IOException {
428 		marshalWriter(graph, new OutputStreamWriter(outputStream, this.encoding));
429 	}
430 
431 	@Override
marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)432 	protected void marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)
433 			throws XmlMappingException {
434 
435 		SaxWriter saxWriter = new SaxWriter();
436 		saxWriter.setContentHandler(contentHandler);
437 		marshal(graph, saxWriter);
438 	}
439 
440 	@Override
marshalWriter(Object graph, Writer writer)441 	protected void marshalWriter(Object graph, Writer writer) throws XmlMappingException, IOException {
442 		if (this.streamDriver != null) {
443 			marshal(graph, this.streamDriver.createWriter(writer));
444 		}
445 		else {
446 			marshal(graph, new CompactWriter(writer));
447 		}
448 	}
449 
450 	/**
451 	 * Marshals the given graph to the given XStream HierarchicalStreamWriter.
452 	 * Converts exceptions using {@link #convertXStreamException}.
453 	 */
marshal(Object graph, HierarchicalStreamWriter streamWriter)454 	private void marshal(Object graph, HierarchicalStreamWriter streamWriter) {
455 		try {
456 			getXStream().marshal(graph, streamWriter);
457 		}
458 		catch (Exception ex) {
459 			throw convertXStreamException(ex, true);
460 		}
461 		finally {
462 			try {
463 				streamWriter.flush();
464 			}
465 			catch (Exception ex) {
466 				logger.debug("Could not flush HierarchicalStreamWriter", ex);
467 			}
468 		}
469 	}
470 
471 
472 	// Unmarshalling
473 
474 	@Override
unmarshalDomNode(Node node)475 	protected Object unmarshalDomNode(Node node) throws XmlMappingException {
476 		HierarchicalStreamReader streamReader;
477 		if (node instanceof Document) {
478 			streamReader = new DomReader((Document) node);
479 		}
480 		else if (node instanceof Element) {
481 			streamReader = new DomReader((Element) node);
482 		}
483 		else {
484 			throw new IllegalArgumentException("DOMSource contains neither Document nor Element");
485 		}
486 		try {
487 			return getXStream().unmarshal(streamReader);
488 		}
489 		catch (Exception ex) {
490 			throw convertXStreamException(ex, false);
491 		}
492 	}
493 
494 	@Override
unmarshalXmlEventReader(XMLEventReader eventReader)495 	protected Object unmarshalXmlEventReader(XMLEventReader eventReader) throws XmlMappingException {
496 		try {
497 			XMLStreamReader streamReader = StaxUtils.createEventStreamReader(eventReader);
498 			return unmarshalXmlStreamReader(streamReader);
499 		}
500 		catch (XMLStreamException ex) {
501 			throw convertXStreamException(ex, false);
502 		}
503 	}
504 
505 	@Override
unmarshalXmlStreamReader(XMLStreamReader streamReader)506 	protected Object unmarshalXmlStreamReader(XMLStreamReader streamReader) throws XmlMappingException {
507 		try {
508 			HierarchicalStreamReader hierarchicalStreamReader =
509 					new StaxReader(new QNameMap(),streamReader);
510 			return getXStream().unmarshal(hierarchicalStreamReader);
511 		}
512 		catch (Exception ex) {
513 			throw convertXStreamException(ex, false);
514 		}
515 	}
516 
517 	@Override
unmarshalInputStream(InputStream inputStream)518 	protected Object unmarshalInputStream(InputStream inputStream) throws XmlMappingException, IOException {
519 		return unmarshalReader(new InputStreamReader(inputStream, this.encoding));
520 	}
521 
522 	@Override
unmarshalReader(Reader reader)523 	protected Object unmarshalReader(Reader reader) throws XmlMappingException, IOException {
524 		try {
525 			HierarchicalStreamReader streamReader;
526 			if (this.streamDriver != null) {
527 				streamReader = this.streamDriver.createReader(reader);
528 			}
529 			else {
530 				streamReader = new XppReader(reader);
531 			}
532 			return getXStream().unmarshal(streamReader);
533 		}
534 		catch (Exception ex) {
535 			throw convertXStreamException(ex, false);
536 		}
537 	}
538 
539 	@Override
unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)540 	protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)
541 			throws XmlMappingException, IOException {
542 
543 		throw new UnsupportedOperationException(
544 				"XStreamMarshaller does not support unmarshalling using SAX XMLReaders");
545 	}
546 
547 	/**
548 	 * Convert the given XStream exception to an appropriate exception from the
549 	 * <code>org.springframework.oxm</code> hierarchy.
550 	 * <p>A boolean flag is used to indicate whether this exception occurs during marshalling or
551 	 * unmarshalling, since XStream itself does not make this distinction in its exception hierarchy.
552 	 * @param ex XStream exception that occured
553 	 * @param marshalling indicates whether the exception occurs during marshalling (<code>true</code>),
554 	 * or unmarshalling (<code>false</code>)
555 	 * @return the corresponding <code>XmlMappingException</code>
556 	 */
convertXStreamException(Exception ex, boolean marshalling)557 	protected XmlMappingException convertXStreamException(Exception ex, boolean marshalling) {
558 		if (ex instanceof StreamException || ex instanceof CannotResolveClassException ||
559 				ex instanceof ConversionException) {
560 			if (marshalling) {
561 				return new MarshallingFailureException("XStream marshalling exception",  ex);
562 			}
563 			else {
564 				return new UnmarshallingFailureException("XStream unmarshalling exception", ex);
565 			}
566 		}
567 		else {
568 			// fallback
569 			return new UncategorizedMappingException("Unknown XStream exception", ex);
570 		}
571 	}
572 }
573