View Javadoc

1   /*
2    * Copyright 2006 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.jaxb;
18  
19  import java.awt.*;
20  import java.io.ByteArrayInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.lang.reflect.GenericArrayType;
26  import java.lang.reflect.ParameterizedType;
27  import java.lang.reflect.Type;
28  import java.math.BigDecimal;
29  import java.math.BigInteger;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.net.URLDecoder;
33  import java.net.URLEncoder;
34  import java.util.Arrays;
35  import java.util.Calendar;
36  import java.util.Date;
37  import java.util.Map;
38  import java.util.UUID;
39  import javax.activation.DataHandler;
40  import javax.activation.DataSource;
41  import javax.xml.XMLConstants;
42  import javax.xml.bind.JAXBContext;
43  import javax.xml.bind.JAXBElement;
44  import javax.xml.bind.JAXBException;
45  import javax.xml.bind.Marshaller;
46  import javax.xml.bind.Unmarshaller;
47  import javax.xml.bind.annotation.XmlRootElement;
48  import javax.xml.bind.annotation.XmlType;
49  import javax.xml.bind.annotation.adapters.XmlAdapter;
50  import javax.xml.bind.attachment.AttachmentMarshaller;
51  import javax.xml.bind.attachment.AttachmentUnmarshaller;
52  import javax.xml.datatype.Duration;
53  import javax.xml.datatype.XMLGregorianCalendar;
54  import javax.xml.namespace.QName;
55  import javax.xml.stream.XMLEventReader;
56  import javax.xml.stream.XMLEventWriter;
57  import javax.xml.stream.XMLStreamReader;
58  import javax.xml.stream.XMLStreamWriter;
59  import javax.xml.transform.Result;
60  import javax.xml.transform.Source;
61  import javax.xml.validation.Schema;
62  
63  import org.springframework.beans.factory.BeanClassLoaderAware;
64  import org.springframework.core.io.Resource;
65  import org.springframework.oxm.GenericMarshaller;
66  import org.springframework.oxm.GenericUnmarshaller;
67  import org.springframework.oxm.XmlMappingException;
68  import org.springframework.oxm.mime.MimeContainer;
69  import org.springframework.oxm.mime.MimeMarshaller;
70  import org.springframework.oxm.mime.MimeUnmarshaller;
71  import org.springframework.util.Assert;
72  import org.springframework.util.ClassUtils;
73  import org.springframework.util.FileCopyUtils;
74  import org.springframework.util.ObjectUtils;
75  import org.springframework.util.StringUtils;
76  import org.springframework.xml.transform.TraxUtils;
77  import org.springframework.xml.validation.SchemaLoaderUtils;
78  
79  /**
80   * Implementation of the <code>Marshaller</code> interface for JAXB 2.0.
81   * <p/>
82   * The typical usage will be to set either the <code>contextPath</code> or the <code>classesToBeBound</code> property on
83   * this bean, possibly customize the marshaller and unmarshaller by setting properties, schemas, adapters, and
84   * listeners, and to refer to it.
85   *
86   * @author Arjen Poutsma
87   * @see #setContextPath(String)
88   * @see #setClassesToBeBound(Class[])
89   * @see #setJaxbContextProperties(java.util.Map)
90   * @see #setMarshallerProperties(java.util.Map)
91   * @see #setUnmarshallerProperties(java.util.Map)
92   * @see #setSchema(org.springframework.core.io.Resource)
93   * @see #setSchemas(org.springframework.core.io.Resource[])
94   * @see #setMarshallerListener(javax.xml.bind.Marshaller.Listener)
95   * @see #setUnmarshallerListener(javax.xml.bind.Unmarshaller.Listener)
96   * @see #setAdapters(javax.xml.bind.annotation.adapters.XmlAdapter[])
97   * @since 1.0.0
98   */
99  public class Jaxb2Marshaller extends AbstractJaxbMarshaller
100         implements MimeMarshaller, MimeUnmarshaller, GenericMarshaller, GenericUnmarshaller, BeanClassLoaderAware {
101 
102     private ClassLoader classLoader;
103 
104     private Resource[] schemaResources;
105 
106     private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
107 
108     private Marshaller.Listener marshallerListener;
109 
110     private Unmarshaller.Listener unmarshallerListener;
111 
112     private XmlAdapter[] adapters;
113 
114     private Schema schema;
115 
116     private Class[] classesToBeBound;
117 
118     private Map<String, ?> jaxbContextProperties;
119 
120     private boolean mtomEnabled = false;
121 
122     public void setBeanClassLoader(ClassLoader classLoader) {
123         this.classLoader = classLoader;
124     }
125 
126     /**
127      * Sets the <code>XmlAdapter</code>s to be registered with the JAXB <code>Marshaller</code> and
128      * <code>Unmarshaller</code>
129      */
130     public void setAdapters(XmlAdapter[] adapters) {
131         this.adapters = adapters;
132     }
133 
134     /**
135      * Sets the list of java classes to be recognized by a newly created JAXBContext. Setting this property or
136      * <code>contextPath</code> is required.
137      *
138      * @see #setContextPath(String)
139      */
140     public void setClassesToBeBound(Class[] classesToBeBound) {
141         this.classesToBeBound = classesToBeBound;
142     }
143 
144     /**
145      * Sets the <code>JAXBContext</code> properties. These implementation-specific properties will be set on the
146      * <code>JAXBContext</code>.
147      */
148     public void setJaxbContextProperties(Map<String, ?> jaxbContextProperties) {
149         this.jaxbContextProperties = jaxbContextProperties;
150     }
151 
152     /** Sets the <code>Marshaller.Listener</code> to be registered with the JAXB <code>Marshaller</code>. */
153     public void setMarshallerListener(Marshaller.Listener marshallerListener) {
154         this.marshallerListener = marshallerListener;
155     }
156 
157     /**
158      * Indicates whether MTOM support should be enabled or not. Default is <code>false</code>, marshalling using
159      * XOP/MTOM is not enabled.
160      */
161     public void setMtomEnabled(boolean mtomEnabled) {
162         this.mtomEnabled = mtomEnabled;
163     }
164 
165     /**
166      * Sets the schema language. Default is the W3C XML Schema: <code>http://www.w3.org/2001/XMLSchema"</code>.
167      *
168      * @see XMLConstants#W3C_XML_SCHEMA_NS_URI
169      * @see XMLConstants#RELAXNG_NS_URI
170      */
171     public void setSchemaLanguage(String schemaLanguage) {
172         this.schemaLanguage = schemaLanguage;
173     }
174 
175     /** Sets the schema resource to use for validation. */
176     public void setSchema(Resource schemaResource) {
177         schemaResources = new Resource[]{schemaResource};
178     }
179 
180     /** Sets the schema resources to use for validation. */
181     public void setSchemas(Resource[] schemaResources) {
182         this.schemaResources = schemaResources;
183     }
184 
185     /** Sets the <code>Unmarshaller.Listener</code> to be registered with the JAXB <code>Unmarshaller</code>. */
186     public void setUnmarshallerListener(Unmarshaller.Listener unmarshallerListener) {
187         this.unmarshallerListener = unmarshallerListener;
188     }
189 
190     public boolean supports(Type type) {
191         if (type instanceof Class) {
192             return supportsInternal((Class) type, true);
193         }
194         else if (type instanceof ParameterizedType) {
195             ParameterizedType parameterizedType = (ParameterizedType) type;
196             if (JAXBElement.class.equals(parameterizedType.getRawType())) {
197                 Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1,
198                         "Invalid amount of parameterized types in JAXBElement");
199                 Type typeArgument = parameterizedType.getActualTypeArguments()[0];
200                 if (typeArgument instanceof Class) {
201                     Class clazz = (Class) typeArgument;
202                     if (!isPrimitiveType(clazz) && !isStandardType(clazz) && !supportsInternal(clazz, false)) {
203                         return false;
204                     }
205                 }
206                 else if (typeArgument instanceof GenericArrayType) {
207                     GenericArrayType genericArrayType = (GenericArrayType) typeArgument;
208                     return genericArrayType.getGenericComponentType().equals(Byte.TYPE);
209                 }
210                 else if (!supports(typeArgument)) {
211                     return false;
212                 }
213                 return true;
214             }
215         }
216         return false;
217     }
218 
219     private boolean isPrimitiveType(Class clazz) {
220         return (Boolean.class.equals(clazz) || Byte.class.equals(clazz) || Short.class.equals(clazz) ||
221                 Integer.class.equals(clazz) || Long.class.equals(clazz) || Float.class.equals(clazz) ||
222                 Double.class.equals(clazz) || byte[].class.equals(clazz));
223     }
224 
225     private boolean isStandardType(Class clazz) {
226         return (String.class.equals(clazz) || BigInteger.class.equals(clazz) || BigDecimal.class.equals(clazz) ||
227                 Calendar.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz) ||
228                 QName.class.equals(clazz) || URI.class.equals(clazz) ||
229                 XMLGregorianCalendar.class.isAssignableFrom(clazz) || Duration.class.isAssignableFrom(clazz) ||
230                 Object.class.equals(clazz) || Image.class.isAssignableFrom(clazz) || DataHandler.class.equals(clazz) ||
231                 Source.class.isAssignableFrom(clazz) || UUID.class.equals(clazz));
232     }
233 
234     public boolean supports(Class clazz) {
235         return supportsInternal(clazz, true);
236     }
237 
238     private boolean supportsInternal(Class<?> clazz, boolean checkForXmlRootElement) {
239         if (checkForXmlRootElement && clazz.getAnnotation(XmlRootElement.class) == null) {
240             return false;
241         }
242         if (clazz.getAnnotation(XmlType.class) == null) {
243             return false;
244         }
245         if (StringUtils.hasLength(getContextPath())) {
246             String className = ClassUtils.getQualifiedName(clazz);
247             int lastDotIndex = className.lastIndexOf('.');
248             if (lastDotIndex == -1) {
249                 return false;
250             }
251             String packageName = className.substring(0, lastDotIndex);
252             String[] contextPaths = StringUtils.tokenizeToStringArray(getContextPath(), ":");
253             for (String contextPath : contextPaths) {
254                 if (contextPath.equals(packageName)) {
255                     return true;
256                 }
257             }
258             return false;
259         }
260         else if (!ObjectUtils.isEmpty(classesToBeBound)) {
261             return Arrays.asList(classesToBeBound).contains(clazz);
262         }
263         return false;
264     }
265 
266     /*
267      * JAXBContext
268      */
269 
270     @Override
271     protected JAXBContext createJaxbContext() throws Exception {
272         if (JaxbUtils.getJaxbVersion(classLoader) < JaxbUtils.JAXB_2) {
273             throw new IllegalStateException(
274                     "Cannot use Jaxb2Marshaller in combination with JAXB 1.0. Use Jaxb1Marshaller instead.");
275         }
276         if (StringUtils.hasLength(getContextPath()) && !ObjectUtils.isEmpty(classesToBeBound)) {
277             throw new IllegalArgumentException("specify either contextPath or classesToBeBound property; not both");
278         }
279         if (!ObjectUtils.isEmpty(schemaResources)) {
280             if (logger.isDebugEnabled()) {
281                 logger.debug(
282                         "Setting validation schema to " + StringUtils.arrayToCommaDelimitedString(schemaResources));
283             }
284             schema = SchemaLoaderUtils.loadSchema(schemaResources, schemaLanguage);
285         }
286         if (StringUtils.hasLength(getContextPath())) {
287             return createJaxbContextFromContextPath();
288         }
289         else if (!ObjectUtils.isEmpty(classesToBeBound)) {
290             return createJaxbContextFromClasses();
291         }
292         else {
293             throw new IllegalArgumentException("setting either contextPath or classesToBeBound is required");
294         }
295     }
296 
297     private JAXBContext createJaxbContextFromContextPath() throws JAXBException {
298         if (logger.isInfoEnabled()) {
299             logger.info("Creating JAXBContext with context path [" + getContextPath() + "]");
300         }
301         if (jaxbContextProperties != null) {
302             if (classLoader != null) {
303                 return JAXBContext
304                         .newInstance(getContextPath(), classLoader, jaxbContextProperties);
305             }
306             else {
307                 return JAXBContext
308                         .newInstance(getContextPath(), ClassUtils.getDefaultClassLoader(), jaxbContextProperties);
309             }
310         }
311         else {
312             return classLoader != null ? JAXBContext.newInstance(getContextPath(), classLoader) :
313                     JAXBContext.newInstance(getContextPath());
314         }
315     }
316 
317     private JAXBContext createJaxbContextFromClasses() throws JAXBException {
318         if (logger.isInfoEnabled()) {
319             logger.info("Creating JAXBContext with classes to be bound [" +
320                     StringUtils.arrayToCommaDelimitedString(classesToBeBound) + "]");
321         }
322         if (jaxbContextProperties != null) {
323             return JAXBContext.newInstance(classesToBeBound, jaxbContextProperties);
324         }
325         else {
326             return JAXBContext.newInstance(classesToBeBound);
327         }
328     }
329 
330     /*
331      * Marshaller/Unmarshaller
332      */
333 
334     @Override
335     protected void initJaxbMarshaller(Marshaller marshaller) throws JAXBException {
336         if (schema != null) {
337             marshaller.setSchema(schema);
338         }
339         if (marshallerListener != null) {
340             marshaller.setListener(marshallerListener);
341         }
342         if (adapters != null) {
343             for (XmlAdapter adapter : adapters) {
344                 marshaller.setAdapter(adapter);
345             }
346         }
347     }
348 
349     @Override
350     protected void initJaxbUnmarshaller(Unmarshaller unmarshaller) throws JAXBException {
351         if (schema != null) {
352             unmarshaller.setSchema(schema);
353         }
354         if (unmarshallerListener != null) {
355             unmarshaller.setListener(unmarshallerListener);
356         }
357         if (adapters != null) {
358             for (XmlAdapter adapter : adapters) {
359                 unmarshaller.setAdapter(adapter);
360             }
361         }
362     }
363 
364     /*
365      * Marshalling
366      */
367 
368     public void marshal(Object graph, Result result) throws XmlMappingException {
369         marshal(graph, result, null);
370     }
371 
372     public void marshal(Object graph, Result result, MimeContainer mimeContainer) throws XmlMappingException {
373         try {
374             Marshaller marshaller = createMarshaller();
375             if (mtomEnabled && mimeContainer != null) {
376                 marshaller.setAttachmentMarshaller(new Jaxb2AttachmentMarshaller(mimeContainer));
377             }
378             if (TraxUtils.isStaxResult(result)) {
379                 marshalStaxResult(marshaller, graph, result);
380             }
381             else {
382                 marshaller.marshal(graph, result);
383             }
384         }
385         catch (JAXBException ex) {
386             throw convertJaxbException(ex);
387         }
388     }
389 
390     private void marshalStaxResult(Marshaller jaxbMarshaller, Object graph, Result staxResult) throws JAXBException {
391         XMLStreamWriter streamWriter = TraxUtils.getXMLStreamWriter(staxResult);
392         if (streamWriter != null) {
393             jaxbMarshaller.marshal(graph, streamWriter);
394         }
395         else {
396             XMLEventWriter eventWriter = TraxUtils.getXMLEventWriter(staxResult);
397             if (eventWriter != null) {
398                 jaxbMarshaller.marshal(graph, eventWriter);
399             }
400             else {
401                 throw new IllegalArgumentException("StAX Result contains neither XMLStreamWriter nor XMLEventConsumer");
402             }
403         }
404     }
405 
406     /*
407      * Unmarshalling
408      */
409 
410     public Object unmarshal(Source source) throws XmlMappingException {
411         return unmarshal(source, null);
412     }
413 
414     public Object unmarshal(Source source, MimeContainer mimeContainer) throws XmlMappingException {
415         try {
416             Unmarshaller unmarshaller = createUnmarshaller();
417             if (mtomEnabled && mimeContainer != null) {
418                 unmarshaller.setAttachmentUnmarshaller(new Jaxb2AttachmentUnmarshaller(mimeContainer));
419             }
420             if (TraxUtils.isStaxSource(source)) {
421                 return unmarshalStaxSource(unmarshaller, source);
422             }
423             else {
424                 return unmarshaller.unmarshal(source);
425             }
426         }
427         catch (JAXBException ex) {
428             throw convertJaxbException(ex);
429         }
430     }
431 
432     private Object unmarshalStaxSource(Unmarshaller jaxbUnmarshaller, Source staxSource) throws JAXBException {
433         XMLStreamReader streamReader = TraxUtils.getXMLStreamReader(staxSource);
434         if (streamReader != null) {
435             return jaxbUnmarshaller.unmarshal(streamReader);
436         }
437         else {
438             XMLEventReader eventReader = TraxUtils.getXMLEventReader(staxSource);
439             if (eventReader != null) {
440                 return jaxbUnmarshaller.unmarshal(eventReader);
441             }
442             else {
443                 throw new IllegalArgumentException("StaxSource contains neither XMLStreamReader nor XMLEventReader");
444             }
445         }
446     }
447 
448     /*
449     * Inner classes
450     */
451 
452     private static class Jaxb2AttachmentMarshaller extends AttachmentMarshaller {
453 
454         private final MimeContainer mimeContainer;
455 
456         private Jaxb2AttachmentMarshaller(MimeContainer mimeContainer) {
457             this.mimeContainer = mimeContainer;
458         }
459 
460         @Override
461         public String addMtomAttachment(byte[] data,
462                                         int offset,
463                                         int length,
464                                         String mimeType,
465                                         String elementNamespace,
466                                         String elementLocalName) {
467             ByteArrayDataSource dataSource = new ByteArrayDataSource(mimeType, data, offset, length);
468             return addMtomAttachment(new DataHandler(dataSource), elementNamespace, elementLocalName);
469         }
470 
471         @Override
472         public String addMtomAttachment(DataHandler dataHandler, String elementNamespace, String elementLocalName) {
473             String host = getHost(elementNamespace, dataHandler);
474             String contentId = UUID.randomUUID() + "@" + host;
475             mimeContainer.addAttachment("<" + contentId + ">", dataHandler);
476             try {
477                 contentId = URLEncoder.encode(contentId, "UTF-8");
478             }
479             catch (UnsupportedEncodingException e) {
480                 // ignore
481             }
482             return "cid:" + contentId;
483         }
484 
485         private String getHost(String elementNamespace, DataHandler dataHandler) {
486             try {
487                 URI uri = new URI(elementNamespace);
488                 return uri.getHost();
489             }
490             catch (URISyntaxException e) {
491                 // ignore
492             }
493             return dataHandler.getName();
494         }
495 
496         @Override
497         public String addSwaRefAttachment(DataHandler dataHandler) {
498             String contentId = UUID.randomUUID() + "@" + dataHandler.getName();
499             mimeContainer.addAttachment(contentId, dataHandler);
500             return contentId;
501         }
502 
503         @Override
504         public boolean isXOPPackage() {
505             return mimeContainer.convertToXopPackage();
506         }
507     }
508 
509     private static class Jaxb2AttachmentUnmarshaller extends AttachmentUnmarshaller {
510 
511         private final MimeContainer mimeContainer;
512 
513         private Jaxb2AttachmentUnmarshaller(MimeContainer mimeContainer) {
514             this.mimeContainer = mimeContainer;
515         }
516 
517         @Override
518         public byte[] getAttachmentAsByteArray(String cid) {
519             try {
520                 DataHandler dataHandler = getAttachmentAsDataHandler(cid);
521                 return FileCopyUtils.copyToByteArray(dataHandler.getInputStream());
522             }
523             catch (IOException ex) {
524                 throw new JaxbUnmarshallingFailureException(ex);
525             }
526         }
527 
528         @Override
529         public DataHandler getAttachmentAsDataHandler(String contentId) {
530             if (contentId.startsWith("cid:")) {
531                 contentId = contentId.substring("cid:".length());
532                 try {
533                     contentId = URLDecoder.decode(contentId, "UTF-8");
534                 }
535                 catch (UnsupportedEncodingException e) {
536                     // ignore
537                 }
538                 contentId = '<' + contentId + '>';
539             }
540             return mimeContainer.getAttachment(contentId);
541         }
542 
543         @Override
544         public boolean isXOPPackage() {
545             return mimeContainer.isXopPackage();
546         }
547     }
548 
549     /*
550      * DataSource that wraps around a byte array
551      */
552     private static class ByteArrayDataSource implements DataSource {
553 
554         private byte[] data;
555 
556         private String contentType;
557 
558         private int offset;
559 
560         private int length;
561 
562         private ByteArrayDataSource(String contentType, byte[] data, int offset, int length) {
563             this.contentType = contentType;
564             this.data = data;
565             this.offset = offset;
566             this.length = length;
567         }
568 
569         public InputStream getInputStream() throws IOException {
570             return new ByteArrayInputStream(data, offset, length);
571         }
572 
573         public OutputStream getOutputStream() throws IOException {
574             throw new UnsupportedOperationException();
575         }
576 
577         public String getContentType() {
578             return contentType;
579         }
580 
581         public String getName() {
582             return "ByteArrayDataSource";
583         }
584     }
585 
586 }
587