From b223e6efecf0c2e4ca1b3f1631ef82e08d1a8710 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Feb 2014 15:12:35 +0100 Subject: [PATCH] MarshallingView should not close response OutputStream after copying to it MarshallingView also explicitly skips BindingResult when searching for a model object now, implementing common custom subclass behavior out-of-the-box. Issue: SPR-11411 Issue: SPR-11417 --- .../web/servlet/view/xml/MarshallingView.java | 86 ++++++++++--------- .../view/xml/MarshallingViewTests.java | 31 ++++++- 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java index c08daa0a851..7a0c5ab6225 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -18,28 +18,29 @@ package org.springframework.web.servlet.view.xml; import java.io.ByteArrayOutputStream; import java.util.Map; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.transform.stream.StreamResult; -import org.springframework.beans.BeansException; import org.springframework.oxm.Marshaller; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; +import org.springframework.validation.BindingResult; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.AbstractView; /** - * Spring-MVC {@link View} that allows for response context to be rendered as the result of marshalling by a {@link - * Marshaller}. + * 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 {@linkplain - * #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. + *

The Object to be marshalled is supplied as a parameter in the model and then + * {@linkplain #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 + * @author Juergen Hoeller * @since 3.0 */ public class MarshallingView extends AbstractView { @@ -49,13 +50,15 @@ public class MarshallingView extends AbstractView { */ public static final String DEFAULT_CONTENT_TYPE = "application/xml"; + private Marshaller marshaller; private String modelKey; + /** - * Constructs a new {@code MarshallingView} with no {@link Marshaller} set. The marshaller must be set after - * construction by invoking {@link #setMarshaller(Marshaller)}. + * Constructs a new {@code MarshallingView} with no {@link Marshaller} set. + * The marshaller must be set after construction by invoking {@link #setMarshaller}. */ public MarshallingView() { setContentType(DEFAULT_CONTENT_TYPE); @@ -66,24 +69,22 @@ public class MarshallingView extends AbstractView { * Constructs a new {@code MarshallingView} with the given {@link Marshaller} set. */ public MarshallingView(Marshaller marshaller) { - Assert.notNull(marshaller, "'marshaller' must not be null"); - setContentType(DEFAULT_CONTENT_TYPE); + this(); + Assert.notNull(marshaller, "Marshaller must not be null"); this.marshaller = marshaller; - setExposePathVariables(false); } + /** * Sets the {@link Marshaller} to be used by this view. */ public void setMarshaller(Marshaller marshaller) { - Assert.notNull(marshaller, "'marshaller' must not be null"); 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. - * + * 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) { @@ -91,55 +92,58 @@ public class MarshallingView extends AbstractView { } @Override - protected void initApplicationContext() throws BeansException { - Assert.notNull(marshaller, "Property 'marshaller' is required"); + protected void initApplicationContext() { + Assert.notNull(this.marshaller, "Property 'marshaller' is required"); } + @Override - protected void renderMergedOutputModel(Map model, - HttpServletRequest request, - HttpServletResponse response) throws Exception { + protected void renderMergedOutputModel(Map model, HttpServletRequest request, + HttpServletResponse response) throws Exception { + Object toBeMarshalled = locateToBeMarshalled(model); if (toBeMarshalled == null) { throw new ServletException("Unable to locate object to be marshalled in model: " + model); } ByteArrayOutputStream bos = new ByteArrayOutputStream(2048); - marshaller.marshal(toBeMarshalled, new StreamResult(bos)); + this.marshaller.marshal(toBeMarshalled, new StreamResult(bos)); setResponseContentType(request, response); response.setContentLength(bos.size()); - FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream()); + StreamUtils.copy(bos.toByteArray(), response.getOutputStream()); } /** - * Locates the object to be marshalled. The default implementation first attempts to look under the configured - * {@linkplain #setModelKey(String) model key}, if any, before attempting to locate an object of {@linkplain - * Marshaller#supports(Class) supported type}. - * + * Locate the object to be marshalled. + *

The default implementation first attempts to look under the configured + * {@linkplain #setModelKey(String) model key}, if any, before attempting to + * locate an object of {@linkplain Marshaller#supports(Class) supported type}. * @param model the model Map * @return the Object to be marshalled (or {@code null} if none found) - * @throws ServletException if the model object specified by the {@linkplain #setModelKey(String) model key} is not - * supported by the marshaller + * @throws ServletException if the model object specified by the + * {@linkplain #setModelKey(String) model key} is not supported by the marshaller * @see #setModelKey(String) */ protected Object locateToBeMarshalled(Map model) throws ServletException { if (this.modelKey != null) { - Object o = model.get(this.modelKey); - if (o == null) { - throw new ServletException("Model contains no object with key [" + modelKey + "]"); + Object obj = model.get(this.modelKey); + if (obj == null) { + throw new ServletException("Model contains no object with key [" + this.modelKey + "]"); } - if (!this.marshaller.supports(o.getClass())) { - throw new ServletException("Model object [" + o + "] retrieved via key [" + modelKey + - "] is not supported by the Marshaller"); + if (!this.marshaller.supports(obj.getClass())) { + throw new ServletException("Model object [" + obj + "] retrieved via key [" + + this.modelKey + "] is not supported by the Marshaller"); } - return o; + return obj; } - for (Object o : model.values()) { - if (o != null && this.marshaller.supports(o.getClass())) { - return o; + for (Object obj : model.values()) { + if (obj != null && (model.size() == 1 || !(obj instanceof BindingResult)) && + this.marshaller.supports(obj.getClass())) { + return obj; } } return null; } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MarshallingViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MarshallingViewTests.java index 6b767c7f78e..1c1d3a99788 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MarshallingViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MarshallingViewTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -17,16 +17,19 @@ package org.springframework.web.servlet.view.xml; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; - import javax.servlet.ServletException; import javax.xml.transform.stream.StreamResult; import org.junit.Before; import org.junit.Test; + import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.oxm.Marshaller; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -36,9 +39,10 @@ import static org.mockito.BDDMockito.*; */ public class MarshallingViewTests { + private Marshaller marshallerMock; + private MarshallingView view; - private Marshaller marshallerMock; @Before public void createView() throws Exception { @@ -46,6 +50,7 @@ public class MarshallingViewTests { view = new MarshallingView(marshallerMock); } + @Test public void getContentType() { assertEquals("Invalid content type", "application/xml", view.getContentType()); @@ -160,6 +165,26 @@ public class MarshallingViewTests { verify(marshallerMock).marshal(eq(toBeMarshalled), isA(StreamResult.class)); } + @Test + public void renderNoModelKeyAndBindingResultFirst() throws Exception { + Object toBeMarshalled = new Object(); + String modelKey = "key"; + Map model = new LinkedHashMap(); + model.put(BindingResult.MODEL_KEY_PREFIX + modelKey, new BeanPropertyBindingResult(toBeMarshalled, modelKey)); + model.put(modelKey, toBeMarshalled); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + given(marshallerMock.supports(BeanPropertyBindingResult.class)).willReturn(true); + given(marshallerMock.supports(Object.class)).willReturn(true); + + view.render(model, request, response); + assertEquals("Invalid content type", "application/xml", response.getContentType()); + assertEquals("Invalid content length", 0, response.getContentLength()); + verify(marshallerMock).marshal(eq(toBeMarshalled), isA(StreamResult.class)); + } + @Test public void testRenderUnsupportedModel() throws Exception { Object toBeMarshalled = new Object();