org.zodiac.template.velocity.spring.view.servlet.ServletVelocityView Maven / Gradle / Ivy
package org.zodiac.template.velocity.spring.view.servlet;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.tools.generic.DateTool;
import org.apache.velocity.tools.generic.NumberTool;
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.NestedIOException;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.AbstractTemplateView;
import org.springframework.web.util.NestedServletException;
import org.zodiac.template.velocity.spring.view.VelocityConfig;
public class ServletVelocityView extends AbstractTemplateView {
private Map> toolAttributes;
private String dateToolAttribute;
private String numberToolAttribute;
private String encoding;
private boolean cacheTemplate = false;
private VelocityEngine velocityEngine;
private Template template;
public void setToolAttributes(Map> toolAttributes) {
this.toolAttributes = toolAttributes;
}
public void setDateToolAttribute(String dateToolAttribute) {
this.dateToolAttribute = dateToolAttribute;
}
public void setNumberToolAttribute(String numberToolAttribute) {
this.numberToolAttribute = numberToolAttribute;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
protected String getEncoding() {
return this.encoding;
}
public void setCacheTemplate(boolean cacheTemplate) {
this.cacheTemplate = cacheTemplate;
}
protected boolean isCacheTemplate() {
return this.cacheTemplate;
}
public void setVelocityEngine(VelocityEngine velocityEngine) {
this.velocityEngine = velocityEngine;
}
protected VelocityEngine getVelocityEngine() {
return this.velocityEngine;
}
/**
* Invoked on startup. Looks for a single VelocityConfig bean to find the relevant VelocityEngine for this factory.
*/
@Override
protected void initApplicationContext() throws BeansException {
super.initApplicationContext();
if (getVelocityEngine() == null) {
// No explicit VelocityEngine: try to autodetect one.
setVelocityEngine(autodetectVelocityEngine());
}
}
/**
* Autodetect a VelocityEngine via the ApplicationContext. Called if no explicit VelocityEngine has been specified.
*
* @return The VelocityEngine to use for VelocityViews
* @throws BeansException if no VelocityEngine could be found
*/
protected VelocityEngine autodetectVelocityEngine() throws BeansException {
try {
VelocityConfig velocityConfig = BeanFactoryUtils.beanOfTypeIncludingAncestors(getApplicationContext(),
VelocityConfig.class, true, false);
return velocityConfig.getVelocityEngine();
} catch (NoSuchBeanDefinitionException ex) {
throw new ApplicationContextException(
"Must define a single VelocityConfig bean in this web application context "
+ "(may be inherited): ServletVelocityConfigurer is the usual implementation. "
+ "This bean may be given any name.",
ex);
}
}
/**
* Check that the Velocity template used for this view exists and is valid.
*
* Can be overridden to customize the behavior, for example in case of multiple templates to be rendered into a
* single view.
*/
@Override
public boolean checkResource(Locale locale) throws Exception {
try {
// Check that we can get the template, even if we might subsequently get it again.
this.template = getTemplate(getUrl());
return true;
} catch (ResourceNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No Velocity view found for URL: " + getUrl());
}
return false;
} catch (Exception ex) {
throw new NestedIOException("Could not load Velocity template for URL [" + getUrl() + "]", ex);
}
}
/**
* Process the model map by merging it with the Velocity template. Output is directed to the servlet response.
*
* This method can be overridden if custom behavior is needed.
*/
@Override
protected void renderMergedTemplateModel(Map model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
exposeHelpers(model, request);
Context velocityContext = createVelocityContext(model, request, response);
exposeHelpers(velocityContext, request, response);
exposeToolAttributes(velocityContext, request);
doRender(velocityContext, response);
}
/**
* Expose helpers unique to each rendering operation. This is necessary so that different rendering operations can't
* overwrite each other's formats etc.
*
* Called by {@code renderMergedTemplateModel}. The default implementation is empty. This method can be overridden
* to add custom helpers to the model.
*
* @param model the model that will be passed to the template for merging
* @param request current HTTP request
* @throws Exception if there's a fatal error while we're adding model attributes
* @see #renderMergedTemplateModel
*/
protected void exposeHelpers(Map model, HttpServletRequest request) throws Exception {}
/**
* Create a Velocity JSONContext instance for the given model, to be passed to the template for merging.
*
* The default implementation delegates to {@link #createVelocityContext(Map)}. Can be overridden for a special
* context class, for example ChainedContext which is part of the view package of Velocity Tools. ChainedContext is
* needed for initialization of ViewTool instances.
*
* Have a look at {@link ServletVelocityToolboxView}, which pre-implements ChainedContext support. This is not part
* of the standard ServletVelocityView class in order to avoid a required dependency on the view package of Velocity
* Tools.
*
* @param model the model Map, containing the model attributes to be exposed to the view
* @param request current HTTP request
* @param response current HTTP response
* @return The Velocity JSONContext
* @throws Exception if there's a fatal error while creating the context
*/
protected Context createVelocityContext(Map model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
return createVelocityContext(model);
}
/**
* Create a Velocity JSONContext instance for the given model, to be passed to the template for merging.
*
* Default implementation creates an instance of Velocity's VelocityContext implementation class.
*
* @param model The model Map, containing the model attributes to be exposed to the view.
* @return The Velocity JSONContext.
* @throws Exception If there's a fatal error while creating the context.
* @see org.apache.velocity.VelocityContext
*/
protected Context createVelocityContext(Map model) throws Exception {
return new VelocityContext(model);
}
/**
* Expose helpers unique to each rendering operation. This is necessary so that different rendering operations can't
* overwrite each other's formats etc.
*
* Called by {@code renderMergedTemplateModel}. Default implementation delegates to
* {@code exposeHelpers(velocityContext, request)}. This method can be overridden to add special tools to the
* context, needing the servlet response to initialize (see Velocity Tools, for example LinkTool and
* ViewTool/ChainedContext).
*
* @param velocityContext
* Velocity context that will be passed to the template
* @param request
* current HTTP request
* @param response
* current HTTP response
* @throws Exception
* if there's a fatal error while we're adding model attributes
* @see #exposeHelpers(org.apache.velocity.context.Context, HttpServletRequest)
*/
protected void exposeHelpers(Context velocityContext, HttpServletRequest request, HttpServletResponse response)
throws Exception {
exposeHelpers(velocityContext, request);
}
/**
* Expose helpers unique to each rendering operation. This is necessary so that different rendering operations can't
* overwrite each other's formats etc.
*
* Default implementation is empty. This method can be overridden to add custom helpers to the Velocity context.
*
* @param velocityContext
* Velocity context that will be passed to the template
* @param request
* current HTTP request
* @throws Exception
* if there's a fatal error while we're adding model attributes
* @see #exposeHelpers(Map, HttpServletRequest)
*/
protected void exposeHelpers(Context velocityContext, HttpServletRequest request) throws Exception {}
/**
* Expose the tool attributes, according to corresponding bean property settings.
*
* Do not override this method unless for further tools driven by bean properties. Override one of the
* {@code exposeHelpers} methods to add custom helpers.
*
* @param velocityContext
* Velocity context that will be passed to the template
* @param request
* current HTTP request
* @throws Exception
* if there's a fatal error while we're adding model attributes
* @see #setDateToolAttribute
* @see #setNumberToolAttribute
* @see #exposeHelpers(Map, HttpServletRequest)
* @see #exposeHelpers(org.apache.velocity.context.Context, HttpServletRequest, HttpServletResponse)
*/
protected void exposeToolAttributes(Context velocityContext, HttpServletRequest request) throws Exception {
// Expose generic attributes.
if (this.toolAttributes != null) {
for (Map.Entry> entry : this.toolAttributes.entrySet()) {
String attributeName = entry.getKey();
Class> toolClass = entry.getValue();
try {
Object tool = toolClass.newInstance();
initTool(tool, velocityContext);
velocityContext.put(attributeName, tool);
} catch (Exception ex) {
throw new NestedServletException("Could not instantiate Velocity tool '" + attributeName + "'", ex);
}
}
}
// Expose locale-aware DateTool/NumberTool attributes.
if (this.dateToolAttribute != null || this.numberToolAttribute != null) {
if (this.dateToolAttribute != null) {
velocityContext.put(this.dateToolAttribute, new LocaleAwareDateTool(request));
}
if (this.numberToolAttribute != null) {
velocityContext.put(this.numberToolAttribute, new LocaleAwareNumberTool(request));
}
}
}
/**
* Initialize the given tool instance. The default implementation is empty.
*
* Can be overridden to check for special callback interfaces, for example the ViewContext interface which is part
* of the view package of Velocity Tools. In the particular case of ViewContext, you'll usually also need a special
* Velocity context, like ChainedContext which is part of Velocity Tools too.
*
* Have a look at {@link ServletVelocityToolboxView}, which pre-implements such a ViewTool check. This is not part
* of the standard ServletVelocityView class in order to avoid a required dependency on the view package of Velocity
* Tools.
*
* @param tool the tool instance to initialize
* @param velocityContext the Velocity context
* @throws Exception if initializion of the tool failed
*/
protected void initTool(Object tool, Context velocityContext) throws Exception {}
/**
* Render the Velocity view to the given response, using the given Velocity context which contains the complete
* template model to use.
*
* The default implementation renders the template specified by the "url" bean property, retrieved via
* {@code getTemplate}. It delegates to the {@code mergeTemplate} method to merge the template instance with the
* given Velocity context.
*
* Can be overridden to customize the behavior, for example to render multiple templates into a single view.
*
* @param context
* the Velocity context to use for rendering
* @param response
* servlet response (use this to get the OutputStream or Writer)
* @throws Exception
* if thrown by Velocity
* @see #setUrl
* @see #getTemplate()
* @see #mergeTemplate
*/
protected void doRender(Context context, HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
logger
.debug("Rendering Velocity template [" + getUrl() + "] in ServletVelocityView '" + getBeanName() + "'");
}
mergeTemplate(getTemplate(), context, response);
}
/**
* Retrieve the Velocity template to be rendered by this view.
*
* By default, the template specified by the "url" bean property will be retrieved: either returning a cached
* template instance or loading a fresh instance (according to the "cacheTemplate" bean property)
*
* @return The Velocity template to render
* @throws Exception
* if thrown by Velocity
* @see #setUrl
* @see #setCacheTemplate
* @see #getTemplate(String)
*/
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());
}
}
/**
* Retrieve the Velocity template specified by the given name, using the encoding specified by the "encoding" bean
* property.
*
* Can be called by subclasses to retrieve a specific template, for example to render multiple templates into a
* single view.
*
* @param name
* the file name of the desired template
* @return The Velocity template
* @throws Exception
* if thrown by Velocity
* @see org.apache.velocity.app.VelocityEngine#getTemplate
*/
protected Template getTemplate(String name) throws Exception {
return (getEncoding() != null ? getVelocityEngine().getTemplate(name, getEncoding())
: getVelocityEngine().getTemplate(name));
}
/**
* Merge the template with the context. Can be overridden to customize the behavior.
*
* @param template
* the template to merge
* @param context
* the Velocity context to use for rendering
* @param response
* servlet response (use this to get the OutputStream or Writer)
* @throws Exception
* if thrown by Velocity
* @see org.apache.velocity.Template#merge
*/
protected void mergeTemplate(Template template, Context context, HttpServletResponse response) throws Exception {
try {
template.merge(context, response.getWriter());
} catch (MethodInvocationException ex) {
Throwable cause = ex.getCause();
throw new NestedServletException("Method invocation failed during rendering of Velocity view with name '"
+ getBeanName() + "': " + ex.getMessage() + "; reference [" + ex.getReferenceName() + "], method '"
+ ex.getMethodName() + "'", cause == null ? ex : cause);
}
}
/**
* Subclass of DateTool from Velocity Tools, using a Spring-resolved Locale and TimeZone instead of the default
* Locale.
*
* @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
* @see org.springframework.web.servlet.support.RequestContextUtils#getTimeZone
*/
private static class LocaleAwareDateTool extends DateTool {
private static final long serialVersionUID = 8726680345499455254L;
private final HttpServletRequest request;
public LocaleAwareDateTool(HttpServletRequest request) {
this.request = request;
}
@Override
public Locale getLocale() {
return RequestContextUtils.getLocale(this.request);
}
@Override
public TimeZone getTimeZone() {
TimeZone timeZone = RequestContextUtils.getTimeZone(this.request);
return (timeZone != null ? timeZone : super.getTimeZone());
}
}
/**
* Subclass of NumberTool from Velocity Tools, using a Spring-resolved Locale instead of the default Locale.
*
* @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
*/
private static class LocaleAwareNumberTool extends NumberTool {
private static final long serialVersionUID = 494548903620628263L;
private final HttpServletRequest request;
public LocaleAwareNumberTool(HttpServletRequest request) {
this.request = request;
}
@Override
public Locale getLocale() {
return RequestContextUtils.getLocale(this.request);
}
}
}