KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > infohazard > maverick > Dispatcher


1 /*
2  * $Id: Dispatcher.java,v 1.25 2004/06/27 17:41:31 eelco12 Exp $
3  * $Source: /cvsroot/mav/maverick/src/java/org/infohazard/maverick/Dispatcher.java,v $
4  */

5
6 package org.infohazard.maverick;
7
8 import org.apache.commons.logging.Log;
9 import org.apache.commons.logging.LogFactory;
10 import org.infohazard.maverick.flow.Loader;
11 import org.infohazard.maverick.flow.Command;
12 import org.infohazard.maverick.flow.ConfigException;
13 import org.infohazard.maverick.flow.MaverickContext;
14
15 import org.jdom.Document;
16 import org.jdom.input.SAXBuilder;
17 import org.jdom.output.XMLOutputter;
18 import org.jdom.transform.JDOMResult;
19
20 import java.util.*;
21 import java.io.*;
22 import java.net.*;
23 import javax.servlet.*;
24 import javax.servlet.http.*;
25 import javax.xml.transform.*;
26 import javax.xml.transform.stream.StreamSource JavaDoc;
27
28
29 /**
30  * <p>
31  * Dispatcher is the central command processor of the Maverick framework.
32  * All commands are routed to this servlet by way of extension mapping
33  * (say, *.m). From here requests are routed through the "workflow"
34  * tree of {@link Command}, {@link org.infohazard.maverick.flow.View View}, and
35  * {@link org.infohazard.maverick.flow.Transform Transform} (or "Pipeline")
36  * objects built from the Maverick configuration file.
37  * </p>
38  *
39  * <p>
40  * Commands can be gracefully chained together; if a view references
41  * another Maverick Command, the same {@link MaverickContext} object is used.
42  * </p>
43  *
44  * <p>
45  * The Dispatcher object is made available to
46  * {@link org.infohazard.maverick.flow.Controller Controllers} (or anyone else)
47  * as an object in the application-scope (aka {@link ServletContext} attribute)
48  * collection.
49  * The attribute key is the value of the {@link #MAVERICK_APPLICATION_KEY
50  * MAVERICK_APPLICATION_KEY} constant.
51  * </p>
52  *
53  * <p>Note that there is are two special pseudocommands defined by this
54  * servlet: "<code>*</code>" and "<code>reload<code>".</p>
55  *
56  * <p>
57  * "<code>reload</code>" triggers a reload of the maverick config file.
58  * This can safely be done on running system; all commands currently being
59  * processed will complete using the old data.
60  * New command requests will use the new data as soon as it is finished loading.
61  * Note that the actual command name used for "<code>reload</code>" is
62  * determined by the <code>reload</code> Servlet init parameter.
63  * </p>
64  *
65  * <p>
66  * "<code>*</code>" is a special command which can be defined in the
67  * configuration file.
68  * If a command request cannot be mapped to a command (because the requested
69  * Command was not defined), the "*" Command will be used instead.
70  * If there is no "*" command defined in the configuration file,
71  * unmatched requests return 404.
72  * </p>
73  */

74 public class Dispatcher extends HttpServlet
75 {
76     /**
77      * <p>
78      * The key in the application context ({@link ServletContext}) under which
79      * the <code>Dispatcher</code> will be made available ["mav.dispatcher"].
80      * </p>
81      */

82     public static final String JavaDoc MAVERICK_APPLICATION_KEY = "mav.dispatcher";
83     
84     /**
85      * <p>
86      * If a value is set as an application attribute with this key,
87      * the value is used to override the setting of the <code>configFile</code>
88      * Servlet init parameter ["mav.configFile"].
89      * </p>
90      */

91     public static final String JavaDoc KEY_CONFIG_FILE = "mav.configFile";
92
93     /**
94      * <p>
95      * Name of the Servlet init parameter which defines the path to the
96      * Maverick configuration file ["configFile"].
97      * This parameter is used when {@link #KEY_CONFIG_FILE} is not set,
98      * otherwise the path defaults to
99      * {@link #DEFAULT_CONFIG_FILE DEFAULT_CONFIG_FILE}.
100      * </p>
101      */

102     public static final String JavaDoc INITPARAM_CONFIG_FILE = "configFile";
103
104     /**
105      * <p>
106      * If a value is set as an application attribute with this key,
107      * the value is used to override the setting of the
108      * <code>configTransform</code> Servlet init parameter
109      * ["mav.configTransform"].
110      * </p>
111      */

112     public static final String JavaDoc KEY_CONFIG_TRANSFORM = "mav.configTransform";
113
114     /**
115      * <p>
116      * Name of the Servlet init parameter which defines the path to a
117      * transform which will be applied to the Maverick configuration
118      * XML document before loading ["configTransform"].
119      * Defaults to null, which means perform no transformation.
120      * </p>
121      */

122     public static final String JavaDoc INITPARAM_CONFIG_TRANSFORM = "configTransform";
123
124     /**
125      * <p>
126      * Name of the Servlet init parameter which defines the name of the
127      * <code>reload</code> Command ["reloadCommand"].
128      * The value will typically be something like "reload".
129      * </p>
130      */

131     public static final String JavaDoc INITPARAM_RELOAD_COMMAND = "reloadCommand";
132
133     /**
134      * <p>
135      * Name of the Servlet init parameter which defines the name of the
136      * Command which displays the current configuration
137      * ["currentConfigCommand"].
138      * The value will typically be something like "currentConfig".
139      * </p>
140      */

141     public static final String JavaDoc INITPARAM_CURRENT_CONFIG_COMMAND =
142             "currentConfigCommand";
143
144     /**
145      * <p>
146      * Name of the Serlvet init parameter used to set the
147      * {@link #defaultRequestCharset defaultRequestCharset} property
148      * ["defaultRequestCharset"].
149      * </p>
150      */

151     public static final String JavaDoc INITPARAM_DEFAULT_REQUEST_CHARSET =
152             "defaultRequestCharset";
153
154     /**
155      * <p>
156      * Name of the Serlvet init parameter used to set the
157      * {@link #limitTransformsParam limitTransformsParam} property
158      * ["limitTransformsParam"].
159      * </p>
160      */

161     public static final String JavaDoc INITPARAM_LIMIT_TRANSFORMS_PARAM =
162             "limitTransformsParam";
163
164     /**
165      * <p>
166      * Name of the Serlvet init parameter used to set the {@link
167      * #reuseMaverickContext reuseMaverickContext} property
168      * ["reuseMaverickContext"].
169      * </p>
170      */

171     public static final String JavaDoc INITPARAM_REUSE_CONTEXT = "reuseMaverickContext";
172
173     /**
174      * <p>
175      * Default, context-relative, location of the Maverick XML configuration
176      * file ["/WEB-INF/maverick.xml"].
177      * </p>
178      * <p>
179      * Used if {@link #KEY_CONFIG_FILE} and {@link #INITPARAM_CONFIG_FILE} are
180      * not set.
181      * </p>
182      */

183     protected static final String JavaDoc DEFAULT_CONFIG_FILE = "/WEB-INF/maverick.xml";
184     
185     /**
186      * <p>
187      * The {@link MaverickContext} object is stored in the request context with
188      * this key so that it can be recovered for recursive maverick execution
189      * ["mav.context"].
190      * </p>
191      */

192     protected static final String JavaDoc SAVED_MAVCTX_KEY = "mav.context";
193
194     /**
195      * <p>
196      * Dispatcher logger.
197      * </p>
198      */

199     private static Log log = LogFactory.getLog(Dispatcher.class);
200
201     /**
202      * <p>
203      * Maps command names to Command objects.
204      * </p>
205      */

206     protected Map commands;
207     
208     /**
209      * <p>
210      * The current configuration document.
211      * </p>
212      */

213     protected Document configDocument;
214
215     /**
216      * <p>
217      * The charset to use by default for request parameter decoding
218      * [<code>null</code>].
219      * If not set, the default charset will be whatever the servlet
220      * container chooses (probably ISO-8859-1 aka Latin-1).
221      * If set, this String is used as the character encoding for HTTP
222      * requests.
223      * Leaving the property unset means do nothing special.
224      * </p>
225      * <p>
226      * This property may be set through the
227      * {@link #INITPARAM_DEFAULT_REQUEST_CHARSET
228      * INITPARAM_DEFAULT_REQUEST_CHARSET} Serlvet init parameter.
229      * </p>
230      */

231     protected String JavaDoc defaultRequestCharset;
232     
233     /**
234      * <p>
235      * The number of transformations to run before stopping, regardless of
236      * whether the final step has been reached.
237      * If this property is not set, all transforms will run to completion.
238      * </p>
239      * <p>
240      * This property may be set through the
241      * {@link #INITPARAM_LIMIT_TRANSFORMS_PARAM
242      * INITPARAM_LIMIT_TRANSFORMS_PARAM} Serlvet init parameter.
243      * </p>
244      */

245     protected String JavaDoc limitTransformsParam;
246     
247     /**
248      * <p>
249      * If set to <code>true</code>, the {@link MaverickContext} is reused
250      * between Commands invoked within the same request.
251      * This allows Maverick Controllers to be "chained" by forwarding
252      * context attributes from one Maverick Command to another.
253      * <p>
254      * The Context is <b>not</b> preserved in the case of a redirected
255      * request, since redirection creates a new HTTP request.
256      * </p>
257      * <p>
258      * This property may be set through the
259      * {@link #INITPARAM_REUSE_CONTEXT INITPARAM_REUSE_CONTEXT} Serlvet init
260      * parameter.
261      * Set the parameter to "true" or leave it undefined ["false"].
262      * </p>
263      */

264     protected boolean reuseMaverickContext;
265
266     /**
267      * <p>
268      * Initializes the Dispatcher by loading the configuration file.
269      * </p>
270      */

271     public void init() throws ServletException
272     {
273         // Make us available in the application attribute collection
274
this.getServletContext().setAttribute(MAVERICK_APPLICATION_KEY, this);
275         
276         // Get defaultRequestCharset from init parameter, null is ok
277
this.defaultRequestCharset = this.getInitParameter(INITPARAM_DEFAULT_REQUEST_CHARSET);
278
279         // Get limitTransformsParam from init parameter, null is ok
280
this.limitTransformsParam = this.getInitParameter(INITPARAM_LIMIT_TRANSFORMS_PARAM);
281
282         // Get reuseMaverickContext from init parameter, null is ok
283
this.reuseMaverickContext = "true".equals(this.getInitParameter(INITPARAM_REUSE_CONTEXT));
284         
285         try
286         {
287             reloadConfig();
288         }
289         catch(ConfigException e)
290         {
291             log.error(e.getMessage(), e);
292             throw e;
293         }
294     }
295
296     /**
297      * <p>
298      * The main entry point of the servlet; this processes an HTTP request.
299      * </p>
300      */

301     protected void service(HttpServletRequest request, HttpServletResponse
302             response) throws IOException, ServletException
303     {
304         // identify the command
305
String JavaDoc commandName = extractCommandName(request);
306
307         // get config for the command
308
Command cmd = this.getCommand(commandName);
309
310         if (cmd == null)
311         {
312             log.warn("No such command " + commandName);
313
314             // return 404
315
response.sendError(HttpServletResponse.SC_NOT_FOUND, "There is no such command \"" + commandName + "\".");
316         }
317         else
318         {
319             if (log.isDebugEnabled())
320                 log.debug("Servicing command: " + commandName);
321
322             // This must be done before any parameters are read
323
if (this.defaultRequestCharset != null)
324                 request.setCharacterEncoding(this.defaultRequestCharset);
325
326             // Maybe we want to use the same context object if we were recursively called from a
327
// Maverick view (say, somebody forwarded to "command.m")
328
MaverickContext ctx;
329             
330             if (this.reuseMaverickContext)
331             {
332                 ctx = (MaverickContext)request.getAttribute(SAVED_MAVCTX_KEY);
333                 
334                 if (ctx == null)
335                 {
336                     ctx = new MaverickContext(this, request, response);
337                     request.setAttribute(SAVED_MAVCTX_KEY, ctx);
338                 }
339             }
340             else
341             {
342                 ctx = new MaverickContext(this, request, response);
343             }
344             
345             cmd.go(ctx);
346         }
347     }
348
349     /**
350      * <p>
351      * Extracts the command name from the request.
352      * Extension and leading / will be removed.
353      * </p>
354      */

355     protected String JavaDoc extractCommandName(HttpServletRequest request)
356     {
357         // If we are include()ed from a RequestDispatcher, the real request
358
// path will be obtained from this special attribute. If we are
359
// produced by a forward() or a normal request, we can use the
360
// getServletPath() method. See section 8.3 of the Servlet 2.3 API.
361
String JavaDoc path = (String JavaDoc)request.getAttribute("javax.servlet.include.servlet_path");
362         if (path == null)
363             path = request.getServletPath();
364
365         if (log.isDebugEnabled())
366         {
367             log.debug("Command servlet path is: " + path);
368             log.debug("Command context path is: " + request.getContextPath());
369         }
370
371         int firstChar = 0;
372         if (path.startsWith("/"))
373             firstChar = 1;
374             
375         int period = path.lastIndexOf(".");
376
377         path = path.substring(firstChar, period);
378
379         return path;
380     }
381
382     /**
383      * <p>
384      * Reloads the XML configuration file.
385      * Can be done on-the-fly.
386      * Any requests being serviced are allowed to complete with the old data.
387      * </p>
388      */

389     protected void reloadConfig() throws ConfigException
390     {
391         log.info("Starting configuration load");
392         
393         Document replacementConfigDocument = this.loadConfigDocument();
394         Loader loader = new Loader(replacementConfigDocument,
395                 this.getServletConfig());
396         Map replacementCommands = loader.getCommands();
397
398         //
399
// Add a simple reload command if the user defined one.
400
//
401
String JavaDoc reloadStr = this.getInitParameter(INITPARAM_RELOAD_COMMAND);
402         if (reloadStr != null)
403         {
404             Command reload = new Command() {
405                 public void go(MaverickContext mctx) throws IOException, ServletException
406                 {
407                     try
408                     {
409                         reloadConfig();
410                     }
411                     catch(ConfigException e)
412                     {
413                         log.error(e.getMessage(), e);
414                         throw e;
415                     }
416                 }
417             };
418
419             replacementCommands.put(reloadStr, reload);
420         }
421
422         //
423
// Add the current config display command if the user defined one.
424
//
425
String JavaDoc currentConfigStr = this.getInitParameter(INITPARAM_CURRENT_CONFIG_COMMAND);
426         if (currentConfigStr != null)
427         {
428             Command currentConfig = new Command() {
429                 public void go(MaverickContext mctx) throws IOException, ServletException
430                 {
431                     XMLOutputter outputter = new XMLOutputter(" ", true, "UTF-8");
432
433                     mctx.getRealResponse().setContentType("text/xml; charset=UTF-8");
434                     outputter.output(configDocument, mctx.getRealResponse().getOutputStream());
435                 }
436             };
437
438             replacementCommands.put(currentConfigStr, currentConfig);
439         }
440
441         // Replace the commands map in place as the *LAST* step. This makes this
442
// operation thread-safe, since all existing threads continue working with
443
// the old data until they are finished.
444
this.commands = replacementCommands;
445         this.configDocument = replacementConfigDocument;
446
447         log.info("Finished configuration load");
448     }
449
450     /**
451      * <p>
452      * Returns the command object associated with the specified name.
453      * If the command is not found, a command with name "*" is returned.
454      * If there is no command with id "*", null is returned.
455      * </p>
456      */

457     protected Command getCommand(String JavaDoc name)
458     {
459         Command cmd = (Command)this.commands.get(name);
460
461         if (cmd == null)
462         {
463             cmd = (Command)this.commands.get("*");
464             if (cmd != null)
465                 log.warn("Unknown command " + name + ", using *.");
466         }
467
468         return cmd;
469     }
470     
471     /**
472      * <p>
473      * Returns a loaded JDOM document containing the configuration information.
474      * @return a loaded JDOM document containing the configuration information
475      * </p>
476      */

477     protected Document loadConfigDocument() throws ConfigException
478     {
479         try
480         {
481             // Figure out the config file
482
String JavaDoc configFile = (String JavaDoc)this.getServletContext().getAttribute(KEY_CONFIG_FILE);
483             
484             if (configFile == null)
485                 configFile = this.getInitParameter(INITPARAM_CONFIG_FILE);
486                 
487             if (configFile == null)
488                 configFile = DEFAULT_CONFIG_FILE;
489
490             java.net.URL JavaDoc configURL = this.convertToURL(configFile);
491             log.info("Loading config from " + configURL.toString());
492
493             // Figure out the config transform (if appropriate)
494
String JavaDoc configTransform = (String JavaDoc)this.getServletContext().getAttribute(KEY_CONFIG_TRANSFORM);
495             
496             if (configTransform == null)
497                 configTransform = this.getInitParameter(INITPARAM_CONFIG_TRANSFORM);
498
499             // Now load the document, maybe performing a transform
500
if (configTransform == null)
501             {
502                 try
503                 {
504                     SAXBuilder builder = new SAXBuilder();
505                     return builder.build(configURL.openStream(), configURL.toString());
506                 }
507                 catch (org.jdom.JDOMException jde)
508                 {
509                     throw new ConfigException(jde);
510                 }
511             }
512             else // must perform a transformation
513
{
514                 java.net.URL JavaDoc transURL = this.convertToURL(configTransform);
515                 log.info("Transforming config with " + transURL.toString());
516
517                 try
518                 {
519                     Transformer transformer = TransformerFactory.newInstance()
520                         .newTransformer(new StreamSource JavaDoc(transURL.openStream(), transURL.toString()));
521
522                     Source JavaDoc in = new StreamSource JavaDoc(configURL.openStream(), configURL.toString());
523                     JDOMResult out = new JDOMResult();
524
525                     transformer.transform(in, out);
526                     return out.getDocument();
527                 }
528                 catch (TransformerException ex)
529                 {
530                     throw new ConfigException(ex);
531                 }
532             }
533         }
534         catch (IOException ex)
535         {
536             throw new ConfigException(ex);
537         }
538     }
539     
540     /**
541      * <p>
542      * Returns the current configuration as a JDOM Document.
543      * @return the current configuration as a JDOM Document.
544      * </p>
545      */

546     public Document getConfigDocument()
547     {
548         return this.configDocument;
549     }
550
551     /**
552      * <p>
553      * Returns the {@link #limitTransformsParam} property.
554      * <code>null</code> null indicates the feature is disabled.
555      * </p>
556      */

557     public String JavaDoc getLimitTransformsParam()
558     {
559         return this.limitTransformsParam;
560     }
561     
562     /**
563      * <p>
564      * Interprets some absolute URLs as external paths, otherwise generates URL
565      * appropriate for loading from internal webapp.
566      * </p>
567      */

568     protected URL convertToURL(String JavaDoc path) throws MalformedURLException
569     {
570         if (path.startsWith("file:") || path.startsWith("http:")
571                 || path.startsWith("https:") || path.startsWith("ftp:")
572                 || path.startsWith("jar:"))
573             return new URL(path);
574         else
575         {
576             // Quick sanity check
577
if (!path.startsWith("/"))
578                 path = "/" + path;
579
580             return this.getServletContext().getResource(path);
581         }
582     }
583 }
584
Popular Tags