blob: f03b222a25f8076444c40750aa11c4d2ccef0bac [file] [log] [blame]
package org.apache.turbine.services.velocity;
/* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2001-2003 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" and
* "Apache Turbine" must not be used to endorse or promote products
* derived from this software without prior written permission. For
* written permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* "Apache Turbine", nor may "Apache" appear in their name, without
* prior written permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletConfig;
import org.apache.commons.collections.ExtendedProperties;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.turbine.Turbine;
import org.apache.turbine.pipeline.PipelineData;
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.pull.PullService;
import org.apache.turbine.services.pull.TurbinePull;
import org.apache.turbine.services.template.BaseTemplateEngineService;
import org.apache.turbine.util.RunData;
import org.apache.turbine.util.TurbineException;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.MethodExceptionEventHandler;
import org.apache.velocity.context.Context;
import org.apache.velocity.runtime.log.SimpleLog4JLogSystem;
/**
* This is a Service that can process Velocity templates from within a
* Turbine Screen. It is used in conjunction with the templating service
* as a Templating Engine for templates ending in "vm". It registers
* itself as translation engine with the template service and gets
* accessed from there. After configuring it in your properties, it
* should never be necessary to call methods from this service directly.
*
* Here's an example of how you might use it from a
* screen:<br>
*
* <code>
* Context context = TurbineVelocity.getContext(data);<br>
* context.put("message", "Hello from Turbine!");<br>
* String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
* data.getPage().getBody().addElement(results);<br>
* </code>
*
* @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
* @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
* @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
* @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
* @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
* @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
* @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
* @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
* @version $Id$
*/
public class TurbineVelocityService
extends BaseTemplateEngineService
implements VelocityService,
MethodExceptionEventHandler
{
/** The generic resource loader path property in velocity.*/
private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
/** Default character set to use if not specified in the RunData object. */
private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
/** The prefix used for URIs which are of type <code>jar</code>. */
private static final String JAR_PREFIX = "jar:";
/** The prefix used for URIs which are of type <code>absolute</code>. */
private static final String ABSOLUTE_PREFIX = "file://";
/** Logging */
private static Log log = LogFactory.getLog(TurbineVelocityService.class);
/** Is the pullModelActive? */
private boolean pullModelActive = false;
/** Shall we catch Velocity Errors and report them in the log file? */
private boolean catchErrors = true;
/** Internal Reference to the pull Service */
private PullService pullService = null;
/**
* Load all configured components and initialize them. This is
* a zero parameter variant which queries the Turbine Servlet
* for its config.
*
* @throws InitializationException Something went wrong in the init
* stage
*/
public void init()
throws InitializationException
{
try
{
initVelocity();
// We can only load the Pull Model ToolBox
// if the Pull service has been listed in the TR.props
// and the service has successfully been initialized.
if (TurbinePull.isRegistered())
{
pullModelActive = true;
pullService = TurbinePull.getService();
log.debug("Activated Pull Tools");
}
// Register with the template service.
registerConfiguration(VelocityService.VELOCITY_EXTENSION);
setInit(true);
}
catch (Exception e)
{
throw new InitializationException(
"Failed to initialize TurbineVelocityService", e);
}
}
/**
* Inits the service using servlet parameters to obtain path to the
* configuration file.
*
* @param config The ServletConfiguration from Turbine
*
* @throws InitializationException Something went wrong when starting up.
* @deprecated use init() instead.
*/
public void init(ServletConfig config)
throws InitializationException
{
init();
}
/**
* Create a Context object that also contains the globalContext.
*
* @return A Context object.
*/
public Context getContext()
{
Context globalContext =
pullModelActive ? pullService.getGlobalContext() : null;
Context ctx = new VelocityContext(globalContext);
return ctx;
}
/**
* This method returns a new, empty Context object.
*
* @return A Context Object.
*/
public Context getNewContext()
{
Context ctx = new VelocityContext();
// Attach an Event Cartridge to it, so we get exceptions
// while invoking methods from the Velocity Screens
EventCartridge ec = new EventCartridge();
ec.addEventHandler(this);
ec.attachToContext(ctx);
return ctx;
}
/**
* MethodException Event Cartridge handler
* for Velocity.
*
* It logs an execption thrown by the velocity processing
* on error level into the log file
*
* @param clazz The class that threw the exception
* @param method The Method name that threw the exception
* @param e The exception that would've been thrown
* @return A valid value to be used as Return value
* @throws Exception We threw the exception further up
*/
public Object methodException(Class clazz, String method, Exception e)
throws Exception
{
log.error("Class " + clazz.getName() + "." + method + " threw Exception", e);
if (!catchErrors)
{
throw e;
}
return "[Turbine caught an Error here. Look into the turbine.log for further information]";
}
/**
* Create a Context from the RunData object. Adds a pointer to
* the RunData object to the VelocityContext so that RunData
* is available in the templates.
* @deprecated. Use PipelineData version.
* @param data The Turbine RunData object.
* @return A clone of the WebContext needed by Velocity.
*/
public Context getContext(RunData data)
{
// Attempt to get it from the data first. If it doesn't
// exist, create it and then stuff it into the data.
Context context = (Context)
data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
if (context == null)
{
context = getContext();
context.put(VelocityService.RUNDATA_KEY, data);
if (pullModelActive)
{
// Populate the toolbox with request scope, session scope
// and persistent scope tools (global tools are already in
// the toolBoxContent which has been wrapped to construct
// this request-specific context).
pullService.populateContext(context, data);
}
data.getTemplateInfo().setTemplateContext(
VelocityService.CONTEXT, context);
}
return context;
}
/**
* Create a Context from the PipelineData object. Adds a pointer to
* the RunData object to the VelocityContext so that RunData
* is available in the templates.
*
* @param data The Turbine RunData object.
* @return A clone of the WebContext needed by Velocity.
*/
public Context getContext(PipelineData pipelineData)
{
//Map runDataMap = (Map)pipelineData.get(RunData.class);
RunData data = (RunData)pipelineData;
// Attempt to get it from the data first. If it doesn't
// exist, create it and then stuff it into the data.
Context context = (Context)
data.getTemplateInfo().getTemplateContext(VelocityService.CONTEXT);
if (context == null)
{
context = getContext();
context.put(VelocityService.RUNDATA_KEY, data);
// we will add both data and pipelineData to the context.
context.put(VelocityService.PIPELINEDATA_KEY, pipelineData);
if (pullModelActive)
{
// Populate the toolbox with request scope, session scope
// and persistent scope tools (global tools are already in
// the toolBoxContent which has been wrapped to construct
// this request-specific context).
pullService.populateContext(context, pipelineData);
}
data.getTemplateInfo().setTemplateContext(
VelocityService.CONTEXT, context);
}
return context;
}
/**
* Process the request and fill in the template with the values
* you set in the Context.
*
* @param context The populated context.
* @param filename The file name of the template.
* @return The process template as a String.
*
* @throws TurbineException Any exception trown while processing will be
* wrapped into a TurbineException and rethrown.
*/
public String handleRequest(Context context, String filename)
throws TurbineException
{
String results = null;
ByteArrayOutputStream bytes = null;
OutputStreamWriter writer = null;
String charset = getCharSet(context);
try
{
bytes = new ByteArrayOutputStream();
writer = new OutputStreamWriter(bytes, charset);
executeRequest(context, filename, writer);
writer.flush();
results = bytes.toString(charset);
}
catch (Exception e)
{
renderingError(filename, e);
}
finally
{
try
{
if (bytes != null)
{
bytes.close();
}
}
catch (IOException ignored)
{
// do nothing.
}
}
return results;
}
/**
* Process the request and fill in the template with the values
* you set in the Context.
*
* @param context A Context.
* @param filename A String with the filename of the template.
* @param output A OutputStream where we will write the process template as
* a String.
*
* @throws TurbineException Any exception trown while processing will be
* wrapped into a TurbineException and rethrown.
*/
public void handleRequest(Context context, String filename,
OutputStream output)
throws TurbineException
{
String charset = getCharSet(context);
OutputStreamWriter writer = null;
try
{
writer = new OutputStreamWriter(output, charset);
executeRequest(context, filename, writer);
}
catch (Exception e)
{
renderingError(filename, e);
}
finally
{
try
{
if (writer != null)
{
writer.flush();
}
}
catch (Exception ignored)
{
// do nothing.
}
}
}
/**
* Process the request and fill in the template with the values
* you set in the Context.
*
* @param context A Context.
* @param filename A String with the filename of the template.
* @param writer A Writer where we will write the process template as
* a String.
*
* @throws TurbineException Any exception trown while processing will be
* wrapped into a TurbineException and rethrown.
*/
public void handleRequest(Context context, String filename, Writer writer)
throws TurbineException
{
try
{
executeRequest(context, filename, writer);
}
catch (Exception e)
{
renderingError(filename, e);
}
finally
{
try
{
if (writer != null)
{
writer.flush();
}
}
catch (Exception ignored)
{
// do nothing.
}
}
}
/**
* Process the request and fill in the template with the values
* you set in the Context. Apply the character and template
* encodings from RunData to the result.
*
* @param context A Context.
* @param filename A String with the filename of the template.
* @param writer A OutputStream where we will write the process template as
* a String.
*
* @throws Exception A problem occured.
*/
private void executeRequest(Context context, String filename,
Writer writer)
throws Exception
{
String encoding = getEncoding(context);
if (encoding == null)
{
encoding = DEFAULT_CHAR_SET;
}
Velocity.mergeTemplate(filename, encoding, context, writer);
}
/**
* Retrieve the required charset from the Turbine RunData in the context
*
* @param context A Context.
* @return The character set applied to the resulting String.
*/
private String getCharSet(Context context)
{
String charset = null;
Object data = context.get(VelocityService.RUNDATA_KEY);
if ((data != null) && (data instanceof RunData))
{
charset = ((RunData) data).getCharSet();
}
return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET : charset;
}
/**
* Retrieve the required encoding from the Turbine RunData in the context
*
* @param context A Context.
* @return The encoding applied to the resulting String.
*/
private String getEncoding(Context context)
{
String encoding = null;
Object data = context.get(VelocityService.RUNDATA_KEY);
if ((data != null) && (data instanceof RunData))
{
encoding = ((RunData) data).getTemplateEncoding();
}
return encoding;
}
/**
* Macro to handle rendering errors.
*
* @param filename The file name of the unrenderable template.
* @param e The error.
*
* @exception TurbineException Thrown every time. Adds additional
* information to <code>e</code>.
*/
private static final void renderingError(String filename, Exception e)
throws TurbineException
{
String err = "Error rendering Velocity template: " + filename;
log.error(err, e);
throw new TurbineException(err, e);
}
/**
* Setup the velocity runtime by using a subset of the
* Turbine configuration which relates to velocity.
*
* @exception Exception An Error occured.
*/
private synchronized void initVelocity()
throws Exception
{
// Get the configuration for this service.
Configuration conf = getConfiguration();
catchErrors = conf.getBoolean(CATCH_ERRORS_KEY, CATCH_ERRORS_DEFAULT);
conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
SimpleLog4JLogSystem.class.getName());
conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
+ ".log4j.category", "velocity");
Velocity.setExtendedProperties(createVelocityProperties(conf));
Velocity.init();
}
/**
* This method generates the Extended Properties object necessary
* for the initialization of Velocity. It also converts the various
* resource loader pathes into webapp relative pathes. It also
*
* @param conf The Velocity Service configuration
*
* @return An ExtendedProperties Object for Velocity
*
* @throws Exception If a problem occured while converting the properties.
*/
public ExtendedProperties createVelocityProperties(Configuration conf)
throws Exception
{
// This bugger is public, because we want to run some Unit tests
// on it.
ExtendedProperties veloConfig = new ExtendedProperties();
// Fix up all the template resource loader pathes to be
// webapp relative. Copy all other keys verbatim into the
// veloConfiguration.
for (Iterator i = conf.getKeys(); i.hasNext();)
{
String key = (String) i.next();
if (!key.endsWith(RESOURCE_LOADER_PATH))
{
Object value = conf.getProperty(key);
if (value instanceof List) {
for (Iterator itr = ((List)value).iterator(); itr.hasNext();)
{
veloConfig.addProperty(key, itr.next());
}
}
else
{
veloConfig.addProperty(key, value);
}
continue; // for()
}
List paths = conf.getList(key, null);
if (paths == null)
{
// We don't copy this into VeloProperties, because
// null value is unhealthy for the ExtendedProperties object...
continue; // for()
}
Velocity.clearProperty(key);
// Translate the supplied pathes given here.
// the following three different kinds of
// pathes must be translated to be webapp-relative
//
// jar:file://path-component!/entry-component
// file://path-component
// path/component
for (Iterator j = paths.iterator(); j.hasNext();)
{
String path = (String) j.next();
log.debug("Translating " + path);
if (path.startsWith(JAR_PREFIX))
{
// skip jar: -> 4 chars
if (path.substring(4).startsWith(ABSOLUTE_PREFIX))
{
// We must convert up to the jar path separator
int jarSepIndex = path.indexOf("!/");
// jar:file:// -> skip 11 chars
path = (jarSepIndex < 0)
? Turbine.getRealPath(path.substring(11))
// Add the path after the jar path separator again to the new url.
: (Turbine.getRealPath(path.substring(11, jarSepIndex)) + path.substring(jarSepIndex));
log.debug("Result (absolute jar path): " + path);
}
}
else if(path.startsWith(ABSOLUTE_PREFIX))
{
// skip file:// -> 7 chars
path = Turbine.getRealPath(path.substring(7));
log.debug("Result (absolute URL Path): " + path);
}
// Test if this might be some sort of URL that we haven't encountered yet.
else if(path.indexOf("://") < 0)
{
path = Turbine.getRealPath(path);
log.debug("Result (normal fs reference): " + path);
}
log.debug("Adding " + key + " -> " + path);
// Re-Add this property to the configuration object
veloConfig.addProperty(key, path);
}
}
return veloConfig;
}
/**
* Find out if a given template exists. Velocity
* will do its own searching to determine whether
* a template exists or not.
*
* @param template String template to search for
* @return True if the template can be loaded by Velocity
*/
public boolean templateExists(String template)
{
return Velocity.templateExists(template);
}
/**
* Performs post-request actions (releases context
* tools back to the object pool).
*
* @param context a Velocity Context
*/
public void requestFinished(Context context)
{
if (pullModelActive)
{
pullService.releaseTools(context);
}
}
}