001: /*
002: * Copyright 2006 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: * http://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:
017: package org.springframework.oxm.jaxb;
018:
019: import java.io.ByteArrayInputStream;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.OutputStream;
023: import java.io.UnsupportedEncodingException;
024: import java.net.URI;
025: import java.net.URISyntaxException;
026: import java.net.URLDecoder;
027: import java.net.URLEncoder;
028: import java.util.Map;
029: import java.util.UUID;
030: import javax.activation.DataHandler;
031: import javax.activation.DataSource;
032: import javax.xml.XMLConstants;
033: import javax.xml.bind.JAXBContext;
034: import javax.xml.bind.JAXBElement;
035: import javax.xml.bind.JAXBException;
036: import javax.xml.bind.Marshaller;
037: import javax.xml.bind.Unmarshaller;
038: import javax.xml.bind.annotation.XmlRootElement;
039: import javax.xml.bind.annotation.adapters.XmlAdapter;
040: import javax.xml.bind.attachment.AttachmentMarshaller;
041: import javax.xml.bind.attachment.AttachmentUnmarshaller;
042: import javax.xml.transform.Result;
043: import javax.xml.transform.Source;
044: import javax.xml.validation.Schema;
045:
046: import org.springframework.core.io.Resource;
047: import org.springframework.oxm.XmlMappingException;
048: import org.springframework.oxm.mime.MimeContainer;
049: import org.springframework.oxm.mime.MimeMarshaller;
050: import org.springframework.oxm.mime.MimeUnmarshaller;
051: import org.springframework.util.ClassUtils;
052: import org.springframework.util.FileCopyUtils;
053: import org.springframework.util.ObjectUtils;
054: import org.springframework.util.StringUtils;
055: import org.springframework.xml.transform.StaxResult;
056: import org.springframework.xml.transform.StaxSource;
057: import org.springframework.xml.validation.SchemaLoaderUtils;
058:
059: /**
060: * Implementation of the <code>Marshaller</code> interface for JAXB 2.0.
061: * <p/>
062: * The typical usage will be to set either the <code>contextPath</code> or the <code>classesToBeBound</code> property on
063: * this bean, possibly customize the marshaller and unmarshaller by setting properties, schemas, adapters, and
064: * listeners, and to refer to it.
065: *
066: * @author Arjen Poutsma
067: * @see #setContextPath(String)
068: * @see #setClassesToBeBound(Class[])
069: * @see #setJaxbContextProperties(java.util.Map)
070: * @see #setMarshallerProperties(java.util.Map)
071: * @see #setUnmarshallerProperties(java.util.Map)
072: * @see #setSchema(org.springframework.core.io.Resource)
073: * @see #setSchemas(org.springframework.core.io.Resource[])
074: * @see #setMarshallerListener(javax.xml.bind.Marshaller.Listener)
075: * @see #setUnmarshallerListener(javax.xml.bind.Unmarshaller.Listener)
076: * @see #setAdapters(javax.xml.bind.annotation.adapters.XmlAdapter[])
077: * @since 1.0.0
078: */
079: public class Jaxb2Marshaller extends AbstractJaxbMarshaller implements
080: MimeMarshaller, MimeUnmarshaller {
081:
082: private Resource[] schemaResources;
083:
084: private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI;
085:
086: private Marshaller.Listener marshallerListener;
087:
088: private Unmarshaller.Listener unmarshallerListener;
089:
090: private XmlAdapter[] adapters;
091:
092: private Schema schema;
093:
094: private Class[] classesToBeBound;
095:
096: private Map<java.lang.String, ?> jaxbContextProperties;
097:
098: private boolean mtomEnabled = false;
099:
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(
123: Map<String, ?> jaxbContextProperties) {
124: this .jaxbContextProperties = jaxbContextProperties;
125: }
126:
127: /** Sets the <code>Marshaller.Listener</code> to be registered with the JAXB <code>Marshaller</code>. */
128: public void setMarshallerListener(
129: 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: /** Sets the schema resource to use for validation. */
152: public void setSchema(Resource schemaResource) {
153: schemaResources = new Resource[] { schemaResource };
154: }
155:
156: /** Sets the schema resources to use for validation. */
157: public void setSchemas(Resource[] schemaResources) {
158: this .schemaResources = schemaResources;
159: }
160:
161: /** Sets the <code>Unmarshaller.Listener</code> to be registered with the JAXB <code>Unmarshaller</code>. */
162: public void setUnmarshallerListener(
163: Unmarshaller.Listener unmarshallerListener) {
164: this .unmarshallerListener = unmarshallerListener;
165: }
166:
167: public boolean supports(Class clazz) {
168: return clazz.getAnnotation(XmlRootElement.class) != null
169: || JAXBElement.class.isAssignableFrom(clazz);
170: }
171:
172: /*
173: * JAXBContext
174: */
175:
176: protected JAXBContext createJaxbContext() throws Exception {
177: if (JaxbUtils.getJaxbVersion() < JaxbUtils.JAXB_2) {
178: throw new IllegalStateException(
179: "Cannot use Jaxb2Marshaller in combination with JAXB 1.0. Use Jaxb1Marshaller instead.");
180: }
181: if (StringUtils.hasLength(getContextPath())
182: && !ObjectUtils.isEmpty(classesToBeBound)) {
183: throw new IllegalArgumentException(
184: "specify either contextPath or classesToBeBound property; not both");
185: }
186: if (!ObjectUtils.isEmpty(schemaResources)) {
187: if (logger.isDebugEnabled()) {
188: logger
189: .debug("Setting validation schema to "
190: + StringUtils
191: .arrayToCommaDelimitedString(schemaResources));
192: }
193: schema = SchemaLoaderUtils.loadSchema(schemaResources,
194: schemaLanguage);
195: }
196: if (StringUtils.hasLength(getContextPath())) {
197: return createJaxbContextFromContextPath();
198: } else if (!ObjectUtils.isEmpty(classesToBeBound)) {
199: return createJaxbContextFromClasses();
200: } else {
201: throw new IllegalArgumentException(
202: "setting either contextPath or classesToBeBound is required");
203: }
204: }
205:
206: private JAXBContext createJaxbContextFromContextPath()
207: throws JAXBException {
208: if (logger.isInfoEnabled()) {
209: logger.info("Creating JAXBContext with context path ["
210: + getContextPath() + "]");
211: }
212: if (jaxbContextProperties != null) {
213: return JAXBContext.newInstance(getContextPath(), ClassUtils
214: .getDefaultClassLoader(), jaxbContextProperties);
215: } else {
216: return JAXBContext.newInstance(getContextPath());
217: }
218: }
219:
220: private JAXBContext createJaxbContextFromClasses()
221: throws JAXBException {
222: if (logger.isInfoEnabled()) {
223: logger
224: .info("Creating JAXBContext with classes to be bound ["
225: + StringUtils
226: .arrayToCommaDelimitedString(classesToBeBound)
227: + "]");
228: }
229: if (jaxbContextProperties != null) {
230: return JAXBContext.newInstance(classesToBeBound,
231: jaxbContextProperties);
232: } else {
233: return JAXBContext.newInstance(classesToBeBound);
234: }
235: }
236:
237: /*
238: * Marshaller/Unmarshaller
239: */
240:
241: protected void initJaxbMarshaller(Marshaller marshaller)
242: throws JAXBException {
243: if (schema != null) {
244: marshaller.setSchema(schema);
245: }
246: if (marshallerListener != null) {
247: marshaller.setListener(marshallerListener);
248: }
249: if (adapters != null) {
250: for (int i = 0; i < adapters.length; i++) {
251: marshaller.setAdapter(adapters[i]);
252: }
253: }
254: }
255:
256: protected void initJaxbUnmarshaller(Unmarshaller unmarshaller)
257: throws JAXBException {
258: if (schema != null) {
259: unmarshaller.setSchema(schema);
260: }
261: if (unmarshallerListener != null) {
262: unmarshaller.setListener(unmarshallerListener);
263: }
264: if (adapters != null) {
265: for (int i = 0; i < adapters.length; i++) {
266: unmarshaller.setAdapter(adapters[i]);
267: }
268: }
269: }
270:
271: /*
272: * Marshalling
273: */
274:
275: public void marshal(Object graph, Result result)
276: throws XmlMappingException {
277: marshal(graph, result, null);
278: }
279:
280: public void marshal(Object graph, Result result,
281: MimeContainer mimeContainer) throws XmlMappingException {
282: try {
283: Marshaller marshaller = createMarshaller();
284: if (mtomEnabled && mimeContainer != null) {
285: marshaller
286: .setAttachmentMarshaller(new Jaxb2AttachmentMarshaller(
287: mimeContainer));
288: }
289: if (result instanceof StaxResult) {
290: marshalStaxResult(marshaller, graph,
291: (StaxResult) result);
292: } else {
293: marshaller.marshal(graph, result);
294: }
295: } catch (JAXBException ex) {
296: throw convertJaxbException(ex);
297: }
298: }
299:
300: private void marshalStaxResult(Marshaller jaxbMarshaller,
301: Object graph, StaxResult staxResult) throws JAXBException {
302: if (staxResult.getXMLStreamWriter() != null) {
303: jaxbMarshaller.marshal(graph, staxResult
304: .getXMLStreamWriter());
305: } else if (staxResult.getXMLEventWriter() != null) {
306: jaxbMarshaller.marshal(graph, staxResult
307: .getXMLEventWriter());
308: } else {
309: throw new IllegalArgumentException(
310: "StaxResult contains neither XMLStreamWriter nor XMLEventConsumer");
311: }
312: }
313:
314: /*
315: * Unmarshalling
316: */
317:
318: public Object unmarshal(Source source) throws XmlMappingException {
319: return unmarshal(source, null);
320: }
321:
322: public Object unmarshal(Source source, MimeContainer mimeContainer)
323: throws XmlMappingException {
324: try {
325: Unmarshaller unmarshaller = createUnmarshaller();
326: if (mtomEnabled && mimeContainer != null) {
327: unmarshaller
328: .setAttachmentUnmarshaller(new Jaxb2AttachmentUnmarshaller(
329: mimeContainer));
330: }
331: if (source instanceof StaxSource) {
332: return unmarshalStaxSource(unmarshaller,
333: (StaxSource) source);
334: } else {
335: return unmarshaller.unmarshal(source);
336: }
337: } catch (JAXBException ex) {
338: throw convertJaxbException(ex);
339: }
340: }
341:
342: private Object unmarshalStaxSource(Unmarshaller jaxbUnmarshaller,
343: StaxSource staxSource) throws JAXBException {
344: if (staxSource.getXMLStreamReader() != null) {
345: return jaxbUnmarshaller.unmarshal(staxSource
346: .getXMLStreamReader());
347: } else if (staxSource.getXMLEventReader() != null) {
348: return jaxbUnmarshaller.unmarshal(staxSource
349: .getXMLEventReader());
350: } else {
351: throw new IllegalArgumentException(
352: "StaxSource contains neither XMLStreamReader nor XMLEventReader");
353: }
354: }
355:
356: /*
357: * Inner classes
358: */
359:
360: private static class Jaxb2AttachmentMarshaller extends
361: AttachmentMarshaller {
362:
363: private final MimeContainer mimeContainer;
364:
365: public Jaxb2AttachmentMarshaller(MimeContainer mimeContainer) {
366: this .mimeContainer = mimeContainer;
367: }
368:
369: public String addMtomAttachment(byte[] data, int offset,
370: int length, String mimeType, String elementNamespace,
371: String elementLocalName) {
372: ByteArrayDataSource dataSource = new ByteArrayDataSource(
373: mimeType, data, offset, length);
374: return addMtomAttachment(new DataHandler(dataSource),
375: elementNamespace, elementLocalName);
376: }
377:
378: public String addMtomAttachment(DataHandler dataHandler,
379: String elementNamespace, String elementLocalName) {
380: String host = getHost(elementNamespace, dataHandler);
381: String contentId = UUID.randomUUID() + "@" + host;
382: mimeContainer.addAttachment("<" + contentId + ">",
383: dataHandler);
384: try {
385: contentId = URLEncoder.encode(contentId, "UTF-8");
386: } catch (UnsupportedEncodingException e) {
387: // ignore
388: }
389: return "cid:" + contentId;
390: }
391:
392: private String getHost(String elementNamespace,
393: DataHandler dataHandler) {
394: try {
395: URI uri = new URI(elementNamespace);
396: return uri.getHost();
397: } catch (URISyntaxException e) {
398: // ignore
399: }
400: return dataHandler.getName();
401: }
402:
403: public String addSwaRefAttachment(DataHandler dataHandler) {
404: String contentId = UUID.randomUUID() + "@"
405: + dataHandler.getName();
406: mimeContainer.addAttachment(contentId, dataHandler);
407: return contentId;
408: }
409:
410: @Override
411: public boolean isXOPPackage() {
412: return mimeContainer.convertToXopPackage();
413: }
414: }
415:
416: private static class Jaxb2AttachmentUnmarshaller extends
417: AttachmentUnmarshaller {
418:
419: private final MimeContainer mimeContainer;
420:
421: public Jaxb2AttachmentUnmarshaller(MimeContainer mimeContainer) {
422: this .mimeContainer = mimeContainer;
423: }
424:
425: public byte[] getAttachmentAsByteArray(String cid) {
426: try {
427: DataHandler dataHandler = getAttachmentAsDataHandler(cid);
428: return FileCopyUtils.copyToByteArray(dataHandler
429: .getInputStream());
430: } catch (IOException ex) {
431: throw new JaxbUnmarshallingFailureException(ex);
432: }
433: }
434:
435: public DataHandler getAttachmentAsDataHandler(String contentId) {
436: if (contentId.startsWith("cid:")) {
437: contentId = contentId.substring("cid:".length());
438: try {
439: contentId = URLDecoder.decode(contentId, "UTF-8");
440: } catch (UnsupportedEncodingException e) {
441: // ignore
442: }
443: contentId = '<' + contentId + '>';
444: }
445: return mimeContainer.getAttachment(contentId);
446: }
447:
448: @Override
449: public boolean isXOPPackage() {
450: return mimeContainer.isXopPackage();
451: }
452: }
453:
454: /*
455: * DataSource that wraps around a byte array
456: */
457: private static class ByteArrayDataSource implements DataSource {
458:
459: private byte[] data;
460:
461: private String contentType;
462:
463: private int offset;
464:
465: private int length;
466:
467: public ByteArrayDataSource(String contentType, byte[] data,
468: int offset, int length) {
469: this .contentType = contentType;
470: this .data = data;
471: this .offset = offset;
472: this .length = length;
473: }
474:
475: public InputStream getInputStream() throws IOException {
476: return new ByteArrayInputStream(data, offset, length);
477: }
478:
479: public OutputStream getOutputStream() throws IOException {
480: throw new UnsupportedOperationException();
481: }
482:
483: public String getContentType() {
484: return contentType;
485: }
486:
487: public String getName() {
488: return "ByteArrayDataSource";
489: }
490: }
491:
492: }
|