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 }