001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * bstefanescu 011 * 012 * $Id$ 013 */ 014 015package org.nuxeo.ecm.platform.rendering.fm; 016 017import java.io.IOException; 018import java.io.Writer; 019import java.net.SocketException; 020import java.util.ResourceBundle; 021 022import org.apache.commons.logging.Log; 023import org.apache.commons.logging.LogFactory; 024import org.nuxeo.ecm.platform.rendering.api.RenderingEngine; 025import org.nuxeo.ecm.platform.rendering.api.RenderingException; 026import org.nuxeo.ecm.platform.rendering.api.ResourceLocator; 027import org.nuxeo.ecm.platform.rendering.api.View; 028import org.nuxeo.ecm.platform.rendering.fm.adapters.DocumentObjectWrapper; 029import org.nuxeo.ecm.platform.rendering.fm.extensions.BlockDirective; 030import org.nuxeo.ecm.platform.rendering.fm.extensions.BlockWriter; 031import org.nuxeo.ecm.platform.rendering.fm.extensions.BlockWriterRegistry; 032import org.nuxeo.ecm.platform.rendering.fm.extensions.DocRefMethod; 033import org.nuxeo.ecm.platform.rendering.fm.extensions.ExtendsDirective; 034import org.nuxeo.ecm.platform.rendering.fm.extensions.FormatDate; 035import org.nuxeo.ecm.platform.rendering.fm.extensions.LocaleMessagesMethod; 036import org.nuxeo.ecm.platform.rendering.fm.extensions.MessagesMethod; 037import org.nuxeo.ecm.platform.rendering.fm.extensions.NewMethod; 038import org.nuxeo.ecm.platform.rendering.fm.extensions.SuperBlockDirective; 039import org.nuxeo.ecm.platform.rendering.fm.i18n.ResourceComposite; 040 041import freemarker.core.Environment; 042import freemarker.template.Configuration; 043import freemarker.template.Template; 044import freemarker.template.TemplateException; 045import freemarker.template.TemplateModelException; 046 047/** 048 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 049 */ 050public class FreemarkerEngine implements RenderingEngine { 051 052 private static final Log log = LogFactory.getLog(FreemarkerEngine.class); 053 054 public static final String RENDERING_ENGINE_KEY = "NX_RENDERING_ENGINE"; 055 056 protected final Configuration cfg; 057 058 // the wrapper is not a singleton since it contains some info about the 059 // engine instance 060 // so we will have one wrapper per engine instance 061 protected final DocumentObjectWrapper wrapper; 062 063 protected final MessagesMethod messages = new MessagesMethod(null); 064 065 protected final LocaleMessagesMethod localeMessages = new LocaleMessagesMethod(null); 066 067 protected ResourceTemplateLoader loader; 068 069 public FreemarkerEngine() { 070 this(null, null); 071 } 072 073 public FreemarkerEngine(Configuration cfg, ResourceLocator locator) { 074 wrapper = new DocumentObjectWrapper(this); 075 this.cfg = cfg == null ? new Configuration() : cfg; 076 this.cfg.setWhitespaceStripping(true); 077 this.cfg.setLocalizedLookup(false); 078 this.cfg.setClassicCompatible(true); 079 this.cfg.setObjectWrapper(wrapper); 080 081 // Output encoding must not be left to null to make sure that the "?url" 082 // escape utility works consistently with the expected output charset. 083 // We hard-code it to UTF-8 as it's already hard-coded to UTF-8 in 084 // various other places where rendering is called (such as WebEngine's 085 // TemplateView or automation's FreemarkerRender). 086 // TODO: expose a public getEncoding method in the RenderingEngine 087 // interface and reuse it in the callers instead of hard coding the 088 // charset everywhere. 089 this.cfg.setOutputEncoding("UTF-8"); 090 091 // custom directives goes here 092 this.cfg.setSharedVariable("block", new BlockDirective()); 093 this.cfg.setSharedVariable("superBlock", new SuperBlockDirective()); 094 this.cfg.setSharedVariable("extends", new ExtendsDirective()); 095 this.cfg.setSharedVariable("docRef", new DocRefMethod()); 096 this.cfg.setSharedVariable("new", new NewMethod()); 097 this.cfg.setSharedVariable("message", messages); 098 this.cfg.setSharedVariable("lmessage", localeMessages); 099 this.cfg.setSharedVariable("formatDate", new FormatDate()); 100 101 this.cfg.setCustomAttribute(RENDERING_ENGINE_KEY, this); 102 setResourceLocator(locator); 103 } 104 105 /** 106 * set the resource bundle to be used with method message and lmessage. If the resourcebundle is not of the type 107 * ResourceComposite, lmessage will create a default ResourceComposite. 108 */ 109 @Override 110 public void setMessageBundle(ResourceBundle messages) { 111 this.messages.setBundle(messages); 112 if (messages instanceof ResourceComposite) { 113 localeMessages.setBundle((ResourceComposite) messages); 114 } 115 } 116 117 @Override 118 public ResourceBundle getMessageBundle() { 119 return messages.getBundle(); 120 } 121 122 @Override 123 public void setResourceLocator(ResourceLocator locator) { 124 loader = new ResourceTemplateLoader(locator); 125 cfg.setTemplateLoader(loader); 126 } 127 128 @Override 129 public ResourceLocator getResourceLocator() { 130 return loader.getLocator(); 131 } 132 133 public ResourceTemplateLoader getLoader() { 134 return loader; 135 } 136 137 @Override 138 public void setSharedVariable(String key, Object value) { 139 try { 140 cfg.setSharedVariable(key, value); 141 } catch (TemplateModelException e) { 142 log.error(e, e); 143 } 144 } 145 146 public DocumentObjectWrapper getObjectWrapper() { 147 return wrapper; 148 } 149 150 public Configuration getConfiguration() { 151 return cfg; 152 } 153 154 @Override 155 public View getView(String path) { 156 return new View(this, path); 157 } 158 159 @Override 160 public View getView(String path, Object object) { 161 return new View(this, path, object); 162 } 163 164 /** 165 * @param template 166 * @param input 167 * @param writer 168 * @param baseUrl a base URL used for resolving referenced files in extends directive. 169 * @throws RenderingException 170 */ 171 @Override 172 public void render(String template, Object input, Writer writer) throws RenderingException { 173 try { 174 /* 175 * A special method to get the absolute path as an URI to be used with freemarker since freemarker removes 176 * the leading / from the absolute path and the file cannot be resolved anymore In the case of URI like path 177 * freemarker is not modifying the path <p> 178 * @see TemplateCache#normalizeName() 179 * @see ResourceTemplateLoader#findTemplateSource() 180 */ 181 if (template.startsWith("/")) { 182 template = "fs://" + template; 183 } 184 Template temp = cfg.getTemplate(template); 185 BlockWriter bw = new BlockWriter(temp.getName(), "", new BlockWriterRegistry()); 186 Environment env = temp.createProcessingEnvironment(input, bw, wrapper); 187 env.process(); 188 bw.copyTo(writer); 189 } catch (SocketException e) { 190 log.debug("Output closed while rendering " + template); 191 } catch (IOException | TemplateException e) { 192 throw new RenderingException(e); 193 } 194 } 195 196 @Override 197 public void flushCache() { 198 cfg.clearTemplateCache(); 199 } 200 201}