From f8057909f9079daf0c108dee92cabcaf2f477223 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Sat, 2 Jun 2007 00:28:13 +0000 Subject: [PATCH] Added useful O/X classes: MarshallingView and MarshallingMessageConverter. --- .../support/MarshallingMessageConverter.java | 268 ++++++++++++++++++ .../oxm/support/MarshallingView.java | 130 +++++++++ .../springframework/oxm/support/package.html | 7 + .../MarshallingMessageConverterTest.java | 150 ++++++++++ .../oxm/support/MarshallingViewTest.java | 104 +++++++ 5 files changed, 659 insertions(+) create mode 100644 sandbox/src/main/java/org/springframework/oxm/support/MarshallingMessageConverter.java create mode 100644 sandbox/src/main/java/org/springframework/oxm/support/MarshallingView.java create mode 100644 sandbox/src/main/java/org/springframework/oxm/support/package.html create mode 100644 sandbox/src/test/java/org/springframework/oxm/support/MarshallingMessageConverterTest.java create mode 100644 sandbox/src/test/java/org/springframework/oxm/support/MarshallingViewTest.java diff --git a/sandbox/src/main/java/org/springframework/oxm/support/MarshallingMessageConverter.java b/sandbox/src/main/java/org/springframework/oxm/support/MarshallingMessageConverter.java new file mode 100644 index 00000000..5a01db20 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/oxm/support/MarshallingMessageConverter.java @@ -0,0 +1,268 @@ +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.oxm.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageEOFException; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.util.Assert; +import org.springframework.xml.transform.StringResult; +import org.springframework.xml.transform.StringSource; + +/** + * Spring JMS {@link MessageConverter} that uses a {@link Marshaller} and {@link Unmarshaller}. Marshals an object to a + * {@link BytesMessage}, or to a {@link TextMessage} if the {@link #setMarshalToTextMessage(boolean) + * marshalToTextMessage} is true. Unmarshals from a {@link TextMessage} or {@link BytesMessage} to an + * object. + * + * @author Arjen Poutsma + */ +public class MarshallingMessageConverter implements MessageConverter, InitializingBean { + + private Marshaller marshaller; + + private Unmarshaller unmarshaller; + + private boolean marshalToTextMessage = false; + + /** + * Constructs a new MarshallingMessageConverter with no {@link Marshaller} set. The marshaller must be + * set after construction by invoking {@link #setMarshaller(Marshaller)}. + */ + public MarshallingMessageConverter() { + } + + /** + * Constructs a new MarshallingMessageConverter with the given {@link Marshaller} set. If the given + * {@link Marshaller} also implements the {@link Unmarshaller} interface, it is used for both marshalling and + * unmarshalling. Otherwise, an exception is thrown. + *

+ * Note that all {@link Marshaller} implementations in Spring-WS also implement the {@link Unmarshaller} interface, + * so that you can safely use this constructor. + * + * @param marshaller object used as marshaller and unmarshaller + * @throws IllegalArgumentException when marshaller does not implement the {@link Unmarshaller} + * interface + */ + public MarshallingMessageConverter(Marshaller marshaller) { + Assert.notNull(marshaller, "marshaller must not be null"); + if (!(marshaller instanceof Unmarshaller)) { + throw new IllegalArgumentException("Marshaller [" + marshaller + "] does not implement the Unmarshaller " + + "interface. Please set an Unmarshaller explicitely by using the " + + "AbstractMarshallingPayloadEndpoint(Marshaller, Unmarshaller) constructor."); + } + else { + this.marshaller = marshaller; + this.unmarshaller = (Unmarshaller) marshaller; + } + } + + /** + * Creates a new MarshallingMessageConverter with the given marshaller and unmarshaller. + * + * @param marshaller the marshaller to use + * @param unmarshaller the unmarshaller to use + */ + public MarshallingMessageConverter(Marshaller marshaller, Unmarshaller unmarshaller) { + Assert.notNull(marshaller, "marshaller must not be null"); + Assert.notNull(unmarshaller, "unmarshaller must not be null"); + this.marshaller = marshaller; + this.unmarshaller = unmarshaller; + } + + /** + * Indicates whether {@link #toMessage(Object,Session)} should marshal to a {@link TextMessage} or a {@link + * BytesMessage}. The default is false, i.e. this converter marshals to a {@link BytesMessage}. + */ + public void setMarshalToTextMessage(boolean marshalToTextMessage) { + this.marshalToTextMessage = marshalToTextMessage; + } + + /** Sets the {@link Marshaller} to be used by this message converter. */ + public void setMarshaller(Marshaller marshaller) { + this.marshaller = marshaller; + } + + /** Sets the {@link Marshaller} to be used by this message converter. */ + public void setUnmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(marshaller, "Property 'marshaller' is required"); + Assert.notNull(unmarshaller, "Property 'unmarshaller' is required"); + } + + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + Result result; + Message message; + if (marshalToTextMessage) { + message = session.createTextMessage(); + result = new StringResult(); + } + else { + message = session.createBytesMessage(); + result = new StreamResult(new BytesMessageOutputStream((BytesMessage) message)); + } + try { + marshaller.marshal(object, result); + if (marshalToTextMessage) { + ((TextMessage) message).setText(result.toString()); + } + return message; + } + catch (MessageConversionException ex) { + handleMessageConversionException(ex); + throw ex; + } + catch (IOException ex) { + throw new MessageConversionException("Could not marshal message [" + message + "]", ex); + } + } + + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + Source source; + if (message instanceof TextMessage) { + source = new StringSource(((TextMessage) message).getText()); + } + else if (message instanceof BytesMessage) { + source = new StreamSource(new BytesMessageInputStream((BytesMessage) message)); + } + else { + throw new MessageConversionException( + "MarshallingMessageConverter only supports TextMessages and BytesMessages"); + } + try { + return unmarshaller.unmarshal(source); + } + catch (MessageConversionException ex) { + handleMessageConversionException(ex); + throw ex; + } + catch (IOException ex) { + throw new MessageConversionException("Could not unmarshal message [" + message + "]", ex); + } + } + + private void handleMessageConversionException(MessageConversionException ex) throws JMSException { + if (ex.getCause() instanceof JMSException) { + throw (JMSException) ex.getCause(); + } + else { + throw ex; + } + } + + /** Input stream that wraps a {@link BytesMessage}. */ + private static class BytesMessageInputStream extends InputStream { + + private BytesMessage message; + + BytesMessageInputStream(BytesMessage message) { + this.message = message; + } + + public int read(byte b[]) throws IOException { + try { + return message.readBytes(b); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not read byte array", ex); + } + } + + public int read(byte b[], int off, int len) throws IOException { + if (off == 0) { + try { + return message.readBytes(b, len); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not read byte array", ex); + } + } + else { + return super.read(b, off, len); + } + } + + public int read() throws IOException { + try { + return message.readByte(); + } + catch (MessageEOFException ex) { + return -1; + } + catch (JMSException ex) { + throw new MessageConversionException("Could not read byte", ex); + } + } + } + + /** Output stream that wraps a {@link BytesMessage}. */ + private static class BytesMessageOutputStream extends OutputStream { + + private BytesMessage message; + + BytesMessageOutputStream(BytesMessage message) { + this.message = message; + } + + public void write(byte b[]) throws IOException { + try { + message.writeBytes(b); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not write byte array", ex); + } + } + + public void write(byte b[], int off, int len) throws IOException { + try { + message.writeBytes(b, off, len); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not write byte array", ex); + } + } + + public void write(int b) throws IOException { + try { + message.writeByte((byte) b); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not write byte", ex); + } + } + } +} + diff --git a/sandbox/src/main/java/org/springframework/oxm/support/MarshallingView.java b/sandbox/src/main/java/org/springframework/oxm/support/MarshallingView.java new file mode 100644 index 00000000..e025cfac --- /dev/null +++ b/sandbox/src/main/java/org/springframework/oxm/support/MarshallingView.java @@ -0,0 +1,130 @@ +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.oxm.support; + +import java.util.Iterator; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.transform.Result; +import javax.xml.transform.stream.StreamResult; + +import org.springframework.beans.BeansException; +import org.springframework.oxm.Marshaller; +import org.springframework.util.Assert; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.AbstractUrlBasedView; + +/** + * Spring-MVC {@link View} that allows for response context to be rendered as the result of marshalling by a {@link + * Marshaller}. + *

+ * The Object to be marshalled is supplied as a parameter in the model and then {@link #locateToBeMarshalled(Map) + * detected} during response rendering. Users can either specify a specific entry in the model via the {@link + * #setModelKey(String) sourceKey} property or have Spring locate the Source object. + * + * @author Arjen Poutsma + */ +public class MarshallingView extends AbstractUrlBasedView { + + /** Default content type. Overridable as bean property. */ + public static final String DEFAULT_CONTENT_TYPE = "text/xml"; + + private Marshaller marshaller; + + private String modelKey; + + /** + * Constructs a new MarshallingView with no {@link Marshaller} set. The marshaller must be set after + * construction by invoking {@link #setMarshaller(Marshaller)}. + */ + public MarshallingView() { + setContentType(DEFAULT_CONTENT_TYPE); + } + + /** Constructs a new MarshallingView with the given {@link Marshaller} set. */ + public MarshallingView(Marshaller marshaller) { + Assert.notNull(marshaller, "'marshaller' must not be null"); + setContentType(DEFAULT_CONTENT_TYPE); + this.marshaller = marshaller; + } + + /** Sets the {@link Marshaller} to be used by this view. */ + public void setMarshaller(Marshaller marshaller) { + this.marshaller = marshaller; + } + + /** + * Set the name of the model key that represents the object to be marshalled. If not specified, the model map will + * be searched for a supported value type. + * + * @see Marshaller#supports(Class) + */ + public void setModelKey(String modelKey) { + this.modelKey = modelKey; + } + + protected void initApplicationContext() throws BeansException { + Assert.notNull(marshaller, "Property 'marshaller' is required"); + } + + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { + Object toBeMarshalled = locateToBeMarshalled(model); + if (toBeMarshalled == null) { + throw new IllegalArgumentException("Unable to locate object to be marshalled in model: " + model); + } + marshaller.marshal(toBeMarshalled, createResult(response)); + } + + /** + * Create the TrAX {@link Result} used to marshal to. + *

+ * The default implementation creates a {@link StreamResult} wrapping the supplied HttpServletResponse's {@link + * HttpServletResponse#getOutputStream() OutputStream}. + * + * @param response current HTTP response + * @return the Result to marshal to + * @throws Exception if the Result cannot be built + */ + protected Result createResult(HttpServletResponse response) throws Exception { + return new StreamResult(response.getOutputStream()); + } + + /** + * Locates the object to be marshalled. The default implementation first attempts to look under the configured + * {@link #setModelKey(String) model key}, if any, before attempting to locate an object of {@link + * Marshaller#supports(Class) supported type}. + * + * @param model the model Map + * @return the Object to be marshalled (or null if none found) + * @throws Exception if an error occured during locating the source + * @see #setModelKey(String) + */ + protected Object locateToBeMarshalled(Map model) { + if (this.modelKey != null) { + return model.get(this.modelKey); + } + for (Iterator iterator = model.values().iterator(); iterator.hasNext();) { + Object o = iterator.next(); + if (this.marshaller.supports(o.getClass())) { + return o; + } + } + return null; + } +} diff --git a/sandbox/src/main/java/org/springframework/oxm/support/package.html b/sandbox/src/main/java/org/springframework/oxm/support/package.html new file mode 100644 index 00000000..ab56f9b1 --- /dev/null +++ b/sandbox/src/main/java/org/springframework/oxm/support/package.html @@ -0,0 +1,7 @@ + + +Provides generic support classes for using Spring's O/X Mapping integration within various scenario's. Includes the +MarshallingView for use withing Spring Web MVC, the MarshallingMessageConverter for use within Spring's JMS support. + + + \ No newline at end of file diff --git a/sandbox/src/test/java/org/springframework/oxm/support/MarshallingMessageConverterTest.java b/sandbox/src/test/java/org/springframework/oxm/support/MarshallingMessageConverterTest.java new file mode 100644 index 00000000..b4b2fd10 --- /dev/null +++ b/sandbox/src/test/java/org/springframework/oxm/support/MarshallingMessageConverterTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.oxm.support; + +import javax.jms.BytesMessage; +import javax.jms.Session; +import javax.jms.TextMessage; + +import junit.framework.TestCase; +import org.easymock.MockControl; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.xml.transform.StringResult; +import org.springframework.xml.transform.StringSource; + +public class MarshallingMessageConverterTest extends TestCase { + + private MarshallingMessageConverter converter; + + private MockControl marshallerControl; + + private Marshaller marshallerMock; + + private MockControl unmarshallerControl; + + private Unmarshaller unmarshallerMock; + + private MockControl sessionControl; + + private Session sessionMock; + + protected void setUp() throws Exception { + marshallerControl = MockControl.createControl(Marshaller.class); + marshallerMock = (Marshaller) marshallerControl.getMock(); + unmarshallerControl = MockControl.createControl(Unmarshaller.class); + unmarshallerMock = (Unmarshaller) unmarshallerControl.getMock(); + converter = new MarshallingMessageConverter(marshallerMock, unmarshallerMock); + sessionControl = MockControl.createControl(Session.class); + sessionMock = (Session) sessionControl.getMock(); + + } + + public void testToBytesMessage() throws Exception { + MockControl bytesMessageControl = MockControl.createControl(BytesMessage.class); + BytesMessage bytesMessageMock = (BytesMessage) bytesMessageControl.getMock(); + Object toBeMarshalled = new Object(); + + sessionControl.expectAndReturn(sessionMock.createBytesMessage(), bytesMessageMock); + marshallerMock.marshal(toBeMarshalled, new StringResult()); + marshallerControl.setMatcher(MockControl.ALWAYS_MATCHER); + + marshallerControl.replay(); + unmarshallerControl.replay(); + sessionControl.replay(); + bytesMessageControl.replay(); + + converter.toMessage(toBeMarshalled, sessionMock); + + marshallerControl.verify(); + unmarshallerControl.verify(); + sessionControl.verify(); + bytesMessageControl.verify(); + } + + public void testFromBytesMessage() throws Exception { + MockControl bytesMessageControl = MockControl.createControl(BytesMessage.class); + BytesMessage bytesMessageMock = (BytesMessage) bytesMessageControl.getMock(); + Object unmarshalled = new Object(); + + unmarshallerMock.unmarshal(new StringSource("")); + unmarshallerControl.setMatcher(MockControl.ALWAYS_MATCHER); + unmarshallerControl.setReturnValue(unmarshalled); + + marshallerControl.replay(); + unmarshallerControl.replay(); + sessionControl.replay(); + bytesMessageControl.replay(); + + Object result = converter.fromMessage(bytesMessageMock); + assertEquals("Invalid result", result, unmarshalled); + + marshallerControl.verify(); + unmarshallerControl.verify(); + sessionControl.verify(); + bytesMessageControl.verify(); + } + + public void testToTextMessage() throws Exception { + converter.setMarshalToTextMessage(true); + MockControl textMessageControl = MockControl.createControl(TextMessage.class); + TextMessage textMessageMock = (TextMessage) textMessageControl.getMock(); + Object toBeMarshalled = new Object(); + + sessionControl.expectAndReturn(sessionMock.createTextMessage(), textMessageMock); + marshallerMock.marshal(toBeMarshalled, new StringResult()); + marshallerControl.setMatcher(MockControl.ALWAYS_MATCHER); + textMessageMock.setText(""); + + marshallerControl.replay(); + unmarshallerControl.replay(); + sessionControl.replay(); + textMessageControl.replay(); + + converter.toMessage(toBeMarshalled, sessionMock); + + marshallerControl.verify(); + unmarshallerControl.verify(); + sessionControl.verify(); + textMessageControl.verify(); + } + + public void testFromTextMessage() throws Exception { + MockControl textMessageControl = MockControl.createControl(TextMessage.class); + TextMessage textMessageMock = (TextMessage) textMessageControl.getMock(); + Object unmarshalled = new Object(); + + unmarshallerMock.unmarshal(new StringSource("")); + unmarshallerControl.setMatcher(MockControl.ALWAYS_MATCHER); + unmarshallerControl.setReturnValue(unmarshalled); + textMessageControl.expectAndReturn(textMessageMock.getText(), ""); + + marshallerControl.replay(); + unmarshallerControl.replay(); + sessionControl.replay(); + textMessageControl.replay(); + + Object result = converter.fromMessage(textMessageMock); + assertEquals("Invalid result", result, unmarshalled); + + marshallerControl.verify(); + unmarshallerControl.verify(); + sessionControl.verify(); + textMessageControl.verify(); + } + +} \ No newline at end of file diff --git a/sandbox/src/test/java/org/springframework/oxm/support/MarshallingViewTest.java b/sandbox/src/test/java/org/springframework/oxm/support/MarshallingViewTest.java new file mode 100644 index 00000000..757f2b38 --- /dev/null +++ b/sandbox/src/test/java/org/springframework/oxm/support/MarshallingViewTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.oxm.support; + +import java.util.HashMap; +import java.util.Map; +import javax.xml.transform.stream.StreamResult; + +import junit.framework.TestCase; +import org.easymock.MockControl; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.oxm.Marshaller; + +public class MarshallingViewTest extends TestCase { + + private MarshallingView view; + + private MockControl control; + + private Marshaller marshallerMock; + + protected void setUp() throws Exception { + control = MockControl.createControl(Marshaller.class); + marshallerMock = (Marshaller) control.getMock(); + view = new MarshallingView(marshallerMock); + } + + public void testGetContentType() { + assertEquals("Invalid content type", "text/xml", view.getContentType()); + } + + public void testRenderModelKey() throws Exception { + Object toBeMarshalled = new Object(); + String modelKey = "key"; + view.setModelKey(modelKey); + Map model = new HashMap(); + model.put(modelKey, toBeMarshalled); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + marshallerMock.marshal(toBeMarshalled, new StreamResult(response.getOutputStream())); + control.setMatcher(MockControl.ALWAYS_MATCHER); + + control.replay(); + view.render(model, request, response); + control.verify(); + } + + public void testRenderNoModelKey() throws Exception { + Object toBeMarshalled = new Object(); + String modelKey = "key"; + Map model = new HashMap(); + model.put(modelKey, toBeMarshalled); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + control.expectAndReturn(marshallerMock.supports(Object.class), true); + marshallerMock.marshal(toBeMarshalled, new StreamResult(response.getOutputStream())); + control.setMatcher(MockControl.ALWAYS_MATCHER); + + control.replay(); + view.render(model, request, response); + control.verify(); + } + + public void testRenderUnsupportedModel() throws Exception { + Object toBeMarshalled = new Object(); + String modelKey = "key"; + Map model = new HashMap(); + model.put(modelKey, toBeMarshalled); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + control.expectAndReturn(marshallerMock.supports(Object.class), false); + + control.replay(); + try { + view.render(model, request, response); + fail("IllegalArgumentException expected"); + } + catch (IllegalArgumentException ex) { + // expected + } + control.verify(); + } +} \ No newline at end of file