001/*
002 * (C) Copyright 2006-2019 Nuxeo (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    @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            @SuppressWarnings("resource") // BlockWriter chaining makes close() hazardous
186            BlockWriter bw = new BlockWriter(temp.getName(), "", new BlockWriterRegistry());
187            Environment env = temp.createProcessingEnvironment(input, bw, wrapper);
188            env.process();
189            bw.copyTo(writer);
190        } catch (SocketException e) {
191            log.debug("Output closed while rendering " + template);
192        } catch (IOException | TemplateException e) {
193            throw new RenderingException(e);
194        }
195    }
196
197    @Override
198    public void flushCache() {
199        cfg.clearTemplateCache();
200    }
201
202}