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