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}