/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.web;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.ws.rs.core.UriInfo;
import org.joda.beans.impl.flexi.FlexiBean;
import org.joda.beans.integrate.freemarker.FreemarkerObjectWrapper;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.format.DateTimeFormatterBuilder;
import com.opengamma.core.user.UserProfile;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.OpenGammaClock;
import com.opengamma.web.user.WebUser;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateScalarModel;
/**
* Main class that groups functionality for outputting to Freemarker.
* <p>
* Freemarker is a template engine that allows Jaa objects to be easily converted to text.
* The Freemarker system is controlled by a {@link Configuration configuration class}.
* <p>
* The configuration is typically managed within the {@link ServletContext}.
* The application initialization should call {@link #createConfiguration()}
* followed by {@link #init(ServletContext, Configuration)} to setup the servlet context.
* This can then be used by subclasses of {@link AbstractPerRequestWebResource}.
* <p>
* An instance of this class is intended to be used from multiple threads,
* however thread-safety is not enforced.
*/
public class FreemarkerOutputter {
/**
* The servlet context attribute.
*/
private static final String FREEMARKER_CONFIGURATION = FreemarkerOutputter.class.getName() + ".FreemarkerConfiguration";
/**
* The Freemarker configuration.
*/
private final Configuration _configuration;
/**
* Creates the Freemarker system configuration.
* <p>
* This creates the {@link Configuration Freemarker configuration} which must be customised
* with a template loader. Callers must then invoke {@link #init(ServletContext, Configuration)}.
* <p>
* The configuration uses UTF-8, a default locale of English, localized lookup and
* always includes the file "common/base.ftl".
* A model is added that converts nulls to empty strings
*
* @return the standard Freemarker configuration, not null
*/
public static Configuration createConfiguration() {
Configuration cfg = new Configuration();
cfg.setDefaultEncoding("UTF-8");
cfg.setOutputEncoding("UTF-8");
cfg.setLocale(Locale.ENGLISH);
cfg.setLocalizedLookup(true);
cfg.addAutoInclude("common/base.ftl");
FreemarkerObjectWrapper objectWrapper = new FreemarkerObjectWrapper();
objectWrapper.setNullModel(TemplateScalarModel.EMPTY_STRING);
cfg.setObjectWrapper(objectWrapper);
return cfg;
}
/**
* Initializes the Freemarker system.
* <p>
* This stores the {@link Configuration Freemarker configuration} in the servlet context for later use.
*
* @param servletContext the servlet context, not null
* @param configuration the configuration to use, not null
*/
public static void init(ServletContext servletContext, Configuration configuration) {
servletContext.setAttribute(FreemarkerOutputter.FREEMARKER_CONFIGURATION, configuration);
}
/**
* Creates a new Freemarker root data.
* <p>
* This creates a new data object to be passed to Freemarker with some standard keys:
* <ul>
* <li>now - the current date-time using {@link OpenGammaClock}
* <li>timeFormatter - a formatter that outputs the time as HH:mm:ss
* <li>offsetFormatter - a formatter that outputs the time-zone offset
* </ul>
*
* @return the root data, not null
*/
public static FlexiBean createRootData() {
FlexiBean data = new FlexiBean();
data.put("now", ZonedDateTime.now(OpenGammaClock.getInstance()));
data.put("locale", Locale.ENGLISH);
data.put("timeZone", OpenGammaClock.getInstance().getZone());
data.put("dateFormatter", DateTimeFormatter.ofPattern("d MMM yyyy"));
data.put("timeFormatter", DateTimeFormatter.ofPattern("HH:mm:ss"));
data.put("offsetFormatter", new DateTimeFormatterBuilder().appendOffsetId().toFormatter());
return data;
}
/**
* Creates a new Freemarker root data.
* <p>
* This creates a new data object to be passed to Freemarker with some standard keys:
* <ul>
* <li>now - the current date-time using {@link OpenGammaClock}
* <li>timeFormatter - a formatter that outputs the time as HH:mm:ss
* <li>offsetFormatter - a formatter that outputs the time-zone offset
* <li>homeUris - the home URIs
* <li>baseUri - the base URI
* <li>security - an instance of WebSecurity
* </ul>
*
* @param uriInfo the URI information, not null
* @return the root data, not null
*/
public static FlexiBean createRootData(UriInfo uriInfo) {
FlexiBean out = FreemarkerOutputter.createRootData();
out.put("homeUris", new WebHomeUris(uriInfo));
out.put("baseUri", uriInfo.getBaseUri().toString());
WebUser user = new WebUser(uriInfo);
UserProfile profile = user.getProfile();
if (profile != null) {
Locale locale = profile.getLocale();
ZoneId zone = profile.getZone();
DateTimeFormatter dateFormatter = profile.getDateStyle().formatter(locale);
DateTimeFormatter timeFormatter = profile.getTimeStyle().formatter(locale);
ZonedDateTime now = ZonedDateTime.now(OpenGammaClock.getInstance().withZone(zone));
out.put("now", now);
out.put("locale", locale);
out.put("timeZone", zone);
out.put("dateFormatter", dateFormatter);
out.put("timeFormatter", timeFormatter);
}
out.put("userSecurity", user);
return out;
}
//-------------------------------------------------------------------------
/**
* Creates the resource.
* <p>
* This constructor extracts the Freemarker configuration from the {@code ServletContext}.
*
* @param servletContext the servlet context, not null
*/
public FreemarkerOutputter(final ServletContext servletContext) {
ArgumentChecker.notNull(servletContext, "servletContext");
_configuration = (Configuration) servletContext.getAttribute(FREEMARKER_CONFIGURATION);
ArgumentChecker.notNull(_configuration, "Freemarker configuration");
}
/**
* Creates the resource.
* <p>
* This constructor allows the Freemarker configuration to be directly passed in.
* It is recommended to use the {@code ServletContext} constructor to allow the configuration
* to be managed in the context.
*
* @param configuration the configuration, not null
*/
public FreemarkerOutputter(final Configuration configuration) {
ArgumentChecker.notNull(configuration, "configuration");
_configuration = configuration;
}
//-------------------------------------------------------------------------
/**
* Gets the Freemarker configuration, which must not be altered.
*
* @return the configuration, not null
*/
public Configuration getConfiguration() {
return _configuration;
}
/**
* Creates a Freemarker template.
* <p>
* This converts a template name, which may include a file system path, into a
* configured {@code Template} object.
*
* @param templateName the template name, not null
* @return the template, not null
* @throws RuntimeException if an error occurs
*/
public Template createTemplate(final String templateName) {
try {
return _configuration.getTemplate(templateName);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
//-------------------------------------------------------------------------
/**
* Builds the Freemarker template creating the output string.
* <p>
* The following keys are added to the data if it is a {@code Map} or {@code FlexiBean}:
* <ul>
* <li>freemarkerTemplateName - the template name
* <li>freemarkerLocale - the locale of the template
* <li>freemarkerVersion - the version of the Freemarker configuration
* </ul>
*
* @param templateName the template name, not null
* @param data the root data to merge, not null
* @return the template, not null
* @throws RuntimeException if an error occurs
*/
public String build(final String templateName, final Object data) {
return build(createTemplate(templateName), data);
}
/**
* Builds the Freemarker template creating the output string.
* <p>
* The following keys are added to the data if it is a {@code Map} or {@code FlexiBean}:
* <ul>
* <li>freemarkerTemplateName - the template name
* <li>freemarkerLocale - the locale of the template
* <li>freemarkerVersion - the version of the Freemarker configuration
* </ul>
*
* @param template the template, not null
* @param data the root data to merge, not null
* @return the template, not null
* @throws RuntimeException if an error occurs
*/
@SuppressWarnings("unchecked")
public String build(final Template template, final Object data) {
if (data instanceof FlexiBean) {
((FlexiBean) data).put("freemarkerTemplateName", template.getName());
((FlexiBean) data).put("freemarkerLocale", template.getLocale());
((FlexiBean) data).put("freemarkerVersion", Configuration.getVersionNumber());
}
if (data instanceof Map<?, ?>) {
((Map<String, Object>) data).put("freemarkerTemplateName", template.getName());
((Map<String, Object>) data).put("freemarkerLocale", template.getLocale());
((Map<String, Object>) data).put("freemarkerVersion", Configuration.getVersionNumber());
}
try (StringWriter out = new StringWriter(1024 * 4)) {
template.process(data, out);
return out.toString();
} catch (Exception ex) {
return handleException(ex);
}
}
/**
* Handles any exception in template output.
*
* @param ex the exception from Freemarker, not null
* @return a dummy return type for Java compiler reasons
*/
private String handleException(final Exception ex) {
throw new RuntimeException(ex);
}
//-------------------------------------------------------------------------
@Override
public String toString() {
return String.format("FreemarkerOutputter[%s]", Configuration.getVersionNumber());
}
}