/*
* 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 org.apache.wicket.velocity.markup.html;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Map;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
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.resource.ResourceUtil;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.IStringResourceStream;
import org.apache.wicket.util.resource.StringResourceStream;
import org.apache.wicket.util.string.Strings;
/**
* Panel that displays the result of rendering a <a
* href="http://jakarta.apache.org/velocity">Velocity</a> template. The template itself can be any
* {@link StringResourceStream} implementation, of which there are a number of convenient
* implementations in the {@link org.apache.wicket.util} package. The model can be any normal
* {@link Map}, which will be used to create the {@link VelocityContext}.
* <p>
* <b>Note:</b> Be sure to properly initialize the Velocity engine before using
* {@link VelocityPanel }.
* </p>
*/
public abstract class VelocityPanel extends Panel
implements
IMarkupResourceStreamProvider,
IMarkupCacheKeyProvider
{
private static final long serialVersionUID = 1L;
/**
* Convenience factory method to create a {@link VelocityPanel} instance with a given
* {@link IStringResourceStream template resource}.
*
* @param id
* Component id
* @param model
* optional model for variable substitution.
* @param templateResource
* The template resource
* @return an instance of {@link VelocityPanel}
*/
@SuppressWarnings("rawtypes")
public static VelocityPanel forTemplateResource(final String id,
final IModel<? extends Map> model, final IResourceStream templateResource)
{
if (templateResource == null)
{
throw new IllegalArgumentException("argument templateResource must be not null");
}
return new VelocityPanel(id, model)
{
private static final long serialVersionUID = 1L;
@Override
protected IResourceStream getTemplateResource()
{
return templateResource;
}
};
}
private transient String stackTraceAsString;
private transient String evaluatedTemplate;
/**
* Construct.
*
* @param id
* Component id
* @param templateResource
* The velocity template as a string resource
* @param model
* Model with variables that can be substituted by Velocity.
*/
@SuppressWarnings("rawtypes")
public VelocityPanel(final String id, final IModel<? extends Map> model)
{
super(id, model);
}
/**
* Gets a reader for the velocity template.
*
* @return reader for the velocity template
*/
private Reader getTemplateReader()
{
final IResourceStream resource = getTemplateResource();
if (resource == null)
{
throw new IllegalArgumentException("getTemplateResource must return a resource");
}
final String template = ResourceUtil.readString(resource);
if (template != null)
{
return new StringReader(template);
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
{
if (!Strings.isEmpty(stackTraceAsString))
{
// TODO: only display the velocity 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 (!throwVelocityExceptions())
{
// 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.
*/
protected boolean escapeHtml()
{
return false;
}
/**
* Returns the template resource passed to the constructor.
*
* @return The template resource
*/
protected abstract IResourceStream getTemplateResource();
/**
* Evaluates the template and returns the result.
*
* @param templateReader
* used to read the template
* @return the result of evaluating the velocity template
*/
private String evaluateVelocityTemplate(final Reader templateReader)
{
if (evaluatedTemplate == null)
{
// Get model as a map
@SuppressWarnings("rawtypes")
final Map map = (Map)getDefaultModelObject();
// create a Velocity context object using the model if set
final VelocityContext ctx = new VelocityContext(map);
// 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
final String logTag = getId();
try
{
// execute the velocity script and capture the output in writer
Velocity.evaluate(ctx, writer, logTag, templateReader);
// replace the tag's body the Velocity 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 (Exception 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.
*/
protected boolean parseGeneratedMarkup()
{
return false;
}
/**
* Whether any velocity 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.
*
* @return Whether any velocity exceptions should be thrown or trapped. The default is false.
*/
protected boolean throwVelocityExceptions()
{
return false;
}
/**
* {@inheritDoc}
*/
@Override
public final IResourceStream getMarkupResourceStream(final MarkupContainer container,
final Class<?> containerClass)
{
Reader reader = getTemplateReader();
if (reader == null)
{
throw new WicketRuntimeException("could not find velocity template for panel: " + this);
}
// evaluate the template and return a new StringResourceStream
StringBuilder sb = new StringBuilder();
sb.append("<wicket:panel>");
sb.append(evaluateVelocityTemplate(reader));
sb.append("</wicket:panel>");
return new StringResourceStream(sb.toString());
}
/**
* {@inheritDoc}
*/
@Override
public final String getCacheKey(final MarkupContainer container, final Class<?> containerClass)
{
// don't cache the evaluated template
return null;
}
/**
* {@inheritDoc}
*/
@Override
protected void onDetach()
{
super.onDetach();
stackTraceAsString = null;
evaluatedTemplate = null;
}
}