001/* 002 * Copyright 2002-2014 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 * https://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 017package org.springframework.web.servlet.view.xml; 018 019import java.io.ByteArrayOutputStream; 020import java.util.Map; 021import javax.servlet.http.HttpServletRequest; 022import javax.servlet.http.HttpServletResponse; 023import javax.xml.bind.JAXBElement; 024import javax.xml.transform.stream.StreamResult; 025 026import org.springframework.oxm.Marshaller; 027import org.springframework.util.Assert; 028import org.springframework.validation.BindingResult; 029import org.springframework.web.servlet.View; 030import org.springframework.web.servlet.view.AbstractView; 031 032/** 033 * Spring-MVC {@link View} that allows for response context to be rendered as the result 034 * of marshalling by a {@link Marshaller}. 035 * 036 * <p>The Object to be marshalled is supplied as a parameter in the model and then 037 * {@linkplain #locateToBeMarshalled(Map) detected} during response rendering. Users can 038 * either specify a specific entry in the model via the {@link #setModelKey(String) sourceKey} 039 * property or have Spring locate the Source object. 040 * 041 * @author Arjen Poutsma 042 * @author Juergen Hoeller 043 * @since 3.0 044 */ 045public class MarshallingView extends AbstractView { 046 047 /** 048 * Default content type. Overridable as bean property. 049 */ 050 public static final String DEFAULT_CONTENT_TYPE = "application/xml"; 051 052 053 private Marshaller marshaller; 054 055 private String modelKey; 056 057 058 /** 059 * Construct a new {@code MarshallingView} with no {@link Marshaller} set. 060 * The marshaller must be set after construction by invoking {@link #setMarshaller}. 061 */ 062 public MarshallingView() { 063 setContentType(DEFAULT_CONTENT_TYPE); 064 setExposePathVariables(false); 065 } 066 067 /** 068 * Constructs a new {@code MarshallingView} with the given {@link Marshaller} set. 069 */ 070 public MarshallingView(Marshaller marshaller) { 071 this(); 072 Assert.notNull(marshaller, "Marshaller must not be null"); 073 this.marshaller = marshaller; 074 } 075 076 077 /** 078 * Set the {@link Marshaller} to be used by this view. 079 */ 080 public void setMarshaller(Marshaller marshaller) { 081 this.marshaller = marshaller; 082 } 083 084 /** 085 * Set the name of the model key that represents the object to be marshalled. 086 * If not specified, the model map will be searched for a supported value type. 087 * @see Marshaller#supports(Class) 088 */ 089 public void setModelKey(String modelKey) { 090 this.modelKey = modelKey; 091 } 092 093 @Override 094 protected void initApplicationContext() { 095 Assert.notNull(this.marshaller, "Property 'marshaller' is required"); 096 } 097 098 099 @Override 100 protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, 101 HttpServletResponse response) throws Exception { 102 103 Object toBeMarshalled = locateToBeMarshalled(model); 104 if (toBeMarshalled == null) { 105 throw new IllegalStateException("Unable to locate object to be marshalled in model: " + model); 106 } 107 ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); 108 this.marshaller.marshal(toBeMarshalled, new StreamResult(baos)); 109 110 setResponseContentType(request, response); 111 response.setContentLength(baos.size()); 112 baos.writeTo(response.getOutputStream()); 113 } 114 115 /** 116 * Locate the object to be marshalled. 117 * <p>The default implementation first attempts to look under the configured 118 * {@linkplain #setModelKey(String) model key}, if any, before attempting to 119 * locate an object of {@linkplain Marshaller#supports(Class) supported type}. 120 * @param model the model Map 121 * @return the Object to be marshalled (or {@code null} if none found) 122 * @throws IllegalStateException if the model object specified by the 123 * {@linkplain #setModelKey(String) model key} is not supported by the marshaller 124 * @see #setModelKey(String) 125 */ 126 protected Object locateToBeMarshalled(Map<String, Object> model) throws IllegalStateException { 127 if (this.modelKey != null) { 128 Object value = model.get(this.modelKey); 129 if (value == null) { 130 throw new IllegalStateException("Model contains no object with key [" + this.modelKey + "]"); 131 } 132 if (!isEligibleForMarshalling(this.modelKey, value)) { 133 throw new IllegalStateException("Model object [" + value + "] retrieved via key [" + 134 this.modelKey + "] is not supported by the Marshaller"); 135 } 136 return value; 137 } 138 for (Map.Entry<String, Object> entry : model.entrySet()) { 139 Object value = entry.getValue(); 140 if (value != null && (model.size() == 1 || !(value instanceof BindingResult)) && 141 isEligibleForMarshalling(entry.getKey(), value)) { 142 return value; 143 } 144 } 145 return null; 146 } 147 148 /** 149 * Check whether the given value from the current view's model is eligible 150 * for marshalling through the configured {@link Marshaller}. 151 * <p>The default implementation calls {@link Marshaller#supports(Class)}, 152 * unwrapping a given {@link JAXBElement} first if applicable. 153 * @param modelKey the value's key in the model (never {@code null}) 154 * @param value the value to check (never {@code null}) 155 * @return whether the given value is to be considered as eligible 156 * @see Marshaller#supports(Class) 157 */ 158 protected boolean isEligibleForMarshalling(String modelKey, Object value) { 159 Class<?> classToCheck = value.getClass(); 160 if (value instanceof JAXBElement) { 161 classToCheck = ((JAXBElement) value).getDeclaredType(); 162 } 163 return this.marshaller.supports(classToCheck); 164 } 165 166}