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