/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.wicket.freemarker; import java.io.IOException; import java.io.StringWriter; import java.util.Map; import org.apache.wicket.MarkupContainer; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.IMarkupCacheKeyProvider; import org.apache.wicket.markup.IMarkupResourceStreamProvider; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.StringResourceStream; import org.apache.wicket.util.string.Strings; import com.gitblit.utils.StringUtils; import freemarker.template.Template; import freemarker.template.TemplateException; /** * This class allows FreeMarker to be used as a Wicket preprocessor or as a * snippet injector for something like a CMS. There are some cases where Wicket * is not flexible enough to generate content, especially when you need to generate * hybrid HTML/JS content outside the scope of Wicket. * * @author James Moger * */ @SuppressWarnings("unchecked") public class FreemarkerPanel extends Panel implements IMarkupResourceStreamProvider, IMarkupCacheKeyProvider { private static final long serialVersionUID = 1L; private final String template; private boolean parseGeneratedMarkup; private boolean escapeHtml; private boolean throwFreemarkerExceptions; private transient String stackTraceAsString; private transient String evaluatedTemplate; /** * Construct. * * @param id * Component id * @param template * The Freemarker template * @param values * values map that can be substituted by Freemarker. */ public FreemarkerPanel(final String id, String template, final Map<String, Object> values) { this(id, template, Model.ofMap(values)); } /** * Construct. * * @param id * Component id * @param templateResource * The Freemarker template as a string resource * @param model * Model with variables that can be substituted by Freemarker. */ public FreemarkerPanel(final String id, final String template, final IModel< ? extends Map<String, Object>> model) { super(id, model); this.template = template; } /** * Gets the Freemarker template. * * @return the Freemarker template */ private Template getTemplate() { if (StringUtils.isEmpty(template)) { throw new IllegalArgumentException("Template not specified!"); } try { return Freemarker.getTemplate(template); } catch (IOException e) { onException(e); } return null; } /** * @see org.apache.wicket.markup.html.panel.Panel#onComponentTagBody(org.apache.wicket.markup. * MarkupStream, org.apache.wicket.markup.ComponentTag) */ @Override protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { if (!Strings.isEmpty(stackTraceAsString)) { // TODO: only display the Freemarker error/stacktrace in development // mode? replaceComponentTagBody(markupStream, openTag, Strings .toMultilineMarkup(stackTraceAsString)); } else if (!parseGeneratedMarkup) { // check that no components have been added in case the generated // markup should not be // parsed if (size() > 0) { throw new WicketRuntimeException( "Components cannot be added if the generated markup should not be parsed."); } if (evaluatedTemplate == null) { // initialize evaluatedTemplate getMarkupResourceStream(null, null); } replaceComponentTagBody(markupStream, openTag, evaluatedTemplate); } else { super.onComponentTagBody(markupStream, openTag); } } /** * Either print or rethrow the throwable. * * @param exception * the cause * @param markupStream * the markup stream * @param openTag * the open tag */ private void onException(final Exception exception) { if (!throwFreemarkerExceptions) { // print the exception on the panel stackTraceAsString = Strings.toString(exception); } else { // rethrow the exception throw new WicketRuntimeException(exception); } } /** * Gets whether to escape HTML characters. * * @return whether to escape HTML characters. The default value is false. */ public void setEscapeHtml(boolean value) { this.escapeHtml = value; } /** * Evaluates the template and returns the result. * * @param templateReader * used to read the template * @return the result of evaluating the velocity template */ private String evaluateFreemarkerTemplate(Template template) { if (evaluatedTemplate == null) { // Get model as a map final Map<String, Object> map = (Map<String, Object>)getDefaultModelObject(); // create a writer for capturing the Velocity output StringWriter writer = new StringWriter(); // string to be used as the template name for log messages in case // of error try { // execute the Freemarker script and capture the output in writer Freemarker.evaluate(template, map, writer); // replace the tag's body the Freemarker output evaluatedTemplate = writer.toString(); if (escapeHtml) { // encode the result in order to get valid html output that // does not break the rest of the page evaluatedTemplate = Strings.escapeMarkup(evaluatedTemplate).toString(); } return evaluatedTemplate; } catch (IOException e) { onException(e); } catch (TemplateException e) { onException(e); } return null; } return evaluatedTemplate; } /** * Gets whether to parse the resulting Wicket markup. * * @return whether to parse the resulting Wicket markup. The default is false. */ public void setParseGeneratedMarkup(boolean value) { this.parseGeneratedMarkup = value; } /** * Whether any Freemarker exception should be trapped and displayed on the panel (false) or thrown * up to be handled by the exception mechanism of Wicket (true). The default is false, which * traps and displays any exception without having consequences for the other components on the * page. * <p> * Trapping these exceptions without disturbing the other components is especially useful in CMS * like applications, where 'normal' users are allowed to do basic scripting. On errors, you * want them to be able to have them correct them while the rest of the application keeps on * working. * </p> * * @return Whether any Freemarker exceptions should be thrown or trapped. The default is false. */ public void setThrowFreemarkerExceptions(boolean value) { this.throwFreemarkerExceptions = value; } /** * @see org.apache.wicket.markup.IMarkupResourceStreamProvider#getMarkupResourceStream(org.apache * .wicket.MarkupContainer, java.lang.Class) */ @Override public final IResourceStream getMarkupResourceStream(MarkupContainer container, Class< ? > containerClass) { Template template = getTemplate(); if (template == null) { throw new WicketRuntimeException("could not find Freemarker template for panel: " + this); } // evaluate the template and return a new StringResourceStream StringBuffer sb = new StringBuffer(); sb.append("<wicket:panel>"); sb.append(evaluateFreemarkerTemplate(template)); sb.append("</wicket:panel>"); return new StringResourceStream(sb.toString()); } /** * @see org.apache.wicket.markup.IMarkupCacheKeyProvider#getCacheKey(org.apache.wicket. * MarkupContainer, java.lang.Class) */ @Override public final String getCacheKey(MarkupContainer container, Class< ? > containerClass) { // don't cache the evaluated template return null; } /** * @see org.apache.wicket.Component#onDetach() */ @Override protected void onDetach() { super.onDetach(); stackTraceAsString = null; evaluatedTemplate = null; } }