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