org.zodiac.template.velocity.spring.view.reactive.ReactiveVelocityView Maven / Gradle / Ivy
The newest version!
package org.zodiac.template.velocity.spring.view.reactive;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.apache.velocity.Template;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.VelocityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.web.reactive.result.view.AbstractUrlBasedView;
import org.springframework.web.reactive.result.view.RequestContext;
import org.springframework.web.reactive.result.view.RequestDataValueProcessor;
import org.springframework.web.server.ServerWebExchange;
import org.zodiac.sdk.toolkit.util.AssertUtil;
import org.zodiac.sdk.toolkit.util.collection.CollUtil;
import org.zodiac.template.velocity.constants.VelocityTemplateConstants;
import org.zodiac.template.velocity.platform.PlatformVelocityContext;
import org.zodiac.template.velocity.spring.view.VelocityConfig;
import org.zodiac.template.velocity.spring.view.VelocityConfigurer;
import reactor.core.publisher.Mono;
public class ReactiveVelocityView extends AbstractUrlBasedView {
protected final Logger log = LoggerFactory.getLogger(getClass());
private Map> toolAttributes;
private String contentType = VelocityTemplateConstants.DEFAULT_CONTENT_TYPE;
private boolean exposeRequestAttributes = false;
private boolean allowRequestOverride = false;
private boolean exposeSessionAttributes = false;
private boolean allowSessionOverride = false;
private boolean exposeSpringMacroHelpers = true;
private String dateToolAttribute;
private String numberToolAttribute;
private String encoding;
private boolean cacheTemplate = false;
private VelocityEngine velocityEngine;
private Template template;
public ReactiveVelocityView() {
super();
}
public ReactiveVelocityView(String url) {
super(url);
}
@Override
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
if (null == getVelocityEngine()) {
VelocityConfig config = autodetectVelocityEngine();
this.velocityEngine = config.getVelocityEngine();
}
}
@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) {
/*Allow for ViewResolver chaining.*/
return false;
} catch (ParseErrorException 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);
}
}
public Map> getToolAttributes() {
return toolAttributes;
}
public void setToolAttributes(Map> toolAttributes) {
this.toolAttributes = toolAttributes;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public boolean isExposeRequestAttributes() {
return exposeRequestAttributes;
}
public void setExposeRequestAttributes(boolean exposeRequestAttributes) {
this.exposeRequestAttributes = exposeRequestAttributes;
}
public boolean isAllowRequestOverride() {
return allowRequestOverride;
}
public void setAllowRequestOverride(boolean allowRequestOverride) {
this.allowRequestOverride = allowRequestOverride;
}
public boolean isExposeSessionAttributes() {
return exposeSessionAttributes;
}
public void setExposeSessionAttributes(boolean exposeSessionAttributes) {
this.exposeSessionAttributes = exposeSessionAttributes;
}
public boolean isAllowSessionOverride() {
return allowSessionOverride;
}
public void setAllowSessionOverride(boolean allowSessionOverride) {
this.allowSessionOverride = allowSessionOverride;
}
public boolean isExposeSpringMacroHelpers() {
return exposeSpringMacroHelpers;
}
public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) {
this.exposeSpringMacroHelpers = exposeSpringMacroHelpers;
}
public String getDateToolAttribute() {
return dateToolAttribute;
}
public void setDateToolAttribute(String dateToolAttribute) {
this.dateToolAttribute = dateToolAttribute;
}
public String getNumberToolAttribute() {
return numberToolAttribute;
}
public void setNumberToolAttribute(String numberToolAttribute) {
this.numberToolAttribute = numberToolAttribute;
}
public String getEncoding() {
return encoding;
}
public boolean isCacheTemplate() {
return cacheTemplate;
}
public void setCacheTemplate(boolean cacheTemplate) {
this.cacheTemplate = cacheTemplate;
}
public void setVelocityEngine(VelocityEngine velocityEngine) {
this.velocityEngine = velocityEngine;
}
protected void setEncoding(String encoding) {
this.encoding = encoding;
}
protected VelocityEngine getVelocityEngine() {
return velocityEngine;
}
protected VelocityEngine obtainVelocityEngine() {
VelocityEngine velocityEngine = getVelocityEngine();
AssertUtil.state(velocityEngine != null, "No velocityEngine set");
return velocityEngine;
}
@Override
protected Mono renderInternal(Map renderAttributes, MediaType contentType,
ServerWebExchange exchange) {
MediaType mediaType = contentType == null && null != getContentType() ? new MediaType(getContentType()) : contentType;
if (this.exposeSessionAttributes) {
Map exposed = new LinkedHashMap<>();
Map attributes = exchange.getSession().block().getAttributes();
if (CollUtil.isNotEmptyMap(attributes)) {
attributes.forEach((k, v) -> {
if (renderAttributes.containsKey(k) && !this.allowRequestOverride) {
throw new VelocityException(String.format("Cannot expose request attribute '%s' because of an existing model object of the same name", k));
}
if (log.isDebugEnabled()) {
exposed.put(k, v);
}
renderAttributes.put(k, v);
});
if (log.isTraceEnabled() && exposed != null) {
log.trace("Exposed request attributes to model: {}", exposed);
}
}
}
if (this.exposeSpringMacroHelpers) {
if (renderAttributes.containsKey(VelocityTemplateConstants.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
throw new VelocityException(
String.format("Cannot expose bind macro helper '%s' because of an existing model object of the same name", VelocityTemplateConstants.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE));
}
/*Expose RequestContext instance for Spring macros.*/
renderAttributes.put(VelocityTemplateConstants.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
new RequestContext(exchange, renderAttributes, obtainApplicationContext(), getRequestDataValueProcessor()));
}
return exchange.getResponse().writeWith(Mono.fromCallable(() -> {
/*Expose all standard FreeMarker hash models.*/
Context velocityContext = getTemplateContext(renderAttributes, exchange);
if (logger.isDebugEnabled()) {
log.debug("{} Rendering [{}]", exchange.getLogPrefix(), getUrl());
}
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
DataBuffer dataBuffer = exchange.getResponse().bufferFactory().allocateBuffer();
try {
Charset charset = getCharset(mediaType);
Writer writer = new OutputStreamWriter(dataBuffer.asOutputStream(), charset);
getTemplate(locale).merge(velocityContext, writer);
return dataBuffer;
} catch (IOException ex) {
DataBufferUtils.release(dataBuffer);
String message = String.format("Could not load FreeMarker template for URL [%s]", getUrl());
throw new IllegalStateException(message, ex);
} catch (Throwable ex) {
DataBufferUtils.release(dataBuffer);
throw ex;
}
}).doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release));
}
protected RequestDataValueProcessor getRequestDataValueProcessor() {
ApplicationContext context = getApplicationContext();
if (context != null && context.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) {
return context.getBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class);
}
return null;
}
protected VelocityConfig autodetectVelocityEngine() throws BeansException {
try {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(obtainApplicationContext(), VelocityConfig.class, true,
false);
} catch (NoSuchBeanDefinitionException ex) {
StringBuilder error =
new StringBuilder("Must define a single ").append(VelocityConfig.class.getSimpleName())
.append(" bean in this web application context (may be inherited): ")
.append(VelocityConfigurer.class.getSimpleName())
.append(" is the usual implementation. This bean may be given any name.");
throw new ApplicationContextException(error.toString(), ex);
}
}
protected Template getTemplate(Locale locale) throws IOException {
obtainVelocityEngine().getTemplate(getUrl(), getEncoding());
return (getEncoding() != null ? obtainVelocityEngine().getTemplate(getUrl(), getEncoding())
: obtainVelocityEngine().getTemplate(getUrl()));
}
protected Template getTemplate(String name) throws Exception {
return (getEncoding() != null ? getVelocityEngine().getTemplate(name, getEncoding())
: getVelocityEngine().getTemplate(name));
}
protected Template getTemplate() throws Exception {
/*
* We already hold a reference to the template, but we might want to load it
* if not caching. Velocity itself caches templates, so our ability to
* cache templates in this class is a minor optimization only.
* */
if (isCacheTemplate() && this.template != null) {
return this.template;
} else {
return getTemplate(getUrl());
}
}
protected Context getTemplateContext(Map model, ServerWebExchange exchange) {
//Context velocityContext = new VelocityContext(model);
Context velocityContext = new PlatformVelocityContext(model);
return velocityContext;
}
private Charset getCharset(@Nullable MediaType mediaType) {
return Optional.ofNullable(mediaType).map(MimeType::getCharset).orElse(getDefaultCharset());
}
}