001: package org.apache.turbine.services.velocity;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.io.ByteArrayOutputStream;
023: import java.io.IOException;
024: import java.io.OutputStream;
025: import java.io.OutputStreamWriter;
026: import java.io.Writer;
027: import java.util.Iterator;
028: import java.util.List;
029: import java.util.Vector;
030:
031: import javax.servlet.ServletConfig;
032:
033: import org.apache.commons.collections.ExtendedProperties;
034: import org.apache.commons.configuration.Configuration;
035: import org.apache.commons.lang.StringUtils;
036: import org.apache.commons.logging.Log;
037: import org.apache.commons.logging.LogFactory;
038: import org.apache.turbine.Turbine;
039: import org.apache.turbine.services.InitializationException;
040: import org.apache.turbine.services.pull.PullService;
041: import org.apache.turbine.services.pull.TurbinePull;
042: import org.apache.turbine.services.template.BaseTemplateEngineService;
043: import org.apache.turbine.util.RunData;
044: import org.apache.turbine.util.TurbineException;
045: import org.apache.velocity.VelocityContext;
046: import org.apache.velocity.app.Velocity;
047: import org.apache.velocity.app.event.EventCartridge;
048: import org.apache.velocity.app.event.MethodExceptionEventHandler;
049: import org.apache.velocity.context.Context;
050: import org.apache.velocity.runtime.log.Log4JLogChute;
051:
052: /**
053: * This is a Service that can process Velocity templates from within a
054: * Turbine Screen. It is used in conjunction with the templating service
055: * as a Templating Engine for templates ending in "vm". It registers
056: * itself as translation engine with the template service and gets
057: * accessed from there. After configuring it in your properties, it
058: * should never be necessary to call methods from this service directly.
059: *
060: * Here's an example of how you might use it from a
061: * screen:<br>
062: *
063: * <code>
064: * Context context = TurbineVelocity.getContext(data);<br>
065: * context.put("message", "Hello from Turbine!");<br>
066: * String results = TurbineVelocity.handleRequest(context,"helloWorld.vm");<br>
067: * data.getPage().getBody().addElement(results);<br>
068: * </code>
069: *
070: * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
071: * @author <a href="mailto:krzewski@e-point.pl">Rafal Krzewski</a>
072: * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
073: * @author <a href="mailto:sean@informage.ent">Sean Legassick</a>
074: * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
075: * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
076: * @version $Id: TurbineVelocityService.java 552336 2007-07-01 16:33:47Z tv $
077: */
078: public class TurbineVelocityService extends BaseTemplateEngineService
079: implements VelocityService, MethodExceptionEventHandler {
080: /** The generic resource loader path property in velocity.*/
081: private static final String RESOURCE_LOADER_PATH = ".resource.loader.path";
082:
083: /** Default character set to use if not specified in the RunData object. */
084: private static final String DEFAULT_CHAR_SET = "ISO-8859-1";
085:
086: /** The prefix used for URIs which are of type <code>jar</code>. */
087: private static final String JAR_PREFIX = "jar:";
088:
089: /** The prefix used for URIs which are of type <code>absolute</code>. */
090: private static final String ABSOLUTE_PREFIX = "file://";
091:
092: /** Logging */
093: private static Log log = LogFactory
094: .getLog(TurbineVelocityService.class);
095:
096: /** Is the pullModelActive? */
097: private boolean pullModelActive = false;
098:
099: /** Shall we catch Velocity Errors and report them in the log file? */
100: private boolean catchErrors = true;
101:
102: /** Default encoding to use if not specified in the RunData object. */
103: private String defaultEncoding = DEFAULT_CHAR_SET;
104:
105: /** Internal Reference to the pull Service */
106: private PullService pullService = null;
107:
108: /**
109: * Load all configured components and initialize them. This is
110: * a zero parameter variant which queries the Turbine Servlet
111: * for its config.
112: *
113: * @throws InitializationException Something went wrong in the init
114: * stage
115: */
116: public void init() throws InitializationException {
117: try {
118: initVelocity();
119:
120: // We can only load the Pull Model ToolBox
121: // if the Pull service has been listed in the TR.props
122: // and the service has successfully been initialized.
123: if (TurbinePull.isRegistered()) {
124: pullModelActive = true;
125:
126: pullService = TurbinePull.getService();
127:
128: log.debug("Activated Pull Tools");
129: }
130:
131: // Register with the template service.
132: registerConfiguration(VelocityService.VELOCITY_EXTENSION);
133:
134: setInit(true);
135: } catch (Exception e) {
136: throw new InitializationException(
137: "Failed to initialize TurbineVelocityService", e);
138: }
139: }
140:
141: /**
142: * Inits the service using servlet parameters to obtain path to the
143: * configuration file.
144: *
145: * @param config The ServletConfiguration from Turbine
146: *
147: * @throws InitializationException Something went wrong when starting up.
148: * @deprecated use init() instead.
149: */
150: public void init(ServletConfig config)
151: throws InitializationException {
152: init();
153: }
154:
155: /**
156: * Create a Context object that also contains the globalContext.
157: *
158: * @return A Context object.
159: */
160: public Context getContext() {
161: Context globalContext = pullModelActive ? pullService
162: .getGlobalContext() : null;
163:
164: Context ctx = new VelocityContext(globalContext);
165: return ctx;
166: }
167:
168: /**
169: * This method returns a new, empty Context object.
170: *
171: * @return A Context Object.
172: */
173: public Context getNewContext() {
174: Context ctx = new VelocityContext();
175:
176: // Attach an Event Cartridge to it, so we get exceptions
177: // while invoking methods from the Velocity Screens
178: EventCartridge ec = new EventCartridge();
179: ec.addEventHandler(this );
180: ec.attachToContext(ctx);
181: return ctx;
182: }
183:
184: /**
185: * MethodException Event Cartridge handler
186: * for Velocity.
187: *
188: * It logs an execption thrown by the velocity processing
189: * on error level into the log file
190: *
191: * @param clazz The class that threw the exception
192: * @param method The Method name that threw the exception
193: * @param e The exception that would've been thrown
194: * @return A valid value to be used as Return value
195: * @throws Exception We threw the exception further up
196: */
197: public Object methodException(Class clazz, String method,
198: Exception e) throws Exception {
199: log.error("Class " + clazz.getName() + "." + method
200: + " threw Exception", e);
201:
202: if (!catchErrors) {
203: throw e;
204: }
205:
206: return "[Turbine caught an Error here. Look into the turbine.log for further information]";
207: }
208:
209: /**
210: * Create a Context from the RunData object. Adds a pointer to
211: * the RunData object to the VelocityContext so that RunData
212: * is available in the templates.
213: *
214: * @param data The Turbine RunData object.
215: * @return A clone of the WebContext needed by Velocity.
216: */
217: public Context getContext(RunData data) {
218: // Attempt to get it from the data first. If it doesn't
219: // exist, create it and then stuff it into the data.
220: Context context = (Context) data.getTemplateInfo()
221: .getTemplateContext(VelocityService.CONTEXT);
222:
223: if (context == null) {
224: context = getContext();
225: context.put(VelocityService.RUNDATA_KEY, data);
226:
227: if (pullModelActive) {
228: // Populate the toolbox with request scope, session scope
229: // and persistent scope tools (global tools are already in
230: // the toolBoxContent which has been wrapped to construct
231: // this request-specific context).
232: pullService.populateContext(context, data);
233: }
234:
235: data.getTemplateInfo().setTemplateContext(
236: VelocityService.CONTEXT, context);
237: }
238: return context;
239: }
240:
241: /**
242: * Process the request and fill in the template with the values
243: * you set in the Context.
244: *
245: * @param context The populated context.
246: * @param filename The file name of the template.
247: * @return The process template as a String.
248: *
249: * @throws TurbineException Any exception trown while processing will be
250: * wrapped into a TurbineException and rethrown.
251: */
252: public String handleRequest(Context context, String filename)
253: throws TurbineException {
254: String results = null;
255: ByteArrayOutputStream bytes = null;
256: OutputStreamWriter writer = null;
257: String charset = getCharSet(context);
258:
259: try {
260: bytes = new ByteArrayOutputStream();
261:
262: writer = new OutputStreamWriter(bytes, charset);
263:
264: executeRequest(context, filename, writer);
265: writer.flush();
266: results = bytes.toString(charset);
267: } catch (Exception e) {
268: renderingError(filename, e);
269: } finally {
270: try {
271: if (bytes != null) {
272: bytes.close();
273: }
274: } catch (IOException ignored) {
275: // do nothing.
276: }
277: }
278: return results;
279: }
280:
281: /**
282: * Process the request and fill in the template with the values
283: * you set in the Context.
284: *
285: * @param context A Context.
286: * @param filename A String with the filename of the template.
287: * @param output A OutputStream where we will write the process template as
288: * a String.
289: *
290: * @throws TurbineException Any exception trown while processing will be
291: * wrapped into a TurbineException and rethrown.
292: */
293: public void handleRequest(Context context, String filename,
294: OutputStream output) throws TurbineException {
295: String charset = getCharSet(context);
296: OutputStreamWriter writer = null;
297:
298: try {
299: writer = new OutputStreamWriter(output, charset);
300: executeRequest(context, filename, writer);
301: } catch (Exception e) {
302: renderingError(filename, e);
303: } finally {
304: try {
305: if (writer != null) {
306: writer.flush();
307: }
308: } catch (Exception ignored) {
309: // do nothing.
310: }
311: }
312: }
313:
314: /**
315: * Process the request and fill in the template with the values
316: * you set in the Context.
317: *
318: * @param context A Context.
319: * @param filename A String with the filename of the template.
320: * @param writer A Writer where we will write the process template as
321: * a String.
322: *
323: * @throws TurbineException Any exception trown while processing will be
324: * wrapped into a TurbineException and rethrown.
325: */
326: public void handleRequest(Context context, String filename,
327: Writer writer) throws TurbineException {
328: try {
329: executeRequest(context, filename, writer);
330: } catch (Exception e) {
331: renderingError(filename, e);
332: } finally {
333: try {
334: if (writer != null) {
335: writer.flush();
336: }
337: } catch (Exception ignored) {
338: // do nothing.
339: }
340: }
341: }
342:
343: /**
344: * Process the request and fill in the template with the values
345: * you set in the Context. Apply the character and template
346: * encodings from RunData to the result.
347: *
348: * @param context A Context.
349: * @param filename A String with the filename of the template.
350: * @param writer A OutputStream where we will write the process template as
351: * a String.
352: *
353: * @throws Exception A problem occured.
354: */
355: private void executeRequest(Context context, String filename,
356: Writer writer) throws Exception {
357: String encoding = getEncoding(context);
358:
359: Velocity.mergeTemplate(filename, encoding, context, writer);
360: }
361:
362: /**
363: * Retrieve the required charset from the Turbine RunData in the context
364: *
365: * @param context A Context.
366: * @return The character set applied to the resulting String.
367: */
368: private String getCharSet(Context context) {
369: String charset = null;
370:
371: Object data = context.get(VelocityService.RUNDATA_KEY);
372: if ((data != null) && (data instanceof RunData)) {
373: charset = ((RunData) data).getCharSet();
374: }
375:
376: return (StringUtils.isEmpty(charset)) ? DEFAULT_CHAR_SET
377: : charset;
378: }
379:
380: /**
381: * Retrieve the required encoding from the Turbine RunData in the context
382: *
383: * @param context A Context.
384: * @return The encoding applied to the resulting String.
385: */
386: private String getEncoding(Context context) {
387: String encoding = null;
388:
389: Object data = context.get(VelocityService.RUNDATA_KEY);
390: if ((data != null) && (data instanceof RunData)) {
391: encoding = ((RunData) data).getTemplateEncoding();
392: }
393:
394: return (StringUtils.isEmpty(encoding)) ? defaultEncoding
395: : encoding;
396: }
397:
398: /**
399: * Macro to handle rendering errors.
400: *
401: * @param filename The file name of the unrenderable template.
402: * @param e The error.
403: *
404: * @exception TurbineException Thrown every time. Adds additional
405: * information to <code>e</code>.
406: */
407: private static void renderingError(String filename, Exception e)
408: throws TurbineException {
409: String err = "Error rendering Velocity template: " + filename;
410: log.error(err, e);
411: throw new TurbineException(err, e);
412: }
413:
414: /**
415: * Setup the velocity runtime by using a subset of the
416: * Turbine configuration which relates to velocity.
417: *
418: * @exception Exception An Error occured.
419: */
420: private synchronized void initVelocity() throws Exception {
421: // Get the configuration for this service.
422: Configuration conf = getConfiguration();
423:
424: catchErrors = conf.getBoolean(CATCH_ERRORS_KEY,
425: CATCH_ERRORS_DEFAULT);
426:
427: if (conf.containsKey(Velocity.INPUT_ENCODING)) {
428: defaultEncoding = conf.getString(Velocity.INPUT_ENCODING);
429: }
430:
431: conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM_CLASS,
432: Log4JLogChute.class.getName());
433: conf.setProperty(Velocity.RUNTIME_LOG_LOGSYSTEM
434: + ".log4j.category", "velocity");
435:
436: Velocity.setExtendedProperties(createVelocityProperties(conf));
437: Velocity.init();
438: }
439:
440: /**
441: * This method generates the Extended Properties object necessary
442: * for the initialization of Velocity. It also converts the various
443: * resource loader pathes into webapp relative pathes. It also
444: *
445: * @param conf The Velocity Service configuration
446: *
447: * @return An ExtendedProperties Object for Velocity
448: *
449: * @throws Exception If a problem occured while converting the properties.
450: */
451:
452: public ExtendedProperties createVelocityProperties(
453: Configuration conf) throws Exception {
454: // This bugger is public, because we want to run some Unit tests
455: // on it.
456:
457: ExtendedProperties veloConfig = new ExtendedProperties();
458:
459: // Fix up all the template resource loader pathes to be
460: // webapp relative. Copy all other keys verbatim into the
461: // veloConfiguration.
462:
463: for (Iterator i = conf.getKeys(); i.hasNext();) {
464: String key = (String) i.next();
465: if (!key.endsWith(RESOURCE_LOADER_PATH)) {
466: Object value = conf.getProperty(key);
467:
468: // Since 1.0-pre-something, Commons Collections suddently
469: // no longer returns a vector for multiple value-keys but a
470: // List object. Velocity will choke if we add this object because
471: // org.apache.commons.collections.ExtendedProperties expect a
472: // Vector object. Ah, the joys of incompatible class changes,
473: // unwritten assumptions and general Java JAR Hell... =8-O
474: if (value instanceof List) {
475: List srcValue = (List) value;
476: Vector targetValue = new Vector(srcValue.size());
477:
478: for (Iterator it = srcValue.iterator(); it
479: .hasNext();) {
480: targetValue.add(it.next());
481: }
482:
483: veloConfig.addProperty(key, targetValue);
484: } else {
485: veloConfig.addProperty(key, value);
486: }
487:
488: continue; // for()
489: }
490:
491: List paths = conf.getList(key, null);
492: if (paths == null) {
493: // We don't copy this into VeloProperties, because
494: // null value is unhealthy for the ExtendedProperties object...
495: continue; // for()
496: }
497:
498: Velocity.clearProperty(key);
499:
500: // Translate the supplied pathes given here.
501: // the following three different kinds of
502: // pathes must be translated to be webapp-relative
503: //
504: // jar:file://path-component!/entry-component
505: // file://path-component
506: // path/component
507:
508: for (Iterator j = paths.iterator(); j.hasNext();) {
509: String path = (String) j.next();
510:
511: log.debug("Translating " + path);
512:
513: if (path.startsWith(JAR_PREFIX)) {
514: // skip jar: -> 4 chars
515: if (path.substring(4).startsWith(ABSOLUTE_PREFIX)) {
516: // We must convert up to the jar path separator
517: int jarSepIndex = path.indexOf("!/");
518:
519: // jar:file:// -> skip 11 chars
520: path = (jarSepIndex < 0) ? Turbine
521: .getRealPath(path.substring(11))
522: // Add the path after the jar path separator again to the new url.
523: : (Turbine.getRealPath(path.substring(
524: 11, jarSepIndex)) + path
525: .substring(jarSepIndex));
526:
527: log
528: .debug("Result (absolute jar path): "
529: + path);
530: }
531: } else if (path.startsWith(ABSOLUTE_PREFIX)) {
532: // skip file:// -> 7 chars
533: path = Turbine.getRealPath(path.substring(7));
534:
535: log.debug("Result (absolute URL Path): " + path);
536: }
537: // Test if this might be some sort of URL that we haven't encountered yet.
538: else if (path.indexOf("://") < 0) {
539: path = Turbine.getRealPath(path);
540:
541: log.debug("Result (normal fs reference): " + path);
542: }
543:
544: log.debug("Adding " + key + " -> " + path);
545: // Re-Add this property to the configuration object
546: veloConfig.addProperty(key, path);
547: }
548: }
549: return veloConfig;
550: }
551:
552: /**
553: * Find out if a given template exists. Velocity
554: * will do its own searching to determine whether
555: * a template exists or not.
556: *
557: * @param template String template to search for
558: * @return True if the template can be loaded by Velocity
559: */
560: public boolean templateExists(String template) {
561: return Velocity.resourceExists(template);
562: }
563:
564: /**
565: * Performs post-request actions (releases context
566: * tools back to the object pool).
567: *
568: * @param context a Velocity Context
569: */
570: public void requestFinished(Context context) {
571: if (pullModelActive) {
572: pullService.releaseTools(context);
573: }
574: }
575: }
|