001/*
002 * Copyright 2002-2018 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.jibx;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.io.Reader;
025import java.io.Writer;
026import javax.xml.stream.XMLEventReader;
027import javax.xml.stream.XMLEventWriter;
028import javax.xml.stream.XMLStreamException;
029import javax.xml.stream.XMLStreamReader;
030import javax.xml.stream.XMLStreamWriter;
031import javax.xml.transform.OutputKeys;
032import javax.xml.transform.Result;
033import javax.xml.transform.Source;
034import javax.xml.transform.Transformer;
035import javax.xml.transform.TransformerException;
036import javax.xml.transform.TransformerFactory;
037import javax.xml.transform.dom.DOMResult;
038import javax.xml.transform.dom.DOMSource;
039import javax.xml.transform.sax.SAXResult;
040import javax.xml.transform.sax.SAXSource;
041import javax.xml.transform.stream.StreamResult;
042import javax.xml.transform.stream.StreamSource;
043
044import org.jibx.runtime.BindingDirectory;
045import org.jibx.runtime.IBindingFactory;
046import org.jibx.runtime.IMarshallingContext;
047import org.jibx.runtime.IUnmarshallingContext;
048import org.jibx.runtime.IXMLReader;
049import org.jibx.runtime.IXMLWriter;
050import org.jibx.runtime.JiBXException;
051import org.jibx.runtime.ValidationException;
052import org.jibx.runtime.impl.MarshallingContext;
053import org.jibx.runtime.impl.StAXReaderWrapper;
054import org.jibx.runtime.impl.StAXWriter;
055import org.jibx.runtime.impl.UnmarshallingContext;
056import org.w3c.dom.Node;
057import org.xml.sax.ContentHandler;
058import org.xml.sax.InputSource;
059import org.xml.sax.XMLReader;
060import org.xml.sax.ext.LexicalHandler;
061
062import org.springframework.beans.factory.InitializingBean;
063import org.springframework.oxm.MarshallingFailureException;
064import org.springframework.oxm.UnmarshallingFailureException;
065import org.springframework.oxm.ValidationFailureException;
066import org.springframework.oxm.XmlMappingException;
067import org.springframework.oxm.support.AbstractMarshaller;
068import org.springframework.util.Assert;
069import org.springframework.util.ClassUtils;
070import org.springframework.util.StringUtils;
071import org.springframework.util.xml.StaxUtils;
072
073/**
074 * Implementation of the {@code Marshaller} and {@code Unmarshaller} interfaces for JiBX.
075 *
076 * <p>The typical usage will be to set the {@code targetClass} and optionally the
077 * {@code bindingName} property on this bean.
078 *
079 * @author Arjen Poutsma
080 * @since 3.0
081 * @see org.jibx.runtime.IMarshallingContext
082 * @see org.jibx.runtime.IUnmarshallingContext
083 */
084public class JibxMarshaller extends AbstractMarshaller implements InitializingBean {
085
086        private static final String DEFAULT_BINDING_NAME = "binding";
087
088
089        private Class<?> targetClass;
090
091        private String targetPackage;
092
093        private String bindingName;
094
095        private int indent = -1;
096
097        private String encoding = "UTF-8";
098
099        private Boolean standalone;
100
101        private String docTypeRootElementName;
102
103        private String docTypeSystemId;
104
105        private String docTypePublicId;
106
107        private String docTypeInternalSubset;
108
109        private IBindingFactory bindingFactory;
110
111        private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
112
113
114        /**
115         * Set the target class for this instance. Setting either this property or the
116         * {@link #setTargetPackage(String) targetPackage} property is required.
117         * <p>If this property is set, {@link #setTargetPackage(String) targetPackage} is ignored.
118         */
119        public void setTargetClass(Class<?> targetClass) {
120                this.targetClass = targetClass;
121        }
122
123        /**
124         * Set the target package for this instance. Setting either this property or the
125         * {@link #setTargetClass(Class) targetClass} property is required.
126         * <p>If {@link #setTargetClass(Class) targetClass} is set, this property is ignored.
127         */
128        public void setTargetPackage(String targetPackage) {
129                this.targetPackage = targetPackage;
130        }
131
132        /**
133         * Set the optional binding name for this instance.
134         */
135        public void setBindingName(String bindingName) {
136                this.bindingName = bindingName;
137        }
138
139        /**
140         * Set the number of nesting indent spaces. Default is {@code -1}, i.e. no indentation.
141         */
142        public void setIndent(int indent) {
143                this.indent = indent;
144        }
145
146        /**
147         * Set the document encoding using for marshalling. Default is UTF-8.
148         */
149        public void setEncoding(String encoding) {
150                this.encoding = encoding;
151        }
152
153        @Override
154        protected String getDefaultEncoding() {
155                return this.encoding;
156        }
157
158        /**
159         * Set the document standalone flag for marshalling. By default, this flag is not present.
160         */
161        public void setStandalone(Boolean standalone) {
162                this.standalone = standalone;
163        }
164
165        /**
166         * Set the root element name for the DTD declaration written when marshalling.
167         * By default, this is {@code null} (i.e. no DTD declaration is written).
168         * <p>If set to a value, the system ID or public ID also need to be set.
169         * @see #setDocTypeSystemId(String)
170         * @see #setDocTypePublicId(String)
171         */
172        public void setDocTypeRootElementName(String docTypeRootElementName) {
173                this.docTypeRootElementName = docTypeRootElementName;
174        }
175
176        /**
177         * Set the system id for the DTD declaration written when marshalling.
178         * By default, this is {@code null}. Only used when the root element also has been set.
179         * <p>Set either this property or {@code docTypePublicId}, not both.
180         * @see #setDocTypeRootElementName(String)
181         */
182        public void setDocTypeSystemId(String docTypeSystemId) {
183                this.docTypeSystemId = docTypeSystemId;
184        }
185
186        /**
187         * Set the public id for the DTD declaration written when marshalling.
188         * By default, this is {@code null}. Only used when the root element also has been set.
189         * <p>Set either this property or {@code docTypeSystemId}, not both.
190         * @see #setDocTypeRootElementName(String)
191         */
192        public void setDocTypePublicId(String docTypePublicId) {
193                this.docTypePublicId = docTypePublicId;
194        }
195
196        /**
197         * Set the internal subset Id for the DTD declaration written when marshalling.
198         * By default, this is {@code null}. Only used when the root element also has been set.
199         * @see #setDocTypeRootElementName(String)
200         */
201        public void setDocTypeInternalSubset(String docTypeInternalSubset) {
202                this.docTypeInternalSubset = docTypeInternalSubset;
203        }
204
205
206        @Override
207        public void afterPropertiesSet() throws JiBXException {
208                if (this.targetClass != null) {
209                        if (StringUtils.hasLength(this.bindingName)) {
210                                if (logger.isInfoEnabled()) {
211                                        logger.info("Configured for target class [" + this.targetClass +
212                                                        "] using binding [" + this.bindingName + "]");
213                                }
214                                this.bindingFactory = BindingDirectory.getFactory(this.bindingName, this.targetClass);
215                        }
216                        else {
217                                if (logger.isInfoEnabled()) {
218                                        logger.info("Configured for target class [" + this.targetClass + "]");
219                                }
220                                this.bindingFactory = BindingDirectory.getFactory(this.targetClass);
221                        }
222                }
223                else if (this.targetPackage != null) {
224                        if (!StringUtils.hasLength(this.bindingName)) {
225                                this.bindingName = DEFAULT_BINDING_NAME;
226                        }
227                        if (logger.isInfoEnabled()) {
228                                logger.info("Configured for target package [" + this.targetPackage +
229                                                "] using binding [" + this.bindingName + "]");
230                        }
231                        this.bindingFactory = BindingDirectory.getFactory(this.bindingName, this.targetPackage);
232                }
233                else {
234                        throw new IllegalArgumentException("Either 'targetClass' or 'targetPackage' is required");
235                }
236        }
237
238
239        @Override
240        public boolean supports(Class<?> clazz) {
241                Assert.notNull(clazz, "Class must not be null");
242                if (this.targetClass != null) {
243                        return (this.targetClass == clazz);
244                }
245                String[] mappedClasses = this.bindingFactory.getMappedClasses();
246                String className = clazz.getName();
247                for (String mappedClass : mappedClasses) {
248                        if (className.equals(mappedClass)) {
249                                return true;
250                        }
251                }
252                return false;
253        }
254
255
256        // Supported marshalling
257
258        @Override
259        protected void marshalOutputStream(Object graph, OutputStream outputStream)
260                        throws XmlMappingException, IOException {
261                try {
262                        IMarshallingContext marshallingContext = createMarshallingContext();
263                        marshallingContext.startDocument(this.encoding, this.standalone, outputStream);
264                        marshalDocument(marshallingContext, graph);
265                }
266                catch (JiBXException ex) {
267                        throw convertJibxException(ex, true);
268                }
269        }
270
271        @Override
272        protected void marshalWriter(Object graph, Writer writer) throws XmlMappingException, IOException {
273                try {
274                        IMarshallingContext marshallingContext = createMarshallingContext();
275                        marshallingContext.startDocument(this.encoding, this.standalone, writer);
276                        marshalDocument(marshallingContext, graph);
277                }
278                catch (JiBXException ex) {
279                        throw convertJibxException(ex, true);
280                }
281        }
282
283        private void marshalDocument(IMarshallingContext marshallingContext, Object graph) throws IOException, JiBXException {
284                if (StringUtils.hasLength(this.docTypeRootElementName)) {
285                        IXMLWriter xmlWriter = marshallingContext.getXmlWriter();
286                        xmlWriter.writeDocType(this.docTypeRootElementName, this.docTypeSystemId,
287                                        this.docTypePublicId, this.docTypeInternalSubset);
288                }
289                marshallingContext.marshalDocument(graph);
290        }
291
292
293        // Unsupported marshalling
294
295        @Override
296        protected void marshalDomNode(Object graph, Node node) throws XmlMappingException {
297                try {
298                        // JiBX does not support DOM natively, so we write to a buffer first, and transform that to the Node
299                        Result result = new DOMResult(node);
300                        transformAndMarshal(graph, result);
301                }
302                catch (IOException ex) {
303                        throw new MarshallingFailureException("JiBX marshalling exception", ex);
304                }
305        }
306
307        @Override
308        protected void marshalXmlEventWriter(Object graph, XMLEventWriter eventWriter) {
309                XMLStreamWriter streamWriter = StaxUtils.createEventStreamWriter(eventWriter);
310                marshalXmlStreamWriter(graph, streamWriter);
311        }
312
313        @Override
314        protected void marshalXmlStreamWriter(Object graph, XMLStreamWriter streamWriter) throws XmlMappingException {
315                try {
316                        MarshallingContext marshallingContext = (MarshallingContext) createMarshallingContext();
317                        IXMLWriter xmlWriter = new StAXWriter(marshallingContext.getNamespaces(), streamWriter);
318                        marshallingContext.setXmlWriter(xmlWriter);
319                        marshallingContext.marshalDocument(graph);
320                }
321                catch (JiBXException ex) {
322                        throw convertJibxException(ex, false);
323                }
324        }
325
326        @Override
327        protected void marshalSaxHandlers(Object graph, ContentHandler contentHandler, LexicalHandler lexicalHandler)
328                        throws XmlMappingException {
329                try {
330                        // JiBX does not support SAX natively, so we write to a buffer first, and transform that to the handlers
331                        SAXResult saxResult = new SAXResult(contentHandler);
332                        saxResult.setLexicalHandler(lexicalHandler);
333                        transformAndMarshal(graph, saxResult);
334                }
335                catch (IOException ex) {
336                        throw new MarshallingFailureException("JiBX marshalling exception", ex);
337                }
338        }
339
340        private void transformAndMarshal(Object graph, Result result) throws IOException {
341                try {
342                        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
343                        marshalOutputStream(graph, os);
344                        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
345                        Transformer transformer = this.transformerFactory.newTransformer();
346                        transformer.transform(new StreamSource(is), result);
347                }
348                catch (TransformerException ex) {
349                        throw new MarshallingFailureException(
350                                        "Could not transform to [" + ClassUtils.getShortName(result.getClass()) + "]", ex);
351                }
352
353        }
354
355
356        // Unmarshalling
357
358        @Override
359        protected Object unmarshalXmlEventReader(XMLEventReader eventReader) {
360                try {
361                        XMLStreamReader streamReader = StaxUtils.createEventStreamReader(eventReader);
362                        return unmarshalXmlStreamReader(streamReader);
363                }
364                catch (XMLStreamException ex) {
365                        return new UnmarshallingFailureException("JiBX unmarshalling exception", ex);
366                }
367        }
368
369        @Override
370        protected Object unmarshalXmlStreamReader(XMLStreamReader streamReader) {
371                try {
372                        UnmarshallingContext unmarshallingContext = (UnmarshallingContext) createUnmarshallingContext();
373                        IXMLReader xmlReader = new StAXReaderWrapper(streamReader, null, true);
374                        unmarshallingContext.setDocument(xmlReader);
375                        return unmarshallingContext.unmarshalElement();
376                }
377                catch (JiBXException ex) {
378                        throw convertJibxException(ex, false);
379                }
380        }
381
382        @Override
383        protected Object unmarshalInputStream(InputStream inputStream) throws XmlMappingException, IOException {
384                try {
385                        IUnmarshallingContext unmarshallingContext = createUnmarshallingContext();
386                        return unmarshallingContext.unmarshalDocument(inputStream, this.encoding);
387                }
388                catch (JiBXException ex) {
389                        throw convertJibxException(ex, false);
390                }
391        }
392
393        @Override
394        protected Object unmarshalReader(Reader reader) throws XmlMappingException, IOException {
395                try {
396                        IUnmarshallingContext unmarshallingContext = createUnmarshallingContext();
397                        return unmarshallingContext.unmarshalDocument(reader);
398                }
399                catch (JiBXException ex) {
400                        throw convertJibxException(ex, false);
401                }
402        }
403
404
405        // Unsupported Unmarshalling
406
407        @Override
408        protected Object unmarshalDomNode(Node node) throws XmlMappingException {
409                try {
410                        return transformAndUnmarshal(new DOMSource(node), null);
411                }
412                catch (IOException ex) {
413                        throw new UnmarshallingFailureException("JiBX unmarshalling exception", ex);
414                }
415        }
416
417        @Override
418        protected Object unmarshalSaxReader(XMLReader xmlReader, InputSource inputSource)
419                        throws XmlMappingException, IOException {
420
421                return transformAndUnmarshal(new SAXSource(xmlReader, inputSource), inputSource.getEncoding());
422        }
423
424        private Object transformAndUnmarshal(Source source, String encoding) throws IOException {
425                try {
426                        Transformer transformer = this.transformerFactory.newTransformer();
427                        if (encoding != null) {
428                                transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
429                        }
430                        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
431                        transformer.transform(source, new StreamResult(os));
432                        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
433                        return unmarshalInputStream(is);
434                }
435                catch (TransformerException ex) {
436                        throw new MarshallingFailureException(
437                                        "Could not transform from [" + ClassUtils.getShortName(source.getClass()) + "]", ex);
438                }
439        }
440
441
442        /**
443         * Create a new {@code IMarshallingContext}, configured with the correct indentation.
444         * @return the created marshalling context
445         * @throws JiBXException in case of errors
446         */
447        protected IMarshallingContext createMarshallingContext() throws JiBXException {
448                IMarshallingContext marshallingContext = this.bindingFactory.createMarshallingContext();
449                marshallingContext.setIndent(this.indent);
450                return marshallingContext;
451        }
452
453        /**
454         * Create a new {@code IUnmarshallingContext}.
455         * @return the created unmarshalling context
456         * @throws JiBXException in case of errors
457         */
458        protected IUnmarshallingContext createUnmarshallingContext() throws JiBXException {
459                return this.bindingFactory.createUnmarshallingContext();
460        }
461
462        /**
463         * Convert the given {@code JiBXException} to an appropriate exception from the
464         * {@code org.springframework.oxm} hierarchy.
465         * <p>A boolean flag is used to indicate whether this exception occurs during marshalling or
466         * unmarshalling, since JiBX itself does not make this distinction in its exception hierarchy.
467         * @param ex {@code JiBXException} that occurred
468         * @param marshalling indicates whether the exception occurs during marshalling ({@code true}),
469         * or unmarshalling ({@code false})
470         * @return the corresponding {@code XmlMappingException}
471         */
472        public XmlMappingException convertJibxException(JiBXException ex, boolean marshalling) {
473                if (ex instanceof ValidationException) {
474                        return new ValidationFailureException("JiBX validation exception", ex);
475                }
476                else {
477                        if (marshalling) {
478                                return new MarshallingFailureException("JiBX marshalling exception", ex);
479                        }
480                        else {
481                                return new UnmarshallingFailureException("JiBX unmarshalling exception", ex);
482                        }
483                }
484        }
485
486}