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