001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.util;
029
030import org.opencms.ade.configuration.CmsADEConfigData;
031import org.opencms.file.CmsFile;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.flex.CmsFlexController;
035import org.opencms.i18n.CmsMessageContainer;
036import org.opencms.jsp.CmsJspTagEditable;
037import org.opencms.main.CmsException;
038import org.opencms.main.CmsRuntimeException;
039import org.opencms.main.OpenCms;
040import org.opencms.util.CmsCollectionsGenericWrapper;
041import org.opencms.util.CmsHtmlValidator;
042import org.opencms.util.CmsStringUtil;
043import org.opencms.xml.containerpage.CmsContainerElementBean;
044import org.opencms.xml.containerpage.CmsFlexFormatterBean;
045import org.opencms.xml.containerpage.CmsMacroFormatterBean;
046import org.opencms.xml.containerpage.I_CmsFormatterBean;
047
048import java.io.IOException;
049import java.util.Date;
050import java.util.HashMap;
051import java.util.Locale;
052import java.util.Map;
053import java.util.Map.Entry;
054
055import javax.servlet.http.HttpServletRequest;
056import javax.servlet.jsp.PageContext;
057
058import org.stringtemplate.v4.DateRenderer;
059import org.stringtemplate.v4.ST;
060import org.stringtemplate.v4.STGroup;
061import org.stringtemplate.v4.compiler.CompiledST;
062import org.stringtemplate.v4.compiler.FormalArgument;
063
064/**
065 * Renderer for string templates.<p>
066 */
067public class CmsStringTemplateRenderer {
068
069    /** The error display HTML. */
070    public static final String ERROR_DISPLAY = "<div class='oc-fomatter-error'>\n"
071        + "<div class='oc-formatter-error-head'>%1$s</div>\n"
072        + "<div class='oc-formatter-error-body'>\n"
073        + "<div class='oc-formatter-error-source'>%2$s</div>\n"
074        + "<div class='oc-formatter-error-message'>%3$s</div>\n"
075        + "</div>\n</div>";
076
077    /** The error display HTML, including error details. */
078    public static final String ERROR_DISPLAY_WITH_DETAILS = "<div class='oc-fomatter-error'>\n"
079        + "<div class='oc-formatter-error-head'>%1$s</div>\n"
080        + "<div class='oc-formatter-error-body'>\n"
081        + "<div class='oc-formatter-error-source'>%2$s</div>\n"
082        + "<div class='oc-formatter-error-message'>%3$s</div>\n"
083        + "<div class='oc-formatter-error-details'><pre>%4$s</pre></div>\n"
084        + "</div>\n</div>";
085
086    /** Key to access object function wrapper. */
087    public static final String KEY_FUNCTIONS = "fn";
088
089    /** Key to access element settings. */
090    public static final String KEY_SETTINGS = "settings";
091
092    /** The current cms context. */
093    private CmsObject m_cms;
094
095    /** The page context. */
096    private PageContext m_context;
097
098    /** The JSP context bean. */
099    private CmsJspStandardContextBean m_contextBean;
100
101    /** The element to render. */
102    private CmsContainerElementBean m_element;
103
104    /** The request. */
105    private HttpServletRequest m_request;
106
107    /**
108     * Constructor.<p>
109     *
110     * @param context the page context
111     * @param req the request
112     */
113    public CmsStringTemplateRenderer(PageContext context, HttpServletRequest req) {
114
115        m_context = context;
116        m_request = req;
117        CmsFlexController controller = CmsFlexController.getController(req);
118        if (controller == null) {
119            handleMissingFlexController();
120            return;
121        }
122        m_cms = controller.getCmsObject();
123        m_contextBean = CmsJspStandardContextBean.getInstance(m_request);
124        m_element = m_contextBean.getElement();
125    }
126
127    /**
128     * Renders the given string template.<p>
129     *
130     * @param cms the cms context
131     * @param template the template
132     * @param content the content
133     * @param contextObjects additional context objects made available to the template
134     *
135     * @return the rendering result
136     */
137    public static String renderTemplate(
138        CmsObject cms,
139        String template,
140        CmsJspContentAccessBean content,
141        Map<String, Object> contextObjects) {
142
143        return renderTemplateInternal(cms, template, content, contextObjects);
144    }
145
146    /**
147     * Renders the given string template.<p>
148     *
149     * @param cms the cms context
150     * @param template the template
151     * @param contentValue the content value (part of a content)
152     * @param contextObjects additional context objects made available to the template
153     *
154     * @return the rendering result
155     */
156    public static String renderTemplate(
157        CmsObject cms,
158        String template,
159        CmsJspContentAccessValueWrapper contentValue,
160        Map<String, Object> contextObjects) {
161
162        return renderTemplateInternal(cms, template, contentValue, contextObjects);
163    }
164
165    /**
166     * Renders the given string template.<p>
167     *
168     * @param cms the cms context
169     * @param template the template
170     * @param content the content
171     * @param contextObjects additional context objects made available to the template
172     *
173     * @return the rendering result
174     */
175    public static String renderTemplate(
176        CmsObject cms,
177        String template,
178        CmsResource content,
179        Map<String, Object> contextObjects) {
180
181        return renderTemplate(cms, template, new CmsJspContentAccessBean(cms, content), contextObjects);
182    }
183
184    /**
185     * Renders the given string template.<p>
186     *
187     * @param cms the cms context
188     * @param template the template
189     * @param content the content
190     * @param contextObjects additional context objects made available to the template
191     * @param pathPrefix the path to the content part that should be accessible as content to the string template.
192     *
193     * @return the rendering result
194     */
195    public static String renderTemplate(
196        CmsObject cms,
197        String template,
198        CmsResource content,
199        Map<String, Object> contextObjects,
200        String pathPrefix) {
201
202        CmsJspContentAccessBean contentBean = new CmsJspContentAccessBean(cms, content);
203        return (null != pathPrefix) && !pathPrefix.isEmpty()
204        ? renderTemplate(cms, template, contentBean.getValue().get(pathPrefix), contextObjects)
205        : renderTemplate(cms, template, contentBean, contextObjects);
206    }
207
208    /**
209     * Wraps the element settings with access wrappers.<p>
210     *
211     * @param cms the current OpenCms user context
212     * @param settings the settings to wrap
213     *
214     * @return the element settings wrapped in access wrappers
215     */
216    public static Map<String, CmsJspObjectValueWrapper> wrapSettings(CmsObject cms, Map<String, String> settings) {
217
218        Map<String, CmsJspObjectValueWrapper> wrappedSettings = null;
219        if (settings != null) {
220            wrappedSettings = new HashMap<String, CmsJspObjectValueWrapper>(settings.size());
221            for (Entry<String, String> setting : settings.entrySet()) {
222                wrappedSettings.put(setting.getKey(), CmsJspObjectValueWrapper.createWrapper(cms, setting.getValue()));
223            }
224        }
225        return wrappedSettings;
226    }
227
228    /**
229     * Renders the given string template.<p>
230     *
231     * @param cms the cms context
232     * @param template the template
233     * @param content the content
234     * @param contextObjects additional context objects made available to the template
235     *
236     * @return the rendering result
237     */
238    private static String renderTemplateInternal(
239        CmsObject cms,
240        String template,
241        Object content,
242        Map<String, Object> contextObjects) {
243
244        STGroup group = new STGroup('%', '%');
245        group.registerRenderer(Date.class, new DateRenderer());
246        CompiledST cST = group.defineTemplate("main", template);
247        cST.addArg(new FormalArgument("content"));
248        if (contextObjects != null) {
249            for (Entry<String, Object> entry : contextObjects.entrySet()) {
250                cST.addArg(new FormalArgument(entry.getKey()));
251            }
252        }
253        ST st = group.getInstanceOf("main");
254        st.add("content", content);
255        if (contextObjects != null) {
256            for (Entry<String, Object> entry : contextObjects.entrySet()) {
257                st.add(entry.getKey(), entry.getValue());
258            }
259        }
260        return st.render(cms.getRequestContext().getLocale());
261    }
262
263    /**
264     * Renders the requested element content with the flex formatter string template.<p>
265     *
266     * @throws IOException in case writing to to page context out fails
267     */
268    @SuppressWarnings("resource")
269    public void render() throws IOException {
270
271        CmsADEConfigData adeConfig = OpenCms.getADEManager().lookupConfigurationWithCache(
272            m_cms,
273            m_cms.getRequestContext().getRootUri());
274        I_CmsFormatterBean formatterConfig = adeConfig.findFormatter(m_element.getFormatterId());
275        if (formatterConfig instanceof CmsFlexFormatterBean) {
276            CmsFlexFormatterBean config = (CmsFlexFormatterBean)formatterConfig;
277            String template = config.getStringTemplate();
278            if (m_element.isInMemoryOnly()) {
279                if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(config.getPlaceholderStringTemplate())) {
280                    template = config.getPlaceholderStringTemplate();
281                }
282                if (config.getDefaultContentStructureId() != null) {
283                    try {
284                        CmsResource defaultContent = m_cms.readResource(
285                            ((CmsMacroFormatterBean)formatterConfig).getDefaultContentStructureId());
286                        CmsFile defaultFile = m_cms.readFile(defaultContent);
287                        m_element = new CmsContainerElementBean(
288                            defaultFile,
289                            m_element.getFormatterId(),
290                            m_element.getIndividualSettings(),
291                            true,
292                            m_element.editorHash(),
293                            m_element.isCreateNew());
294                    } catch (CmsException e) {
295                        //      LOG.error("Error reading default content for new resource", e);
296                    }
297                }
298            }
299            try {
300                Map<String, Object> context = new HashMap<String, Object>();
301                context.put(KEY_SETTINGS, wrapSettings(m_cms, m_element.getSettings()));
302                context.put(
303                    KEY_FUNCTIONS,
304                    CmsCollectionsGenericWrapper.createLazyMap(new CmsObjectFunctionTransformer(m_cms)));
305                String output = renderTemplate(m_cms, template, m_element.getResource(), context);
306                if (CmsJspTagEditable.isEditableRequest(m_request)) {
307                    CmsHtmlValidator validator = new CmsHtmlValidator();
308                    validator.validate(output);
309                    if (!validator.isBalanced()) {
310                        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
311                        String messages = "";
312                        for (CmsMessageContainer message : validator.getMessages()) {
313                            messages += message.key(locale) + "\n";
314                        }
315                        output = String.format(
316                            ERROR_DISPLAY_WITH_DETAILS,
317                            Messages.get().getBundle(locale).key(Messages.GUI_FORMATTER_RENDERING_ERROR_0),
318                            formatterConfig.getJspRootPath(),
319                            Messages.get().getBundle(locale).key(Messages.GUI_FORMATTER_RENDERING_NOT_WELL_FORMED_0),
320                            messages);
321                    } else if (validator.getRootElementCount() > 1) {
322                        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
323                        output = String.format(
324                            ERROR_DISPLAY,
325                            Messages.get().getBundle(locale).key(Messages.GUI_FORMATTER_RENDERING_ERROR_0),
326                            formatterConfig.getJspRootPath(),
327                            Messages.get().getBundle(locale).key(
328                                Messages.GUI_FORMATTER_RENDERING_MULTIPLE_ROOT_ELEMENTS_0));
329                    }
330
331                }
332                m_context.getOut().print(output);
333            } catch (Throwable t) {
334                if (CmsJspTagEditable.isEditableRequest(m_request)) {
335                    String stackTrace = "";
336                    for (StackTraceElement element : t.getStackTrace()) {
337                        stackTrace += element.toString() + "\n";
338                    }
339                    Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(m_cms);
340                    m_context.getOut().println(
341                        String.format(
342                            ERROR_DISPLAY_WITH_DETAILS,
343                            Messages.get().getBundle(locale).key(Messages.GUI_FORMATTER_RENDERING_ERROR_0),
344                            formatterConfig.getJspRootPath(),
345                            t.getMessage(),
346                            stackTrace));
347                }
348            }
349        }
350    }
351
352    /**
353     * This method is called when the flex controller can not be found during initialization.<p>
354     *
355     * Override this if you are reusing old workplace classes in a context where no flex controller is available.
356     */
357    private void handleMissingFlexController() {
358
359        // controller not found - this request was not initialized properly
360        throw new CmsRuntimeException(
361            org.opencms.jsp.Messages.get().container(
362                org.opencms.jsp.Messages.ERR_MISSING_CMS_CONTROLLER_1,
363                CmsMacroFormatterResolver.class.getName()));
364    }
365
366}