001/*
002 * Copyright 2002-2015 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.oxm.xstream;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.InputStreamReader;
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.io.Reader;
025import java.io.Writer;
026import java.lang.reflect.Constructor;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import javax.xml.stream.XMLEventReader;
031import javax.xml.stream.XMLEventWriter;
032import javax.xml.stream.XMLStreamException;
033import javax.xml.stream.XMLStreamReader;
034import javax.xml.stream.XMLStreamWriter;
035import javax.xml.transform.stream.StreamSource;
036
037import com.thoughtworks.xstream.MarshallingStrategy;
038import com.thoughtworks.xstream.XStream;
039import com.thoughtworks.xstream.converters.ConversionException;
040import com.thoughtworks.xstream.converters.Converter;
041import com.thoughtworks.xstream.converters.ConverterLookup;
042import com.thoughtworks.xstream.converters.ConverterMatcher;
043import com.thoughtworks.xstream.converters.ConverterRegistry;
044import com.thoughtworks.xstream.converters.DataHolder;
045import com.thoughtworks.xstream.converters.SingleValueConverter;
046import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
047import com.thoughtworks.xstream.core.ClassLoaderReference;
048import com.thoughtworks.xstream.core.DefaultConverterLookup;
049import com.thoughtworks.xstream.core.util.CompositeClassLoader;
050import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
051import com.thoughtworks.xstream.io.HierarchicalStreamReader;
052import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
053import com.thoughtworks.xstream.io.StreamException;
054import com.thoughtworks.xstream.io.naming.NameCoder;
055import com.thoughtworks.xstream.io.xml.CompactWriter;
056import com.thoughtworks.xstream.io.xml.DomReader;
057import com.thoughtworks.xstream.io.xml.DomWriter;
058import com.thoughtworks.xstream.io.xml.QNameMap;
059import com.thoughtworks.xstream.io.xml.SaxWriter;
060import com.thoughtworks.xstream.io.xml.StaxReader;
061import com.thoughtworks.xstream.io.xml.StaxWriter;
062import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
063import com.thoughtworks.xstream.io.xml.XppDriver;
064import com.thoughtworks.xstream.mapper.CannotResolveClassException;
065import com.thoughtworks.xstream.mapper.Mapper;
066import com.thoughtworks.xstream.mapper.MapperWrapper;
067import org.w3c.dom.Document;
068import org.w3c.dom.Element;
069import org.w3c.dom.Node;
070import org.xml.sax.ContentHandler;
071import org.xml.sax.InputSource;
072import org.xml.sax.XMLReader;
073import org.xml.sax.ext.LexicalHandler;
074
075import org.springframework.beans.factory.BeanClassLoaderAware;
076import org.springframework.beans.factory.InitializingBean;
077import org.springframework.oxm.MarshallingFailureException;
078import org.springframework.oxm.UncategorizedMappingException;
079import org.springframework.oxm.UnmarshallingFailureException;
080import org.springframework.oxm.XmlMappingException;
081import org.springframework.oxm.support.AbstractMarshaller;
082import org.springframework.util.ClassUtils;
083import org.springframework.util.ObjectUtils;
084import org.springframework.util.StringUtils;
085import org.springframework.util.xml.StaxUtils;
086
087/**
088 * Implementation of the {@code Marshaller} interface for XStream.
089 *
090 * <p>By default, XStream does not require any further configuration and can (un)marshal
091 * any class on the classpath. As such, it is <b>not recommended to use the
092 * {@code XStreamMarshaller} to unmarshal XML from external sources</b> (i.e. the Web),
093 * as this can result in <b>security vulnerabilities</b>. If you do use the
094 * {@code XStreamMarshaller} to unmarshal external XML, set the
095 * {@link #setSupportedClasses(Class[]) supportedClasses} and
096 * {@link #setConverters(ConverterMatcher[]) converters} properties (possibly using
097 * a {@link CatchAllConverter}) or override the {@link #customizeXStream(XStream)}
098 * method to make sure it only accepts the classes you want it to support.
099 *
100 * <p>Due to XStream's API, it is required to set the encoding used for writing to
101 * OutputStreams. It defaults to {@code UTF-8}.
102 *
103 * <p><b>NOTE:</b> XStream is an XML serialization library, not a data binding library.
104 * Therefore, it has limited namespace support. As such, it is rather unsuitable for
105 * usage within Web Services.
106 *
107 * <p>This marshaller requires XStream 1.4.5 or higher, as of Spring 4.3.
108 * Note that {@link XStream} construction has been reworked in 4.0, with the
109 * stream driver and the class loader getting passed into XStream itself now.
110 *
111 * @author Peter Meijer
112 * @author Arjen Poutsma
113 * @author Juergen Hoeller
114 * @since 3.0
115 */
116public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLoaderAware, InitializingBean {
117
118        /**
119         * The default encoding used for stream access: UTF-8.
120         */
121        public static final String DEFAULT_ENCODING = "UTF-8";
122
123
124        private ReflectionProvider reflectionProvider;
125
126        private HierarchicalStreamDriver streamDriver;
127
128        private HierarchicalStreamDriver defaultDriver;
129
130        private Mapper mapper;
131
132        private Class<? extends MapperWrapper>[] mapperWrappers;
133
134        private ConverterLookup converterLookup = new DefaultConverterLookup();
135
136        private ConverterRegistry converterRegistry = (ConverterRegistry) this.converterLookup;
137
138        private ConverterMatcher[] converters;
139
140        private MarshallingStrategy marshallingStrategy;
141
142        private Integer mode;
143
144        private Map<String, ?> aliases;
145
146        private Map<String, ?> aliasesByType;
147
148        private Map<String, String> fieldAliases;
149
150        private Class<?>[] useAttributeForTypes;
151
152        private Map<?, ?> useAttributeFor;
153
154        private Map<Class<?>, String> implicitCollections;
155
156        private Map<Class<?>, String> omittedFields;
157
158        private Class<?>[] annotatedClasses;
159
160        private boolean autodetectAnnotations;
161
162        private String encoding = DEFAULT_ENCODING;
163
164        private NameCoder nameCoder = new XmlFriendlyNameCoder();
165
166        private Class<?>[] supportedClasses;
167
168        private ClassLoader beanClassLoader = new CompositeClassLoader();
169
170        private XStream xstream;
171
172
173        /**
174         * Set a custom XStream {@link ReflectionProvider} to use.
175         * @since 4.0
176         */
177        public void setReflectionProvider(ReflectionProvider reflectionProvider) {
178                this.reflectionProvider = reflectionProvider;
179        }
180
181        /**
182         * Set a XStream {@link HierarchicalStreamDriver} to be used for readers and writers.
183         * <p>As of Spring 4.0, this stream driver will also be passed to the {@link XStream}
184         * constructor and therefore used by streaming-related native API methods themselves.
185         */
186        public void setStreamDriver(HierarchicalStreamDriver streamDriver) {
187                this.streamDriver = streamDriver;
188                this.defaultDriver = streamDriver;
189        }
190
191        private HierarchicalStreamDriver getDefaultDriver() {
192                if (this.defaultDriver == null) {
193                        this.defaultDriver = new XppDriver();
194                }
195                return this.defaultDriver;
196        }
197
198        /**
199         * Set a custom XStream {@link Mapper} to use.
200         * @since 4.0
201         */
202        public void setMapper(Mapper mapper) {
203                this.mapper = mapper;
204        }
205
206        /**
207         * Set one or more custom XStream {@link MapperWrapper} classes.
208         * Each of those classes needs to have a constructor with a single argument
209         * of type {@link Mapper} or {@link MapperWrapper}.
210         * @since 4.0
211         */
212        @SuppressWarnings("unchecked")
213        public void setMapperWrappers(Class<? extends MapperWrapper>... mapperWrappers) {
214                this.mapperWrappers = mapperWrappers;
215        }
216
217        /**
218         * Set a custom XStream {@link ConverterLookup} to use.
219         * Also used as {@link ConverterRegistry} if the given reference implements it as well.
220         * @since 4.0
221         * @see DefaultConverterLookup
222         */
223        public void setConverterLookup(ConverterLookup converterLookup) {
224                this.converterLookup = converterLookup;
225                if (converterLookup instanceof ConverterRegistry) {
226                        this.converterRegistry = (ConverterRegistry) converterLookup;
227                }
228        }
229
230        /**
231         * Set a custom XStream {@link ConverterRegistry} to use.
232         * @since 4.0
233         * @see #setConverterLookup
234         * @see DefaultConverterLookup
235         */
236        public void setConverterRegistry(ConverterRegistry converterRegistry) {
237                this.converterRegistry = converterRegistry;
238        }
239
240        /**
241         * Set the {@code Converters} or {@code SingleValueConverters} to be registered
242         * with the {@code XStream} instance.
243         * @see Converter
244         * @see SingleValueConverter
245         */
246        public void setConverters(ConverterMatcher... converters) {
247                this.converters = converters;
248        }
249
250        /**
251         * Set a custom XStream {@link MarshallingStrategy} to use.
252         * @since 4.0
253         */
254        public void setMarshallingStrategy(MarshallingStrategy marshallingStrategy) {
255                this.marshallingStrategy = marshallingStrategy;
256        }
257
258        /**
259         * Set the XStream mode to use.
260         * @see XStream#ID_REFERENCES
261         * @see XStream#NO_REFERENCES
262         */
263        public void setMode(int mode) {
264                this.mode = mode;
265        }
266
267        /**
268         * Set the alias/type map, consisting of string aliases mapped to classes.
269         * <p>Keys are aliases; values are either {@code Class} instances, or String class names.
270         * @see XStream#alias(String, Class)
271         */
272        public void setAliases(Map<String, ?> aliases) {
273                this.aliases = aliases;
274        }
275
276        /**
277         * Set the <em>aliases by type</em> map, consisting of string aliases mapped to classes.
278         * <p>Any class that is assignable to this type will be aliased to the same name.
279         * Keys are aliases; values are either {@code Class} instances, or String class names.
280         * @see XStream#aliasType(String, Class)
281         */
282        public void setAliasesByType(Map<String, ?> aliasesByType) {
283                this.aliasesByType = aliasesByType;
284        }
285
286        /**
287         * Set the field alias/type map, consisting of field names.
288         * @see XStream#aliasField(String, Class, String)
289         */
290        public void setFieldAliases(Map<String, String> fieldAliases) {
291                this.fieldAliases = fieldAliases;
292        }
293
294        /**
295         * Set types to use XML attributes for.
296         * @see XStream#useAttributeFor(Class)
297         */
298        public void setUseAttributeForTypes(Class<?>... useAttributeForTypes) {
299                this.useAttributeForTypes = useAttributeForTypes;
300        }
301
302        /**
303         * Set the types to use XML attributes for. The given map can contain
304         * either {@code <String, Class>} pairs, in which case
305         * {@link XStream#useAttributeFor(String, Class)} is called.
306         * Alternatively, the map can contain {@code <Class, String>}
307         * or {@code <Class, List<String>>} pairs, which results
308         * in {@link XStream#useAttributeFor(Class, String)} calls.
309         */
310        public void setUseAttributeFor(Map<?, ?> useAttributeFor) {
311                this.useAttributeFor = useAttributeFor;
312        }
313
314        /**
315         * Specify implicit collection fields, as a Map consisting of {@code Class} instances
316         * mapped to comma separated collection field names.
317         * @see XStream#addImplicitCollection(Class, String)
318         */
319        public void setImplicitCollections(Map<Class<?>, String> implicitCollections) {
320                this.implicitCollections = implicitCollections;
321        }
322
323        /**
324         * Specify omitted fields, as a Map consisting of {@code Class} instances
325         * mapped to comma separated field names.
326         * @see XStream#omitField(Class, String)
327         */
328        public void setOmittedFields(Map<Class<?>, String> omittedFields) {
329                this.omittedFields = omittedFields;
330        }
331
332        /**
333         * Set annotated classes for which aliases will be read from class-level annotation metadata.
334         * @see XStream#processAnnotations(Class[])
335         */
336        public void setAnnotatedClasses(Class<?>... annotatedClasses) {
337                this.annotatedClasses = annotatedClasses;
338        }
339
340        /**
341         * Activate XStream's autodetection mode.
342         * <p><b>Note</b>: Autodetection implies that the XStream instance is being configured while
343         * it is processing the XML streams, and thus introduces a potential concurrency problem.
344         * @see XStream#autodetectAnnotations(boolean)
345         */
346        public void setAutodetectAnnotations(boolean autodetectAnnotations) {
347                this.autodetectAnnotations = autodetectAnnotations;
348        }
349
350        /**
351         * Set the encoding to be used for stream access.
352         * @see #DEFAULT_ENCODING
353         */
354        public void setEncoding(String encoding) {
355                this.encoding = encoding;
356        }
357
358        @Override
359        protected String getDefaultEncoding() {
360                return this.encoding;
361        }
362
363        /**
364         * Set a custom XStream {@link NameCoder} to use.
365         * The default is an {@link XmlFriendlyNameCoder}.
366         * @since 4.0.4
367         */
368        public void setNameCoder(NameCoder nameCoder) {
369                this.nameCoder = nameCoder;
370        }
371
372        /**
373         * Set the classes supported by this marshaller.
374         * <p>If this property is empty (the default), all classes are supported.
375         * @see #supports(Class)
376         */
377        public void setSupportedClasses(Class<?>... supportedClasses) {
378                this.supportedClasses = supportedClasses;
379        }
380
381        @Override
382        public void setBeanClassLoader(ClassLoader classLoader) {
383                this.beanClassLoader = classLoader;
384        }
385
386
387        @Override
388        public void afterPropertiesSet() {
389                this.xstream = buildXStream();
390        }
391
392        /**
393         * Build the native XStream delegate to be used by this marshaller,
394         * delegating to {@link #constructXStream()}, {@link #configureXStream}
395         * and {@link #customizeXStream}.
396         */
397        protected XStream buildXStream() {
398                XStream xstream = constructXStream();
399                configureXStream(xstream);
400                customizeXStream(xstream);
401                return xstream;
402        }
403
404        /**
405         * Construct an XStream instance, either using one of the
406         * standard constructors or creating a custom subclass.
407         * @return the {@code XStream} instance
408         */
409        protected XStream constructXStream() {
410                return new XStream(this.reflectionProvider, getDefaultDriver(), new ClassLoaderReference(this.beanClassLoader),
411                                this.mapper, this.converterLookup, this.converterRegistry) {
412                        @Override
413                        protected MapperWrapper wrapMapper(MapperWrapper next) {
414                                MapperWrapper mapperToWrap = next;
415                                if (mapperWrappers != null) {
416                                        for (Class<? extends MapperWrapper> mapperWrapper : mapperWrappers) {
417                                                Constructor<? extends MapperWrapper> ctor;
418                                                try {
419                                                        ctor = mapperWrapper.getConstructor(Mapper.class);
420                                                }
421                                                catch (NoSuchMethodException ex) {
422                                                        try {
423                                                                ctor = mapperWrapper.getConstructor(MapperWrapper.class);
424                                                        }
425                                                        catch (NoSuchMethodException ex2) {
426                                                                throw new IllegalStateException("No appropriate MapperWrapper constructor found: " + mapperWrapper);
427                                                        }
428                                                }
429                                                try {
430                                                        mapperToWrap = ctor.newInstance(mapperToWrap);
431                                                }
432                                                catch (Throwable ex) {
433                                                        throw new IllegalStateException("Failed to construct MapperWrapper: " + mapperWrapper);
434                                                }
435                                        }
436                                }
437                                return mapperToWrap;
438                        }
439                };
440        }
441
442        /**
443         * Configure the XStream instance with this marshaller's bean properties.
444         * @param xstream the {@code XStream} instance
445         */
446        protected void configureXStream(XStream xstream) {
447                if (this.converters != null) {
448                        for (int i = 0; i < this.converters.length; i++) {
449                                if (this.converters[i] instanceof Converter) {
450                                        xstream.registerConverter((Converter) this.converters[i], i);
451                                }
452                                else if (this.converters[i] instanceof SingleValueConverter) {
453                                        xstream.registerConverter((SingleValueConverter) this.converters[i], i);
454                                }
455                                else {
456                                        throw new IllegalArgumentException("Invalid ConverterMatcher [" + this.converters[i] + "]");
457                                }
458                        }
459                }
460
461                if (this.marshallingStrategy != null) {
462                        xstream.setMarshallingStrategy(this.marshallingStrategy);
463                }
464                if (this.mode != null) {
465                        xstream.setMode(this.mode);
466                }
467
468                try {
469                        if (this.aliases != null) {
470                                Map<String, Class<?>> classMap = toClassMap(this.aliases);
471                                for (Map.Entry<String, Class<?>> entry : classMap.entrySet()) {
472                                        xstream.alias(entry.getKey(), entry.getValue());
473                                }
474                        }
475                        if (this.aliasesByType != null) {
476                                Map<String, Class<?>> classMap = toClassMap(this.aliasesByType);
477                                for (Map.Entry<String, Class<?>> entry : classMap.entrySet()) {
478                                        xstream.aliasType(entry.getKey(), entry.getValue());
479                                }
480                        }
481                        if (this.fieldAliases != null) {
482                                for (Map.Entry<String, String> entry : this.fieldAliases.entrySet()) {
483                                        String alias = entry.getValue();
484                                        String field = entry.getKey();
485                                        int idx = field.lastIndexOf('.');
486                                        if (idx != -1) {
487                                                String className = field.substring(0, idx);
488                                                Class<?> clazz = ClassUtils.forName(className, this.beanClassLoader);
489                                                String fieldName = field.substring(idx + 1);
490                                                xstream.aliasField(alias, clazz, fieldName);
491                                        }
492                                        else {
493                                                throw new IllegalArgumentException("Field name [" + field + "] does not contain '.'");
494                                        }
495                                }
496                        }
497                }
498                catch (ClassNotFoundException ex) {
499                        throw new IllegalStateException("Failed to load specified alias class", ex);
500                }
501
502                if (this.useAttributeForTypes != null) {
503                        for (Class<?> type : this.useAttributeForTypes) {
504                                xstream.useAttributeFor(type);
505                        }
506                }
507                if (this.useAttributeFor != null) {
508                        for (Map.Entry<?, ?> entry : this.useAttributeFor.entrySet()) {
509                                if (entry.getKey() instanceof String) {
510                                        if (entry.getValue() instanceof Class) {
511                                                xstream.useAttributeFor((String) entry.getKey(), (Class<?>) entry.getValue());
512                                        }
513                                        else {
514                                                throw new IllegalArgumentException(
515                                                                "'useAttributesFor' takes Map<String, Class> when using a map key of type String");
516                                        }
517                                }
518                                else if (entry.getKey() instanceof Class) {
519                                        Class<?> key = (Class<?>) entry.getKey();
520                                        if (entry.getValue() instanceof String) {
521                                                xstream.useAttributeFor(key, (String) entry.getValue());
522                                        }
523                                        else if (entry.getValue() instanceof List) {
524                                                @SuppressWarnings("unchecked")
525                                                List<Object> listValue = (List<Object>) entry.getValue();
526                                                for (Object element : listValue) {
527                                                        if (element instanceof String) {
528                                                                xstream.useAttributeFor(key, (String) element);
529                                                        }
530                                                }
531                                        }
532                                        else {
533                                                throw new IllegalArgumentException("'useAttributesFor' property takes either Map<Class, String> " +
534                                                                "or Map<Class, List<String>> when using a map key of type Class");
535                                        }
536                                }
537                                else {
538                                        throw new IllegalArgumentException(
539                                                        "'useAttributesFor' property takes either a map key of type String or Class");
540                                }
541                        }
542                }
543
544                if (this.implicitCollections != null) {
545                        for (Map.Entry<Class<?>, String> entry : this.implicitCollections.entrySet()) {
546                                String[] collectionFields = StringUtils.commaDelimitedListToStringArray(entry.getValue());
547                                for (String collectionField : collectionFields) {
548                                        xstream.addImplicitCollection(entry.getKey(), collectionField);
549                                }
550                        }
551                }
552                if (this.omittedFields != null) {
553                        for (Map.Entry<Class<?>, String> entry : this.omittedFields.entrySet()) {
554                                String[] fields = StringUtils.commaDelimitedListToStringArray(entry.getValue());
555                                for (String field : fields) {
556                                        xstream.omitField(entry.getKey(), field);
557                                }
558                        }
559                }
560
561                if (this.annotatedClasses != null) {
562                        xstream.processAnnotations(this.annotatedClasses);
563                }
564                if (this.autodetectAnnotations) {
565                        xstream.autodetectAnnotations(true);
566                }
567        }
568
569        private Map<String, Class<?>> toClassMap(Map<String, ?> map) throws ClassNotFoundException {
570                Map<String, Class<?>> result = new LinkedHashMap<String, Class<?>>(map.size());
571                for (Map.Entry<String, ?> entry : map.entrySet()) {
572                        String key = entry.getKey();
573                        Object value = entry.getValue();
574                        Class<?> type;
575                        if (value instanceof Class) {
576                                type = (Class<?>) value;
577                        }
578                        else if (value instanceof String) {
579                                String className = (String) value;
580                                type = ClassUtils.forName(className, this.beanClassLoader);
581                        }
582                        else {
583                                throw new IllegalArgumentException("Unknown value [" + value + "] - expected String or Class");
584                        }
585                        result.put(key, type);
586                }
587                return result;
588        }
589
590        /**
591         * Template to allow for customizing the given {@link XStream}.
592         * <p>The default implementation is empty.
593         * @param xstream the {@code XStream} instance
594         */
595        protected void customizeXStream(XStream xstream) {
596        }
597
598        /**
599         * Return the native XStream delegate used by this marshaller.
600         * <p><b>NOTE: This method has been marked as final as of Spring 4.0.</b>
601         * It can be used to access the fully configured XStream for marshalling
602         * but not configuration purposes anymore.
603         */
604        public final XStream getXStream() {
605                if (this.xstream == null) {
606                        this.xstream = buildXStream();
607                }
608                return this.xstream;
609        }
610
611
612        @Override
613        public boolean supports(Class<?> clazz) {
614                if (ObjectUtils.isEmpty(this.supportedClasses)) {
615                        return true;
616                }
617                else {
618                        for (Class<?> supportedClass : this.supportedClasses) {
619                                if (supportedClass.isAssignableFrom(clazz)) {
620                                        return true;
621                                }
622                        }
623                        return false;
624                }
625        }
626
627
628        // Marshalling
629
630        @Override
631        protected void marshalDomNode(Object graph, Node node) throws XmlMappingException {
632                HierarchicalStreamWriter streamWriter;
633                if (node instanceof Document) {
634                        streamWriter = new DomWriter((Document) node, this.nameCoder);
635                }
636                else if (node instanceof Element) {
637                        streamWriter = new DomWriter((Element) node, node.getOwnerDocument(), this.nameCoder);
638                }
639                else {
640                        throw new IllegalArgumentException("DOMResult contains neither Document nor Element");
641                }
642                doMarshal(graph, streamWriter, null);
643        }
644
645        @Override
646        protected void marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter) throws XmlMappingException {
647                ContentHandler contentHandler = StaxUtils.createContentHandler(eventWriter);
648                LexicalHandler lexicalHandler = null;
649                if (contentHandler instanceof LexicalHandler) {
650                        lexicalHandler = (LexicalHandler) contentHandler;
651                }
652                marshalSaxHandlers(graph, contentHandler, lexicalHandler);
653        }
654
655        @Override
656        protected void marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter) throws XmlMappingException {
657                try {
658                        doMarshal(graph, new StaxWriter(new QNameMap(), streamWriter, this.nameCoder), null);
659                }
660                catch (XMLStreamException ex) {
661                        throw convertXStreamException(ex, true);
662                }
663        }
664
665        @Override
666        protected void marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)
667                        throws XmlMappingException {
668
669                SaxWriter saxWriter = new SaxWriter(this.nameCoder);
670                saxWriter.setContentHandler(contentHandler);
671                doMarshal(graph, saxWriter, null);
672        }
673
674        @Override
675        public void marshalOutputStream(Object graph, OutputStream outputStream) throws XmlMappingException, IOException {
676                marshalOutputStream(graph, outputStream, null);
677        }
678
679        public void marshalOutputStream(Object graph, OutputStream outputStream, DataHolder dataHolder)
680                        throws XmlMappingException, IOException {
681
682                if (this.streamDriver != null) {
683                        doMarshal(graph, this.streamDriver.createWriter(outputStream), dataHolder);
684                }
685                else {
686                        marshalWriter(graph, new OutputStreamWriter(outputStream, this.encoding), dataHolder);
687                }
688        }
689
690        @Override
691        public void marshalWriter(Object graph, Writer writer) throws XmlMappingException, IOException {
692                marshalWriter(graph, writer, null);
693        }
694
695        public void marshalWriter(Object graph, Writer writer, DataHolder dataHolder)
696                        throws XmlMappingException, IOException {
697
698                if (this.streamDriver != null) {
699                        doMarshal(graph, this.streamDriver.createWriter(writer), dataHolder);
700                }
701                else {
702                        doMarshal(graph, new CompactWriter(writer), dataHolder);
703                }
704        }
705
706        /**
707         * Marshals the given graph to the given XStream HierarchicalStreamWriter.
708         * Converts exceptions using {@link #convertXStreamException}.
709         */
710        private void doMarshal(Object graph, HierarchicalStreamWriter streamWriter, DataHolder dataHolder) {
711                try {
712                        getXStream().marshal(graph, streamWriter, dataHolder);
713                }
714                catch (Exception ex) {
715                        throw convertXStreamException(ex, true);
716                }
717                finally {
718                        try {
719                                streamWriter.flush();
720                        }
721                        catch (Exception ex) {
722                                logger.debug("Could not flush HierarchicalStreamWriter", ex);
723                        }
724                }
725        }
726
727
728        // Unmarshalling
729
730        @Override
731        protected Object unmarshalStreamSource(StreamSource streamSource) throws XmlMappingException, IOException {
732                if (streamSource.getInputStream() != null) {
733                        return unmarshalInputStream(streamSource.getInputStream());
734                }
735                else if (streamSource.getReader() != null) {
736                        return unmarshalReader(streamSource.getReader());
737                }
738                else {
739                        throw new IllegalArgumentException("StreamSource contains neither InputStream nor Reader");
740                }
741        }
742
743        @Override
744        protected Object unmarshalDomNode(Node node) throws XmlMappingException {
745                HierarchicalStreamReader streamReader;
746                if (node instanceof Document) {
747                        streamReader = new DomReader((Document) node, this.nameCoder);
748                }
749                else if (node instanceof Element) {
750                        streamReader = new DomReader((Element) node, this.nameCoder);
751                }
752                else {
753                        throw new IllegalArgumentException("DOMSource contains neither Document nor Element");
754                }
755        return doUnmarshal(streamReader, null);
756        }
757
758        @Override
759        protected Object unmarshalXmlEventReader(XMLEventReader eventReader) throws XmlMappingException {
760                try {
761                        XMLStreamReader streamReader = StaxUtils.createEventStreamReader(eventReader);
762                        return unmarshalXmlStreamReader(streamReader);
763                }
764                catch (XMLStreamException ex) {
765                        throw convertXStreamException(ex, false);
766                }
767        }
768
769        @Override
770        protected Object unmarshalXmlStreamReader(XMLStreamReader streamReader) throws XmlMappingException {
771        return doUnmarshal(new StaxReader(new QNameMap(), streamReader, this.nameCoder), null);
772        }
773
774        @Override
775        protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)
776                        throws XmlMappingException, IOException {
777
778                throw new UnsupportedOperationException(
779                                "XStreamMarshaller does not support unmarshalling using SAX XMLReaders");
780        }
781
782        @Override
783        public Object unmarshalInputStream(InputStream inputStream) throws XmlMappingException, IOException {
784                return unmarshalInputStream(inputStream, null);
785        }
786
787        public Object unmarshalInputStream(InputStream inputStream, DataHolder dataHolder) throws XmlMappingException, IOException {
788        if (this.streamDriver != null) {
789            return doUnmarshal(this.streamDriver.createReader(inputStream), dataHolder);
790        }
791        else {
792                    return unmarshalReader(new InputStreamReader(inputStream, this.encoding), dataHolder);
793        }
794        }
795
796        @Override
797        public Object unmarshalReader(Reader reader) throws XmlMappingException, IOException {
798                return unmarshalReader(reader, null);
799        }
800
801        public Object unmarshalReader(Reader reader, DataHolder dataHolder) throws XmlMappingException, IOException {
802                return doUnmarshal(getDefaultDriver().createReader(reader), dataHolder);
803        }
804
805    /**
806     * Unmarshals the given graph to the given XStream HierarchicalStreamWriter.
807     * Converts exceptions using {@link #convertXStreamException}.
808     */
809    private Object doUnmarshal(HierarchicalStreamReader streamReader, DataHolder dataHolder) {
810        try {
811            return getXStream().unmarshal(streamReader, null, dataHolder);
812        }
813        catch (Exception ex) {
814            throw convertXStreamException(ex, false);
815        }
816    }
817
818
819    /**
820     * Convert the given XStream exception to an appropriate exception from the
821     * {@code org.springframework.oxm} hierarchy.
822     * <p>A boolean flag is used to indicate whether this exception occurs during marshalling or
823     * unmarshalling, since XStream itself does not make this distinction in its exception hierarchy.
824     * @param ex XStream exception that occurred
825     * @param marshalling indicates whether the exception occurs during marshalling ({@code true}),
826     * or unmarshalling ({@code false})
827     * @return the corresponding {@code XmlMappingException}
828     */
829        protected XmlMappingException convertXStreamException(Exception ex, boolean marshalling) {
830                if (ex instanceof StreamException || ex instanceof CannotResolveClassException ||
831                                ex instanceof ConversionException) {
832                        if (marshalling) {
833                                return new MarshallingFailureException("XStream marshalling exception",  ex);
834                        }
835                        else {
836                                return new UnmarshallingFailureException("XStream unmarshalling exception", ex);
837                        }
838                }
839                else {
840                        // fallback
841                        return new UncategorizedMappingException("Unknown XStream exception", ex);
842                }
843        }
844
845}