| /******************************************************************************* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| *******************************************************************************/ |
| package org.apache.ofbiz.base.util.template; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TimeZone; |
| |
| import javax.servlet.ServletContext; |
| import javax.servlet.http.HttpServletRequest; |
| |
| import org.apache.ofbiz.base.location.FlexibleLocation; |
| import org.apache.ofbiz.base.util.Debug; |
| import org.apache.ofbiz.base.util.StringUtil; |
| import org.apache.ofbiz.base.util.UtilGenerics; |
| import org.apache.ofbiz.base.util.UtilMisc; |
| import org.apache.ofbiz.base.util.UtilProperties; |
| import org.apache.ofbiz.base.util.UtilValidate; |
| import org.apache.ofbiz.base.util.cache.UtilCache; |
| |
| import freemarker.cache.ConditionalTemplateConfigurationFactory; |
| import freemarker.cache.MultiTemplateLoader; |
| import freemarker.cache.PathGlobMatcher; |
| import freemarker.cache.StringTemplateLoader; |
| import freemarker.cache.TemplateLoader; |
| import freemarker.cache.URLTemplateLoader; |
| import freemarker.core.Environment; |
| import freemarker.core.HTMLOutputFormat; |
| import freemarker.core.TemplateConfiguration; |
| import freemarker.ext.beans.BeanModel; |
| import freemarker.ext.beans.BeansWrapper; |
| import freemarker.ext.beans.BeansWrapperBuilder; |
| import freemarker.template.Configuration; |
| import freemarker.template.SimpleHash; |
| import freemarker.template.SimpleScalar; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateException; |
| import freemarker.template.TemplateExceptionHandler; |
| import freemarker.template.TemplateHashModel; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.Version; |
| |
| /** |
| * FreeMarkerWorker - Freemarker Template Engine Utilities. |
| */ |
| public final class FreeMarkerWorker { |
| |
| public static final String module = FreeMarkerWorker.class.getName(); |
| |
| public static final Version version = Configuration.VERSION_2_3_25; |
| |
| private FreeMarkerWorker () {} |
| |
| // use soft references for this so that things from Content records don't kill all of our memory, or maybe not for performance reasons... hmmm, leave to config file... |
| private static final UtilCache<String, Template> cachedTemplates = UtilCache.createUtilCache("template.ftl.general", 0, 0, false); |
| private static final BeansWrapper defaultOfbizWrapper = new BeansWrapperBuilder(version).build(); |
| private static final Configuration defaultOfbizConfig = makeConfiguration(defaultOfbizWrapper); |
| |
| public static BeansWrapper getDefaultOfbizWrapper() { |
| return defaultOfbizWrapper; |
| } |
| |
| public static Configuration newConfiguration() { |
| return new Configuration(version); |
| } |
| |
| public static Configuration makeConfiguration(BeansWrapper wrapper) { |
| Configuration newConfig = newConfiguration(); |
| |
| TemplateConfiguration tcHTML = new TemplateConfiguration(); |
| tcHTML.setOutputFormat(HTMLOutputFormat.INSTANCE); |
| |
| newConfig.setTemplateConfigurations( |
| new ConditionalTemplateConfigurationFactory( |
| new PathGlobMatcher("*.ftl"), |
| tcHTML)); |
| |
| newConfig.setObjectWrapper(wrapper); |
| TemplateHashModel staticModels = wrapper.getStaticModels(); |
| newConfig.setSharedVariable("Static", staticModels); |
| try { |
| newConfig.setSharedVariable("EntityQuery", staticModels.get("org.apache.ofbiz.entity.util.EntityQuery")); |
| } catch (TemplateModelException e) { |
| Debug.logError(e, module); |
| } |
| newConfig.setLocalizedLookup(false); |
| newConfig.setSharedVariable("StringUtil", new BeanModel(StringUtil.INSTANCE, wrapper)); |
| TemplateLoader[] templateLoaders = {new FlexibleTemplateLoader(), new StringTemplateLoader()}; |
| MultiTemplateLoader multiTemplateLoader = new MultiTemplateLoader(templateLoaders); |
| newConfig.setTemplateLoader(multiTemplateLoader); |
| Map freemarkerImports = UtilProperties.getProperties("freemarkerImports"); |
| if (freemarkerImports != null) { |
| newConfig.setAutoImports(freemarkerImports); |
| } |
| newConfig.setLogTemplateExceptions(false); |
| newConfig.setTemplateExceptionHandler(new FreeMarkerWorker.OFBizTemplateExceptionHandler()); |
| try { |
| newConfig.setSetting("datetime_format", "yyyy-MM-dd HH:mm:ss.SSS"); |
| newConfig.setSetting("number_format", "0.##########"); |
| } catch (TemplateException e) { |
| Debug.logError("Unable to set date/time and number formats in FreeMarker: " + e, module); |
| } |
| // Transforms properties file set up as key=transform name, property=transform class name |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| Enumeration<URL> resources; |
| try { |
| resources = loader.getResources("freemarkerTransforms.properties"); |
| } catch (IOException e) { |
| Debug.logError(e, "Could not load list of freemarkerTransforms.properties", module); |
| throw UtilMisc.initCause(new InternalError(e.getMessage()), e); |
| } |
| while (resources.hasMoreElements()) { |
| URL propertyURL = resources.nextElement(); |
| Debug.logInfo("loading properties: " + propertyURL, module); |
| Properties props = UtilProperties.getProperties(propertyURL); |
| if (UtilValidate.isEmpty(props)) { |
| Debug.logError("Unable to locate properties file " + propertyURL, module); |
| } else { |
| loadTransforms(loader, props, newConfig); |
| } |
| } |
| |
| return newConfig; |
| } |
| |
| private static void loadTransforms(ClassLoader loader, Properties props, Configuration config) { |
| for (Iterator<Object> i = props.keySet().iterator(); i.hasNext();) { |
| String key = (String) i.next(); |
| String className = props.getProperty(key); |
| if (Debug.verboseOn()) { |
| Debug.logVerbose("Adding FTL Transform " + key + " with class " + className, module); |
| } |
| try { |
| config.setSharedVariable(key, loader.loadClass(className).newInstance()); |
| } catch (Exception e) { |
| Debug.logError(e, "Could not pre-initialize dynamically loaded class: " + className + ": " + e, module); |
| } |
| } |
| } |
| |
| /** |
| * Renders a template from a Reader. |
| * @param templateLocation A unique ID for this template - used for caching |
| * @param context The context Map |
| * @param outWriter The Writer to render to |
| */ |
| public static void renderTemplate(String templateLocation, Map<String, Object> context, Appendable outWriter) throws TemplateException, IOException { |
| Template template = getTemplate(templateLocation); |
| renderTemplate(template, context, outWriter); |
| } |
| |
| public static void renderTemplateFromString(String templateName, String templateString, Map<String, Object> context, Appendable outWriter, long lastModificationTime, boolean useCache) throws TemplateException, IOException { |
| Template template = null; |
| if (useCache) { |
| template = cachedTemplates.get(templateName); |
| } |
| if (template == null) { |
| StringTemplateLoader stringTemplateLoader = (StringTemplateLoader)((MultiTemplateLoader)defaultOfbizConfig.getTemplateLoader()).getTemplateLoader(1); |
| Object templateSource = stringTemplateLoader.findTemplateSource(templateName); |
| if (templateSource == null || stringTemplateLoader.getLastModified(templateSource) < lastModificationTime) { |
| stringTemplateLoader.putTemplate(templateName, templateString, lastModificationTime); |
| } |
| } |
| |
| template = getTemplate(templateName); |
| renderTemplate(template, context, outWriter); |
| } |
| |
| public static void clearTemplateFromCache(String templateLocation) { |
| cachedTemplates.remove(templateLocation); |
| try { |
| defaultOfbizConfig.removeTemplateFromCache(templateLocation); |
| } catch(Exception e) { |
| Debug.logInfo("Template not found in Fremarker cache with name: " + templateLocation, module); |
| } |
| } |
| |
| /** |
| * Renders a Template instance. |
| * @param template A Template instance |
| * @param context The context Map |
| * @param outWriter The Writer to render to |
| */ |
| public static Environment renderTemplate(Template template, Map<String, Object> context, Appendable outWriter) throws TemplateException, IOException { |
| // make sure there is no "null" string in there as FreeMarker will try to use it |
| context.remove("null"); |
| // Since the template cache keeps a single instance of a Template that is shared among users, |
| // and since that Template instance is immutable, we need to create an Environment instance and |
| // use it to process the template with the user's settings. |
| // |
| // FIXME: the casting from Appendable to Writer is a temporary fix that could cause a |
| // run time error if in the future we will pass a different class to the method |
| // (such as a StringBuffer). |
| Environment env = template.createProcessingEnvironment(context, (Writer) outWriter); |
| applyUserSettings(env, context); |
| env.process(); |
| return env; |
| } |
| |
| /** |
| * Apply user settings to an Environment instance. |
| * @param env An Environment instance |
| * @param context The context Map containing the user settings |
| */ |
| private static void applyUserSettings(Environment env, Map<String, Object> context) throws TemplateException { |
| Locale locale = (Locale) context.get("locale"); |
| if (locale == null) { |
| locale = Locale.getDefault(); |
| } |
| env.setLocale(locale); |
| |
| TimeZone timeZone = (TimeZone) context.get("timeZone"); |
| if (timeZone == null) { |
| timeZone = TimeZone.getDefault(); |
| } |
| env.setTimeZone(timeZone); |
| } |
| |
| /** |
| * Returns a <code>Configuration</code> instance initialized to OFBiz defaults. Client code should |
| * call this method instead of creating its own <code>Configuration</code> instance. The instance |
| * returned by this method includes the <code>component://</code> resolver and the OFBiz custom |
| * transformations. |
| * |
| * @return A <code>Configuration</code> instance. |
| */ |
| public static Configuration getDefaultOfbizConfig() { |
| return defaultOfbizConfig; |
| } |
| |
| /** |
| * Gets a Template instance from the template cache. If the Template instance isn't |
| * found in the cache, then one will be created. |
| * @param templateLocation Location of the template - file path or URL |
| */ |
| public static Template getTemplate(String templateLocation) throws TemplateException, IOException { |
| return getTemplate(templateLocation, cachedTemplates, defaultOfbizConfig); |
| } |
| |
| public static Template getTemplate(String templateLocation, UtilCache<String, Template> cache, Configuration config) throws TemplateException, IOException { |
| Template template = cache.get(templateLocation); |
| if (template == null) { |
| template = config.getTemplate(templateLocation); |
| template = cache.putIfAbsentAndGet(templateLocation, template); |
| } |
| return template; |
| } |
| |
| public static String getArg(Map<String, ? extends Object> args, String key, Environment env) { |
| Map<String, ? extends Object> templateContext = FreeMarkerWorker.getWrappedObject("context", env); |
| return getArg(args, key, templateContext); |
| } |
| |
| public static String getArg(Map<String, ? extends Object> args, String key, Map<String, ? extends Object> templateContext) { |
| Object o = args.get(key); |
| String returnVal = (String) unwrap(o); |
| if (returnVal == null) { |
| try { |
| if (templateContext != null) { |
| returnVal = (String) templateContext.get(key); |
| } |
| } catch (ClassCastException e2) { |
| Debug.logInfo(e2.getMessage(), module); |
| } |
| } |
| return returnVal; |
| } |
| |
| /** |
| * Gets BeanModel from FreeMarker context and returns the object that it wraps. |
| * @param varName the name of the variable in the FreeMarker context. |
| * @param env the FreeMarker Environment |
| */ |
| public static <T> T getWrappedObject(String varName, Environment env) { |
| Object obj = null; |
| try { |
| obj = env.getVariable(varName); |
| if (obj != null) { |
| if (obj == TemplateModel.NOTHING) { |
| obj = null; |
| } else if (obj instanceof BeanModel) { |
| BeanModel bean = (BeanModel) obj; |
| obj = bean.getWrappedObject(); |
| } else if (obj instanceof SimpleScalar) { |
| obj = obj.toString(); |
| } |
| } |
| } catch (TemplateModelException e) { |
| Debug.logInfo(e.getMessage(), module); |
| } |
| return UtilGenerics.<T>cast(obj); |
| } |
| |
| public static Object get(SimpleHash args, String key) { |
| Object o = null; |
| try { |
| o = args.get(key); |
| } catch (TemplateModelException e) { |
| Debug.logVerbose(e.getMessage(), module); |
| return null; |
| } |
| |
| Object returnObj = unwrap(o); |
| |
| if (returnObj == null) { |
| Object ctxObj = null; |
| try { |
| ctxObj = args.get("context"); |
| } catch (TemplateModelException e) { |
| Debug.logInfo(e.getMessage(), module); |
| return returnObj; |
| } |
| Map<String, ?> ctx = null; |
| if (ctxObj instanceof BeanModel) { |
| ctx = UtilGenerics.cast(((BeanModel) ctxObj).getWrappedObject()); |
| returnObj = ctx.get(key); |
| } |
| } |
| return returnObj; |
| } |
| |
| @SuppressWarnings("unchecked") |
| public static <T> T unwrap(Object o) { |
| Object returnObj = null; |
| |
| if (o == TemplateModel.NOTHING) { |
| returnObj = null; |
| } else if (o instanceof SimpleScalar) { |
| returnObj = o.toString(); |
| } else if (o instanceof BeanModel) { |
| returnObj = ((BeanModel) o).getWrappedObject(); |
| } |
| |
| return (T) returnObj; |
| } |
| |
| public static Map<String, Object> createEnvironmentMap(Environment env) { |
| Map<String, Object> templateRoot = new HashMap<String, Object>(); |
| Set<String> varNames = null; |
| try { |
| varNames = UtilGenerics.checkSet(env.getKnownVariableNames()); |
| } catch (TemplateModelException e1) { |
| Debug.logError(e1, "Error getting FreeMarker variable names, will not put pass current context on to sub-content", module); |
| } |
| if (varNames != null) { |
| for (String varName: varNames) { |
| templateRoot.put(varName, FreeMarkerWorker.getWrappedObject(varName, env)); |
| } |
| } |
| return templateRoot; |
| } |
| |
| public static void saveContextValues(Map<String, Object> context, String [] saveKeyNames, Map<String, Object> saveMap) { |
| for (String key: saveKeyNames) { |
| Object o = context.get(key); |
| if (o instanceof Map<?, ?>) { |
| o = UtilMisc.makeMapWritable(UtilGenerics.checkMap(o)); |
| } else if (o instanceof List<?>) { |
| o = UtilMisc.makeListWritable(UtilGenerics.checkList(o)); |
| } |
| saveMap.put(key, o); |
| } |
| } |
| |
| public static Map<String, Object> saveValues(Map<String, Object> context, String [] saveKeyNames) { |
| Map<String, Object> saveMap = new HashMap<String, Object>(); |
| for (String key: saveKeyNames) { |
| Object o = context.get(key); |
| if (o instanceof Map<?, ?>) { |
| o = UtilMisc.makeMapWritable(UtilGenerics.checkMap(o)); |
| } else if (o instanceof List<?>) { |
| o = UtilMisc.makeListWritable(UtilGenerics.checkList(o)); |
| } |
| saveMap.put(key, o); |
| } |
| return saveMap; |
| } |
| |
| public static void reloadValues(Map<String, Object> context, Map<String, Object> saveValues, Environment env) { |
| for (Map.Entry<String, Object> entry: saveValues.entrySet()) { |
| String key = entry.getKey(); |
| Object o = entry.getValue(); |
| if (o instanceof Map<?, ?>) { |
| context.put(key, UtilMisc.makeMapWritable(UtilGenerics.checkMap(o))); |
| } else if (o instanceof List<?>) { |
| List<Object> list = new ArrayList<Object>(); |
| list.addAll(UtilGenerics.checkList(o)); |
| context.put(key, list); |
| } else { |
| context.put(key, o); |
| } |
| env.setVariable(key, autoWrap(o, env)); |
| } |
| } |
| |
| public static void removeValues(Map<String, ?> context, String... removeKeyNames) { |
| for (String key: removeKeyNames) { |
| context.remove(key); |
| } |
| } |
| |
| public static void overrideWithArgs(Map<String, Object> ctx, Map<String, Object> args) { |
| for (Map.Entry<String, Object> entry: args.entrySet()) { |
| String key = entry.getKey(); |
| Object obj = entry.getValue(); |
| if (obj != null) { |
| if (obj == TemplateModel.NOTHING) { |
| ctx.put(key, null); |
| } else { |
| Object unwrappedObj = unwrap(obj); |
| if (unwrappedObj == null) { |
| unwrappedObj = obj; |
| } |
| ctx.put(key, unwrappedObj.toString()); |
| } |
| } else { |
| ctx.put(key, null); |
| } |
| } |
| } |
| |
| public static void getSiteParameters(HttpServletRequest request, Map<String, Object> ctx) { |
| if (request == null) { |
| return; |
| } |
| if (ctx == null) { |
| throw new IllegalArgumentException("Error in getSiteParameters, context/ctx cannot be null"); |
| } |
| ServletContext servletContext = request.getSession().getServletContext(); |
| String rootDir = (String)ctx.get("rootDir"); |
| String webSiteId = (String)ctx.get("webSiteId"); |
| String https = (String)ctx.get("https"); |
| if (UtilValidate.isEmpty(rootDir)) { |
| rootDir = servletContext.getRealPath("/"); |
| ctx.put("rootDir", rootDir); |
| } |
| if (UtilValidate.isEmpty(webSiteId)) { |
| webSiteId = (String) servletContext.getAttribute("webSiteId"); |
| ctx.put("webSiteId", webSiteId); |
| } |
| if (UtilValidate.isEmpty(https)) { |
| https = (String) servletContext.getAttribute("https"); |
| ctx.put("https", https); |
| } |
| } |
| |
| public static TemplateModel autoWrap(Object obj, Environment env) { |
| TemplateModel templateModelObj = null; |
| try { |
| templateModelObj = getDefaultOfbizWrapper().wrap(obj); |
| } catch (TemplateModelException e) { |
| throw new RuntimeException(e.getMessage()); |
| } |
| return templateModelObj; |
| } |
| |
| /* |
| * Custom TemplateLoader for Freemarker to locate templates by resource identifier |
| * following the format: |
| * component://componentname/path/to/some/file.ftl |
| */ |
| static class FlexibleTemplateLoader extends URLTemplateLoader { |
| @Override |
| protected URL getURL(String name) { |
| if (name != null && name.startsWith("delegator:")) return null; // this is a template stored in the database |
| URL locationUrl = null; |
| try { |
| locationUrl = FlexibleLocation.resolveLocation(name); |
| } catch (Exception e) { |
| Debug.logWarning("Unable to locate the template: " + name, module); |
| } |
| return locationUrl != null && new File(locationUrl.getFile()).exists()? locationUrl: null; |
| } |
| } |
| |
| /** |
| * OFBiz specific TemplateExceptionHandler. |
| */ |
| static class OFBizTemplateExceptionHandler implements TemplateExceptionHandler { |
| public void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException { |
| try { |
| out.write(te.getMessage()); |
| Debug.logError(te, module); |
| } catch (IOException e) { |
| Debug.logError(e, module); |
| } |
| } |
| } |
| } |