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