FreeMarkerView.java 8.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*
 * Copyright 2002-2016 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.
 */
16

17
package org.springframework.web.reactive.result.view.freemarker;
18 19 20 21 22

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
23
import java.nio.charset.Charset;
24 25
import java.util.Locale;
import java.util.Map;
26
import java.util.Optional;
27 28 29 30 31 32 33 34 35

import freemarker.core.ParseException;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.Version;
import reactor.core.publisher.Flux;
R
Rossen Stoyanchev 已提交
36
import reactor.core.publisher.Mono;
37 38 39 40 41 42

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.buffer.DataBuffer;
43
import org.springframework.http.MediaType;
44
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
import org.springframework.web.server.ServerWebExchange;

/**
 * A {@code View} implementation that uses the FreeMarker template engine.
 *
 * <p>Depends on a single {@link FreeMarkerConfig} object such as
 * {@link FreeMarkerConfigurer} being accessible in the application context.
 * Alternatively set the FreeMarker configuration can be set directly on this
 * class via {@link #setConfiguration}.
 *
 * <p>The {@link #setUrl(String) url} property is the location of the FreeMarker
 * template relative to the FreeMarkerConfigurer's
 * {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}.
 *
 * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
 *
 * @author Rossen Stoyanchev
62
 * @since 5.0
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
 */
public class FreeMarkerView extends AbstractUrlBasedView {

	private Configuration configuration;

	private String encoding;


	/**
	 * Set the FreeMarker Configuration to be used by this view.
	 * <p>Typically this property is not set directly. Instead a single
	 * {@link FreeMarkerConfig} is expected in the Spring application context
	 * which is used to obtain the FreeMarker configuration.
	 */
	public void setConfiguration(Configuration configuration) {
		this.configuration = configuration;
	}

	/**
	 * Return the FreeMarker configuration used by this view.
	 */
	protected Configuration getConfiguration() {
		return this.configuration;
	}

	/**
	 * Set the encoding of the FreeMarker template file.
	 * <p>By default {@link FreeMarkerConfigurer} sets the default encoding in
	 * the FreeMarker configuration to "UTF-8". It's recommended to specify the
	 * encoding in the FreeMarker Configuration rather than per template if all
	 * your templates share a common encoding.
	 */
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	/**
	 * Return the encoding for the FreeMarker template.
	 */
	protected String getEncoding() {
		return this.encoding;
	}


	@Override
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		if (getConfiguration() == null) {
			FreeMarkerConfig config = autodetectConfiguration();
			setConfiguration(config.getConfiguration());
		}
	}

	/**
	 * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
	 * @return the Configuration instance to use for FreeMarkerViews
	 * @throws BeansException if no Configuration instance could be found
	 * @see #setConfiguration
	 */
	protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
		try {
			return BeanFactoryUtils.beanOfTypeIncludingAncestors(
					getApplicationContext(), FreeMarkerConfig.class, true, false);
		}
		catch (NoSuchBeanDefinitionException ex) {
			throw new ApplicationContextException(
					"Must define a single FreeMarkerConfig bean in this web application context " +
							"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
							"This bean may be given any name.", ex);
		}
	}


	/**
	 * Check that the FreeMarker template used for this view exists and is valid.
	 * <p>Can be overridden to customize the behavior, for example in case of
	 * multiple templates to be rendered into a single view.
	 */
	@Override
	public boolean checkResourceExists(Locale locale) throws Exception {
		try {
			// Check that we can get the template, even if we might subsequently get it again.
			getTemplate(locale);
			return true;
		}
		catch (FileNotFoundException ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("No FreeMarker view found for URL: " + getUrl());
			}
			return false;
		}
		catch (ParseException ex) {
			throw new ApplicationContextException(
					"Failed to parse FreeMarker template for URL [" +  getUrl() + "]", ex);
		}
		catch (IOException ex) {
			throw new ApplicationContextException(
					"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
		}
	}

	@Override
165 166
	protected Mono<Void> renderInternal(Map<String, Object> renderAttributes, MediaType contentType,
			ServerWebExchange exchange) {
167 168 169 170 171 172
		// Expose all standard FreeMarker hash models.
		SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
		if (logger.isDebugEnabled()) {
			logger.debug("Rendering FreeMarker template [" + getUrl() + "].");
		}
		Locale locale = Locale.getDefault(); // TODO
173
		DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
174
		try {
175 176
			Charset charset = getCharset(contentType).orElse(getDefaultCharset());
			Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset);
177 178 179 180
			getTemplate(locale).process(freeMarkerModel, writer);
		}
		catch (IOException ex) {
			String message = "Could not load FreeMarker template for URL [" + getUrl() + "]";
R
Rossen Stoyanchev 已提交
181
			return Mono.error(new IllegalStateException(message, ex));
182 183
		}
		catch (Throwable ex) {
R
Rossen Stoyanchev 已提交
184
			return Mono.error(ex);
185
		}
R
Rossen Stoyanchev 已提交
186
		return exchange.getResponse().writeWith(Flux.just(dataBuffer));
187 188
	}

189 190 191 192
	private static Optional<Charset> getCharset(MediaType mediaType) {
		return mediaType != null ? Optional.ofNullable(mediaType.getCharset()) : Optional.empty();
	}

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
	/**
	 * Build a FreeMarker template model for the given model Map.
	 * <p>The default implementation builds a {@link SimpleHash}.
	 * @param model the model to use for rendering
	 * @param exchange current exchange
	 * @return the FreeMarker template model, as a {@link SimpleHash} or subclass thereof
	 */
	protected SimpleHash getTemplateModel(Map<String, Object> model, ServerWebExchange exchange) {
		SimpleHash fmModel = new SimpleHash(getObjectWrapper());
		fmModel.putAll(model);
		return fmModel;
	}

	/**
	 * Return the configured FreeMarker {@link ObjectWrapper}, or the
	 * {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified.
	 * @see freemarker.template.Configuration#getObjectWrapper()
	 */
	protected ObjectWrapper getObjectWrapper() {
		ObjectWrapper ow = getConfiguration().getObjectWrapper();
		Version version = Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS;
		return (ow != null ? ow : new DefaultObjectWrapperBuilder(version).build());
	}

	/**
	 * Retrieve the FreeMarker template for the given locale,
	 * to be rendering by this view.
	 * <p>By default, the template specified by the "url" bean property
	 * will be retrieved.
	 * @param locale the current locale
	 * @return the FreeMarker template to render
	 */
	protected Template getTemplate(Locale locale) throws IOException {
		return (getEncoding() != null ?
				getConfiguration().getTemplate(getUrl(), locale, getEncoding()) :
				getConfiguration().getTemplate(getUrl(), locale));
	}

}