1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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
128
129
130 public void setAdapters(XmlAdapter[] adapters) {
131 this.adapters = adapters;
132 }
133
134
135
136
137
138
139
140 public void setClassesToBeBound(Class[] classesToBeBound) {
141 this.classesToBeBound = classesToBeBound;
142 }
143
144
145
146
147
148 public void setJaxbContextProperties(Map<String, ?> jaxbContextProperties) {
149 this.jaxbContextProperties = jaxbContextProperties;
150 }
151
152
153 public void setMarshallerListener(Marshaller.Listener marshallerListener) {
154 this.marshallerListener = marshallerListener;
155 }
156
157
158
159
160
161 public void setMtomEnabled(boolean mtomEnabled) {
162 this.mtomEnabled = mtomEnabled;
163 }
164
165
166
167
168
169
170
171 public void setSchemaLanguage(String schemaLanguage) {
172 this.schemaLanguage = schemaLanguage;
173 }
174
175
176 public void setSchema(Resource schemaResource) {
177 schemaResources = new Resource[]{schemaResource};
178 }
179
180
181 public void setSchemas(Resource[] schemaResources) {
182 this.schemaResources = schemaResources;
183 }
184
185
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
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
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
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
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
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
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
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
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
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