Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: org/infohazard/maverick/Dispatcher.java


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;
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 path = (String)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 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 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 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 configFile = (String)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 configURL = this.convertToURL(configFile);
491       log.info("Loading config from " + configURL.toString());
492 
493       // Figure out the config transform (if appropriate)
494       String configTransform = (String)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 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(transURL.openStream(), transURL.toString()));
521 
522           Source in = new StreamSource(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 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 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 }