Fix using system default charset in view rendering

Prior to this commit, FreeMarkerView used the system default charset to
render. This commit switches this by defaulting to UTF-8, if no charset
is specified in the content type.

 - Add contentType parameter to AbstractView.renderInternal, used to
 determine the charset contained therein
 - Adds a defaultCharset property to AbstractView and
 ViewResolverSupport.
This commit is contained in:
Arjen Poutsma 2016-09-05 12:31:00 +02:00
parent a746c3c54e
commit 5f941c1dd1
5 changed files with 68 additions and 8 deletions

View File

@ -16,6 +16,8 @@
package org.springframework.web.reactive.result.view; package org.springframework.web.reactive.result.view;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -45,6 +47,8 @@ public abstract class AbstractView implements View, ApplicationContextAware {
private final List<MediaType> mediaTypes = new ArrayList<>(4); private final List<MediaType> mediaTypes = new ArrayList<>(4);
private Charset defaultCharset = StandardCharsets.UTF_8;
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
@ -73,6 +77,24 @@ public abstract class AbstractView implements View, ApplicationContextAware {
return this.mediaTypes; return this.mediaTypes;
} }
/**
* Set the default charset for this view, used when the
* {@linkplain #setSupportedMediaTypes(List) content type} does not contain one.
* Default is {@linkplain StandardCharsets#UTF_8 UTF 8}.
*/
public void setDefaultCharset(Charset defaultCharset) {
Assert.notNull(defaultCharset, "'defaultCharset' must not be null");
this.defaultCharset = defaultCharset;
}
/**
* Return the default charset, used when the
* {@linkplain #setSupportedMediaTypes(List) content type} does not contain one.
*/
public Charset getDefaultCharset() {
return this.defaultCharset;
}
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) { public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
@ -104,7 +126,7 @@ public abstract class AbstractView implements View, ApplicationContextAware {
} }
Map<String, Object> mergedModel = getModelAttributes(model, exchange); Map<String, Object> mergedModel = getModelAttributes(model, exchange);
return renderInternal(mergedModel, exchange); return renderInternal(mergedModel, contentType, exchange);
} }
/** /**
@ -127,11 +149,12 @@ public abstract class AbstractView implements View, ApplicationContextAware {
* Subclasses must implement this method to actually render the view. * Subclasses must implement this method to actually render the view.
* @param renderAttributes combined output Map (never {@code null}), * @param renderAttributes combined output Map (never {@code null}),
* with dynamic values taking precedence over static attributes * with dynamic values taking precedence over static attributes
* @param exchange current exchange * @param contentType the content type selected to render with which should
* @return {@code Mono} to represent when and if rendering succeeds * match one of the {@link #getSupportedMediaTypes() supported media types}.
*@param exchange current exchange @return {@code Mono} to represent when and if rendering succeeds
*/ */
protected abstract Mono<Void> renderInternal(Map<String, Object> renderAttributes, protected abstract Mono<Void> renderInternal(Map<String, Object> renderAttributes,
ServerWebExchange exchange); MediaType contentType, ServerWebExchange exchange);
@Override @Override

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.reactive.result.view; package org.springframework.web.reactive.result.view;
import java.util.Locale; import java.util.Locale;
@ -194,6 +195,7 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes
protected AbstractUrlBasedView createUrlBasedView(String viewName) { protected AbstractUrlBasedView createUrlBasedView(String viewName) {
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
view.setSupportedMediaTypes(getSupportedMediaTypes()); view.setSupportedMediaTypes(getSupportedMediaTypes());
view.setDefaultCharset(getDefaultCharset());
view.setUrl(getPrefix() + viewName + getSuffix()); view.setUrl(getPrefix() + viewName + getSuffix());
return view; return view;
} }

View File

@ -16,6 +16,8 @@
package org.springframework.web.reactive.result.view; package org.springframework.web.reactive.result.view;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -38,6 +40,8 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or
private List<MediaType> mediaTypes = new ArrayList<>(4); private List<MediaType> mediaTypes = new ArrayList<>(4);
private Charset defaultCharset = StandardCharsets.UTF_8;
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
private int order = Integer.MAX_VALUE; private int order = Integer.MAX_VALUE;
@ -67,6 +71,25 @@ public abstract class ViewResolverSupport implements ApplicationContextAware, Or
return this.mediaTypes; return this.mediaTypes;
} }
/**
* Set the default charset for this view, used when the
* {@linkplain #setSupportedMediaTypes(List) content type} does not contain one.
* Default is {@linkplain StandardCharsets#UTF_8 UTF 8}.
*/
public void setDefaultCharset(Charset defaultCharset) {
Assert.notNull(defaultCharset, "'defaultCharset' must not be null");
this.defaultCharset = defaultCharset;
}
/**
* Return the default charset, used when the
* {@linkplain #setSupportedMediaTypes(List) content type} does not contain one.
*/
public Charset getDefaultCharset() {
return this.defaultCharset;
}
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) { public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;

View File

@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.reactive.result.view.freemarker; package org.springframework.web.reactive.result.view.freemarker;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import freemarker.core.ParseException; import freemarker.core.ParseException;
import freemarker.template.Configuration; import freemarker.template.Configuration;
@ -37,6 +40,7 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContextException; import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -158,7 +162,8 @@ public class FreeMarkerView extends AbstractUrlBasedView {
} }
@Override @Override
protected Mono<Void> renderInternal(Map<String, Object> renderAttributes, ServerWebExchange exchange) { protected Mono<Void> renderInternal(Map<String, Object> renderAttributes, MediaType contentType,
ServerWebExchange exchange) {
// Expose all standard FreeMarker hash models. // Expose all standard FreeMarker hash models.
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange); SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@ -167,8 +172,8 @@ public class FreeMarkerView extends AbstractUrlBasedView {
Locale locale = Locale.getDefault(); // TODO Locale locale = Locale.getDefault(); // TODO
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer(); DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
try { try {
// TODO: pass charset Charset charset = getCharset(contentType).orElse(getDefaultCharset());
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream()); Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset);
getTemplate(locale).process(freeMarkerModel, writer); getTemplate(locale).process(freeMarkerModel, writer);
} }
catch (IOException ex) { catch (IOException ex) {
@ -181,6 +186,10 @@ public class FreeMarkerView extends AbstractUrlBasedView {
return exchange.getResponse().writeWith(Flux.just(dataBuffer)); return exchange.getResponse().writeWith(Flux.just(dataBuffer));
} }
private static Optional<Charset> getCharset(MediaType mediaType) {
return mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty();
}
/** /**
* Build a FreeMarker template model for the given model Map. * Build a FreeMarker template model for the given model Map.
* <p>The default implementation builds a {@link SimpleHash}. * <p>The default implementation builds a {@link SimpleHash}.

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.reactive.result.view; package org.springframework.web.reactive.result.view;
import java.util.Locale; import java.util.Locale;
@ -22,6 +23,7 @@ import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -61,7 +63,8 @@ public class UrlBasedViewResolverTests {
} }
@Override @Override
protected Mono<Void> renderInternal(Map<String, Object> attributes, ServerWebExchange exchange) { protected Mono<Void> renderInternal(Map<String, Object> attributes, MediaType contentType,
ServerWebExchange exchange) {
return Mono.empty(); return Mono.empty();
} }
} }