View Javadoc

1   /*
2    * Copyright 2002-2009 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  package org.springframework.oxm.castor;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.io.OutputStreamWriter;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.util.Iterator;
25  import java.util.Properties;
26  import javax.xml.stream.XMLEventReader;
27  import javax.xml.stream.XMLEventWriter;
28  import javax.xml.stream.XMLStreamReader;
29  import javax.xml.stream.XMLStreamWriter;
30  
31  import org.exolab.castor.mapping.Mapping;
32  import org.exolab.castor.mapping.MappingException;
33  import org.exolab.castor.xml.MarshalException;
34  import org.exolab.castor.xml.Marshaller;
35  import org.exolab.castor.xml.ResolverException;
36  import org.exolab.castor.xml.UnmarshalHandler;
37  import org.exolab.castor.xml.Unmarshaller;
38  import org.exolab.castor.xml.XMLContext;
39  import org.exolab.castor.xml.XMLException;
40  import org.w3c.dom.Node;
41  import org.xml.sax.ContentHandler;
42  import org.xml.sax.InputSource;
43  import org.xml.sax.SAXException;
44  import org.xml.sax.XMLReader;
45  import org.xml.sax.ext.LexicalHandler;
46  
47  import org.springframework.beans.factory.InitializingBean;
48  import org.springframework.core.io.Resource;
49  import org.springframework.oxm.AbstractMarshaller;
50  import org.springframework.oxm.XmlMappingException;
51  import org.springframework.util.ObjectUtils;
52  import org.springframework.util.StringUtils;
53  import org.springframework.xml.dom.DomContentHandler;
54  import org.springframework.xml.sax.SaxUtils;
55  import org.springframework.xml.stream.StaxEventContentHandler;
56  import org.springframework.xml.stream.StaxEventXmlReader;
57  import org.springframework.xml.stream.StaxStreamContentHandler;
58  import org.springframework.xml.stream.StaxStreamXmlReader;
59  
60  /**
61   * Implementation of the <code>Marshaller</code> interface for Castor. By default, Castor does not require any further
62   * configuration, though setting a target class or providing a mapping file can be used to have more control over the
63   * behavior of Castor.
64   * <p/>
65   * If a target class is specified using <code>setTargetClass</code>, the <code>CastorMarshaller</code> can only be used
66   * to unmarshall XML that represents that specific class. If you want to unmarshall multiple classes, you have to
67   * provide a mapping file using <code>setMappingLocations</code>.
68   * <p/>
69   * Due to Castor's API, it is required to set the encoding used for writing to output streams. It defaults to
70   * <code>UTF-8</code>.
71   *
72   * @author Arjen Poutsma
73   * @see #setEncoding(String)
74   * @see #setTargetClass(Class)
75   * @see #setMappingLocation(org.springframework.core.io.Resource)
76   * @see #setMappingLocations(org.springframework.core.io.Resource[])
77   * @since 1.0.0
78   */
79  public class CastorMarshaller extends AbstractMarshaller implements InitializingBean {
80  
81      /** The default encoding used for stream access. */
82      public static final String DEFAULT_ENCODING = "UTF-8";
83  
84      private Resource[] mappingLocations;
85  
86      private String encoding = DEFAULT_ENCODING;
87  
88      private Class targetClass;
89  
90      private XMLContext xmlContext;
91  
92      private boolean validating = false;
93  
94      private boolean whitespacePreserve = false;
95  
96      private boolean ignoreExtraAttributes = true;
97  
98      private boolean ignoreExtraElements = false;
99  
100     private Properties namespaceMappings;
101 
102     private boolean suppressNamespaces = false;
103 
104     private boolean suppressXsiType = false;
105 
106     /** Returns whether the Castor  {@link Unmarshaller} should ignore attributes that do not match a specific field. */
107     public boolean getIgnoreExtraAttributes() {
108         return ignoreExtraAttributes;
109     }
110 
111     /**
112      * Sets whether the Castor  {@link Unmarshaller} should ignore attributes that do not match a specific field.
113      * Default is <code>true</code>: extra attributes are ignored.
114      *
115      * @see org.exolab.castor.xml.Unmarshaller#setIgnoreExtraAttributes(boolean)
116      */
117     public void setIgnoreExtraAttributes(boolean ignoreExtraAttributes) {
118         this.ignoreExtraAttributes = ignoreExtraAttributes;
119     }
120 
121     /** Returns whether the Castor  {@link Unmarshaller} should ignore elements that do not match a specific field. */
122     public boolean getIgnoreExtraElements() {
123         return ignoreExtraElements;
124     }
125 
126     /**
127      * Sets whether the Castor  {@link Unmarshaller} should ignore elements that do not match a specific field. Default
128      * is <code>false</code>, extra attributes are flagged as an error.
129      *
130      * @see org.exolab.castor.xml.Unmarshaller#setIgnoreExtraElements(boolean)
131      */
132     public void setIgnoreExtraElements(boolean ignoreExtraElements) {
133         this.ignoreExtraElements = ignoreExtraElements;
134     }
135 
136     /** Returns whether the Castor {@link Unmarshaller} should preserve "ignorable" whitespace. */
137     public boolean getWhitespacePreserve() {
138         return whitespacePreserve;
139     }
140 
141     /**
142      * Sets whether the Castor {@link Unmarshaller} should preserve "ignorable" whitespace. Default is
143      * <code>false</code>.
144      *
145      * @see org.exolab.castor.xml.Unmarshaller#setWhitespacePreserve(boolean)
146      */
147     public void setWhitespacePreserve(boolean whitespacePreserve) {
148         this.whitespacePreserve = whitespacePreserve;
149     }
150 
151     /** Returns whether this marshaller should validate in- and outgoing documents. */
152     public boolean isValidating() {
153         return validating;
154     }
155 
156     /**
157      * Sets whether this marshaller should validate in- and outgoing documents. Default is <code>false</code>.
158      *
159      * @see Marshaller#setValidation(boolean)
160      */
161     public void setValidating(boolean validating) {
162         this.validating = validating;
163     }
164 
165     /** Returns the namespace mappings. Property names are interpreted as namespace prefixes; values are namespace URIs. */
166     public Properties getNamespaceMappings() {
167         return namespaceMappings;
168     }
169 
170     /**
171      * Sets the namespace mappings. Property names are interpreted as namespace prefixes; values are namespace URIs.
172      *
173      * @see org.exolab.castor.xml.Marshaller#setNamespaceMapping(String, String)
174      */
175     public void setNamespaceMappings(Properties namespaceMappings) {
176         this.namespaceMappings = namespaceMappings;
177     }
178 
179     /**
180      * Sets the encoding to be used for stream access. If this property is not set, the default encoding is used.
181      *
182      * @see #DEFAULT_ENCODING
183      */
184     public void setEncoding(String encoding) {
185         this.encoding = encoding;
186     }
187 
188     /** Sets the locations of the Castor XML Mapping files. */
189     public void setMappingLocation(Resource mappingLocation) {
190         mappingLocations = new Resource[]{mappingLocation};
191     }
192 
193     /** Sets the locations of the Castor XML Mapping files. */
194     public void setMappingLocations(Resource[] mappingLocations) {
195         this.mappingLocations = mappingLocations;
196     }
197 
198     /** Returns whether this marshaller should output namespaces. */
199     public boolean isSuppressNamespaces() {
200         return suppressNamespaces;
201     }
202 
203     /**
204      * Sets whether this marshaller should output namespaces. The default is {@code false}, i.e. namespaces are
205      * written.
206      *
207      * @see org.exolab.castor.xml.Marshaller#setSuppressNamespaces(boolean)
208      */
209     public void setSuppressNamespaces(boolean suppressNamespaces) {
210         this.suppressNamespaces = suppressNamespaces;
211     }
212 
213     /** Sets whether this marshaller should output the xsi:type attribute. */
214     public boolean isSuppressXsiType() {
215         return suppressXsiType;
216     }
217 
218     /**
219      * Sets whether this marshaller should output the {@code xsi:type} attribute. The default is {@code false}, i.e. the
220      * {@code xsi:type} is written.
221      *
222      * @see org.exolab.castor.xml.Marshaller#setSuppressXSIType(boolean)
223      */
224     public void setSuppressXsiType(boolean suppressXsiType) {
225         this.suppressXsiType = suppressXsiType;
226     }
227 
228     /**
229      * Sets the Castor target class. If this property is set, this <code>CastorMarshaller</code> is tied to this one
230      * specific class. Use a mapping file for unmarshalling multiple classes.
231      */
232     public void setTargetClass(Class targetClass) {
233         this.targetClass = targetClass;
234     }
235 
236     public final void afterPropertiesSet() throws IOException {
237         if (mappingLocations != null && targetClass != null) {
238             throw new IllegalArgumentException("Cannot set both the 'mappingLocations' and 'targetClass' property. " +
239                     "Set targetClass for unmarshalling a single class, and 'mappingLocations' for multiple classes'");
240         }
241         if (logger.isInfoEnabled()) {
242             if (mappingLocations != null) {
243                 logger.info("Configured using " + StringUtils.arrayToCommaDelimitedString(mappingLocations));
244             }
245             else if (targetClass != null) {
246                 logger.info("Configured for target class [" + targetClass.getName() + "]");
247             }
248             else {
249                 logger.info("Using default configuration");
250             }
251         }
252         try {
253             xmlContext = createXMLContext(mappingLocations, targetClass);
254         }
255         catch (MappingException ex) {
256             throw new CastorSystemException("Could not load Castor mapping: " + ex.getMessage(), ex);
257         }
258         catch (ResolverException rex) {
259             throw new CastorSystemException("Could not load Castor mapping: " + rex.getMessage(), rex);
260         }
261     }
262 
263     /** Returns <code>true</code> for all classes, i.e. Castor supports arbitrary classes. */
264     public boolean supports(Class clazz) {
265         return true;
266     }
267 
268     /**
269      * Creates the Castor <code>XMLContext</code>. Subclasses can override this to create a custom context.
270      * <p/>
271      * The default implementation loads mapping files if defined, and the target class if not defined.
272      *
273      * @return the created resolver
274      * @throws MappingException when the mapping file cannot be loaded
275      * @throws IOException      in case of I/O errors
276      * @see XMLContext#addMapping(org.exolab.castor.mapping.Mapping)
277      * @see XMLContext#addClass(Class)
278      */
279     protected XMLContext createXMLContext(Resource[] mappingLocations, Class targetClass)
280             throws MappingException, IOException, ResolverException {
281         XMLContext context = new XMLContext();
282         if (!ObjectUtils.isEmpty(mappingLocations)) {
283             Mapping mapping = new Mapping();
284             for (int i = 0; i < mappingLocations.length; i++) {
285                 mapping.loadMapping(SaxUtils.createInputSource(mappingLocations[i]));
286             }
287             context.addMapping(mapping);
288         }
289         if (targetClass != null) {
290             context.addClass(targetClass);
291         }
292         return context;
293     }
294 
295     //
296     // Marshalling
297     //
298 
299     protected final void marshalDomNode(Object graph, Node node) throws XmlMappingException {
300         marshalSaxHandlers(graph, new DomContentHandler(node), null);
301     }
302 
303     protected final void marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)
304             throws XmlMappingException {
305         Marshaller marshaller = xmlContext.createMarshaller();
306         marshaller.setContentHandler(contentHandler);
307         marshal(graph, marshaller);
308     }
309 
310     protected final void marshalOutputStream(Object graph, OutputStream outputStream)
311             throws XmlMappingException, IOException {
312         marshalWriter(graph, new OutputStreamWriter(outputStream, encoding));
313     }
314 
315     protected final void marshalWriter(Object graph, Writer writer) throws XmlMappingException, IOException {
316         Marshaller marshaller = xmlContext.createMarshaller();
317         marshaller.setWriter(writer);
318         marshal(graph, marshaller);
319     }
320 
321     protected final void marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter) throws XmlMappingException {
322         marshalSaxHandlers(graph, new StaxEventContentHandler(eventWriter), null);
323     }
324 
325     protected final void marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter) throws XmlMappingException {
326         marshalSaxHandlers(graph, new StaxStreamContentHandler(streamWriter), null);
327     }
328 
329     private void marshal(Object graph, Marshaller marshaller) {
330         try {
331             customizeMarshaller(marshaller);
332             marshaller.marshal(graph);
333         }
334         catch (XMLException ex) {
335             throw convertCastorException(ex, true);
336         }
337     }
338 
339     /**
340      * Template method that allows for customizing of the given Castor {@link Marshaller}.
341      * <p/>
342      * Default implementation invokes {@link Marshaller#setValidation(boolean)} with the property set on this
343      * marshaller, and calls {@link Marshaller#setNamespaceMapping(String, String)} with the {@linkplain
344      * #setNamespaceMappings(java.util.Properties) namespace mappings}.
345      */
346     protected void customizeMarshaller(Marshaller marshaller) {
347         marshaller.setValidation(isValidating());
348         marshaller.setSuppressNamespaces(isSuppressNamespaces());
349         marshaller.setSuppressXSIType(isSuppressXsiType());
350         Properties namespaceMappings = getNamespaceMappings();
351         if (namespaceMappings != null) {
352             for (Iterator iterator = namespaceMappings.keySet().iterator(); iterator.hasNext();) {
353                 String prefix = (String) iterator.next();
354                 String uri = namespaceMappings.getProperty(prefix);
355                 marshaller.setNamespaceMapping(prefix, uri);
356             }
357         }
358     }
359 
360     //
361     // Unmarshalling
362     //
363 
364     protected final Object unmarshalDomNode(Node node) throws XmlMappingException {
365         try {
366             return createUnmarshaller().unmarshal(node);
367         }
368         catch (XMLException ex) {
369             throw convertCastorException(ex, false);
370         }
371     }
372 
373     protected final Object unmarshalInputStream(InputStream inputStream) throws XmlMappingException, IOException {
374         try {
375             return createUnmarshaller().unmarshal(new InputSource(inputStream));
376         }
377         catch (XMLException ex) {
378             throw convertCastorException(ex, false);
379         }
380     }
381 
382     protected final Object unmarshalReader(Reader reader) throws XmlMappingException, IOException {
383         try {
384             return createUnmarshaller().unmarshal(new InputSource(reader));
385         }
386         catch (XMLException ex) {
387             throw convertCastorException(ex, false);
388         }
389     }
390 
391     protected final Object unmarshalXmlEventReader(XMLEventReader eventReader) {
392         XMLReader reader = new StaxEventXmlReader(eventReader);
393         try {
394             return unmarshalSaxReader(reader, new InputSource());
395         }
396         catch (IOException ex) {
397             throw new CastorUnmarshallingFailureException(new MarshalException(ex));
398         }
399     }
400 
401     protected final Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)
402             throws XmlMappingException, IOException {
403         UnmarshalHandler unmarshalHandler = createUnmarshaller().createHandler();
404         try {
405             ContentHandler contentHandler = Unmarshaller.getContentHandler(unmarshalHandler);
406             xmlReader.setContentHandler(contentHandler);
407             xmlReader.parse(inputSource);
408             return unmarshalHandler.getObject();
409         }
410         catch (SAXException ex) {
411             throw new CastorUnmarshallingFailureException(ex);
412         }
413     }
414 
415     protected final Object unmarshalXmlStreamReader(XMLStreamReader streamReader) {
416         XMLReader reader = new StaxStreamXmlReader(streamReader);
417         try {
418             return unmarshalSaxReader(reader, new InputSource());
419         }
420         catch (IOException ex) {
421             throw new CastorUnmarshallingFailureException(new MarshalException(ex));
422         }
423     }
424 
425     private Unmarshaller createUnmarshaller() {
426         Unmarshaller unmarshaller = xmlContext.createUnmarshaller();
427         if (targetClass != null) {
428             unmarshaller.setClass(targetClass);
429             unmarshaller.setClassLoader(targetClass.getClassLoader());
430         }
431         customizeUnmarshaller(unmarshaller);
432         return unmarshaller;
433     }
434 
435     /**
436      * Template method that allows for customizing of the given Castor {@link Unmarshaller}.
437      * <p/>
438      * Default implementation invokes {@link Unmarshaller#setValidation(boolean)}, {@link
439      * Unmarshaller#setWhitespacePreserve(boolean)}, {@link Unmarshaller#setIgnoreExtraAttributes(boolean)}, and {@link
440      * Unmarshaller#setIgnoreExtraElements(boolean)} with the properties set on this marshaller.
441      */
442     protected void customizeUnmarshaller(Unmarshaller unmarshaller) {
443         unmarshaller.setValidation(isValidating());
444         unmarshaller.setWhitespacePreserve(getWhitespacePreserve());
445         unmarshaller.setIgnoreExtraAttributes(getIgnoreExtraAttributes());
446         unmarshaller.setIgnoreExtraElements(getIgnoreExtraElements());
447     }
448 
449     /**
450      * Converts the given <code>CastorException</code> to an appropriate exception from the
451      * <code>org.springframework.oxm</code> hierarchy.
452      * <p/>
453      * The default implementation delegates to <code>CastorUtils</code>. Can be overridden in subclasses.
454      * <p/>
455      * A boolean flag is used to indicate whether this exception occurs during marshalling or unmarshalling, since
456      * Castor itself does not make this distinction in its exception hierarchy.
457      *
458      * @param ex          Castor <code>XMLException</code> that occured
459      * @param marshalling indicates whether the exception occurs during marshalling (<code>true</code>), or
460      *                    unmarshalling (<code>false</code>)
461      * @return the corresponding <code>XmlMappingException</code>
462      * @see CastorUtils#convertXmlException
463      */
464     public XmlMappingException convertCastorException(XMLException ex, boolean marshalling) {
465         return CastorUtils.convertXmlException(ex, marshalling);
466     }
467 }