diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/ContextPopulator.java b/projects/samskivert/src/java/com/samskivert/webmacro/ContextPopulator.java new file mode 100644 index 00000000..9e4e7845 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/ContextPopulator.java @@ -0,0 +1,30 @@ +// +// $Id: ContextPopulator.java,v 1.1 2001/02/15 01:44:34 mdb Exp $ + +package com.samskivert.webmacro; + +import org.webmacro.servlet.WebContext; + +/** + * The context populator is called upon to populate the WebMacro web + * context, prior to invoking a particular WebMacro template upon it to + * generate a response page. The populator takes the place of the servlet + * in the standard WebMacro architecture and should perform all of the + * logic involved in handling a particular request. + * + * @see DispatcherServlet + */ +public interface ContextPopulator +{ + /** + * Perform any necessary computation and populate the context with + * data for this request. Any exceptions that are thrown will be + * converted into friendly error messages using the exception mapping + * services. + * + * @param context The WebMacro context in scope for this request. + * + * @see ExceptionMap + */ + public void populate (WebContext context) throws Exception; +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/DataValidationException.java b/projects/samskivert/src/java/com/samskivert/webmacro/DataValidationException.java new file mode 100644 index 00000000..6d85dc58 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/DataValidationException.java @@ -0,0 +1,20 @@ +// +// $Id: DataValidationException.java,v 1.1 2001/02/15 01:44:34 mdb Exp $ + +package com.samskivert.webmacro; + +/** + * A data validation exception is thrown when a value supplied in a form + * element is not valid. The servlet framework will substitute the message + * assosciated with this exception into the context with the key + * "invalid_data". Templates should be structured so that + * this is reported to the user along with the filled out form field for + * their editing and resubmitting pleasures. + */ +public class DataValidationException extends FriendlyException +{ + public DataValidationException (String message) + { + super(message); + } +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java b/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java new file mode 100644 index 00000000..6ed70ef5 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java @@ -0,0 +1,363 @@ +// +// $Id: DispatcherServlet.java,v 1.1 2001/02/15 01:44:34 mdb Exp $ + +package com.samskivert.webmacro; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Properties; +import java.util.StringTokenizer; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import com.samskivert.util.ConfigUtil; +import com.samskivert.util.StringUtil; +import org.webmacro.*; +import org.webmacro.servlet.*; + +/** + * The dispatcher servlet builds upon WebMacro's architecture. It does so + * in the following ways: + * + * + * + *

URI to servlet mapping
+ * The mapping process allows the webmacro framework to be invoked for all + * requests ending in a particular file extension (usually + * .wm). It is necessary to instruct your servlet engine of + * choice to invoke the Dispatcher servlet for all requests + * ending in that extension. For Apache/JServ this looks something like + * this: + * + *

+ * ApJServAction .wm /servlets/com.samskivert.webmacro.Dispatcher
+ * 
+ * + * The request URI then defines the path of the webmacro template that + * will be used to satisfy the request. To understand how code is selected + * to go along with the request, let's look at an example. Consider the + * following configuration: + * + *
+ * applications=whowhere
+ * whowhere.base_uri=/whowhere
+ * whowhere.base_pkg=whowhere.servlets
+ * 
+ * + * This defines an application identified as whowhere. An + * application is defined by three parameters, the application identifier, + * the base_uri, and the base_pkg. The + * base_uri defines the prefix shared by all pages served by + * the application and which serves to identify which application to + * invoke when processing a request. The base_pkg is used to + * construct the populator classname based on the URI and the + * base_uri parameter. + * + *

Now let's look at a sample request to determine how the populator + * classname is resolved. Consider the following request URI: + * + *

+ * /whowhere/view/trips.wm
+ * 
+ * + * It begins with /whowhere which tells the dispatcher that + * it's part of the whowhere application. That application's + * base_uri is then stripped from the URI leaving + * /view/trips.wm. The slashes are converted into periods to + * map directories to packages, giving us view.trips.wm. + * Finally, the base_pkg is prepended and the trailing + * .wm extension removed. + * + *

Thus the class invoked to populate the context for this request is + * whowhere.servlets.view.trips (note that the classname + * is lowercase which is an intentional choice in resolving + * conflicting recommendations that classnames should always start with a + * capital letter and URLs should always be lowercase). + * + *

The template used to generate the result is loaded based on the + * full URI, essentially with a call to + * getTemplate("/whowhere/view/trips.wm") in this example. + * This is the place where more sophisticated cobranding support could be + * inserted in the future (ie. if I ever want to use this to develop a + * cobranded web site). + * + *

Error handling
+ * The dispatcher servlet provides a common error handling mechanism. The + * design is to catch any exceptions thrown by the populator and to + * convert them into friendly error messages that are inserted into the + * web context with the key "error" for easy display in the + * resulting web page. + * + *

The process of mapping exceptions to friendly error messages is + * done using the ExceptionMap class. Consult its + * documentation for an explanation of how it works. + * + * @see ContextPopulator + * @see ExceptionMap + */ +public class DispatcherServlet extends WMServlet +{ + public void start () + throws ServletException + { + // Log.log.info("Initializing dispatcher servlet."); + + // first load the properties file + Properties props = null; + try { + // load up our configuration + ClassLoader cld = DispatcherServlet.class.getClassLoader(); + props = ConfigUtil.loadProperties("dispatcher.properties", cld); + + } catch (IOException ioe) { + Log.log.warning("Failure trying to load " + + "'dispatcher.properties': " + ioe); + } + + if (props == null) { + Log.log.warning("Unable to load properties for " + + "dispatcher servlet."); + Log.log.warning("Make sure 'dispatcher.properties' file " + + "exists somewhere in your classpath."); + props = new Properties(); + } + + // now parse the configuration + String apps = props.getProperty("applications"); + if (apps != null) { + StringTokenizer tok = new StringTokenizer(apps); + while (tok.hasMoreTokens()) { + String appid = tok.nextToken(); + String baseURI = props.getProperty(appid + ".base_uri"); + String basePkg = props.getProperty(appid + ".base_pkg"); + + // make sure we're not missing anything + if (baseURI == null) { + Log.log.warning("Application '" + appid + + "' missing base_uri specification."); + continue; + } + if (basePkg == null) { + Log.log.warning("Application '" + appid + + "' missing base_pkg specification."); + continue; + } + + // construct an application object and add it to our list + _apps.add(new Application(baseURI, basePkg)); + } + } + } + + public Template handle (WebContext ctx) throws HandlerException + { + // first we select the template + Template tmpl; + try { + tmpl = selectTemplate(ctx); + } catch (NotFoundException e) { + throw new HandlerException("Unable to load template: " + e); + } + + // then we populate the context with data + try { + ContextPopulator pop = selectPopulator(ctx); + // if no populator is matched, we simply execute the template + // directly with the default information in the context (tools + // and other WebMacro services can be used by the template to + // do their WebMacro thing) + if (pop != null) { + pop.populate(ctx); + } + + } catch (DataValidationException dve) { + ctx.put(DV_ERROR_KEY, ExceptionMap.getMessage(dve)); + + } catch (Exception e) { + ctx.put(ERROR_KEY, ExceptionMap.getMessage(e)); + } + + return tmpl; + } + + /** + * This method is called to select the appropriate template for this + * request. The default implementation simply loads the template using + * WebMacro's default template loading services based on the URI + * provided in the request. It is assumed that the document root is + * registered in WebMacro's search path. + * + * @param ctx The context of this request. + * + * @return The template to be used in generating the response. + */ + protected Template selectTemplate (WebContext ctx) + throws NotFoundException + { + String path = cleanupURI(ctx.getRequest().getRequestURI()); + // Log.log.info("Loading template [path=" + path + "]."); + return getTemplate(path); + } + + /** + * This method is called to select the appropriate context populator + * for this request. The dispatcher configuration described in this + * class's documentation is consulted to map the URI to a populator + * class which is then instantiated and a single instance used to + * process all matching requests. + * + * @param ctx The context of this request. + * + * @return The populator to be used in generating the response or null + * if no populator could be matched. + */ + protected ContextPopulator selectPopulator (WebContext ctx) + { + String path = cleanupURI(ctx.getRequest().getRequestURI()); + String pclass = null; + // Log.log.info("Loading populator [path=" + path + "]."); + + // try to locate an application that matches this URI + for (int i = 0; i < _apps.size(); i++) { + Application app = (Application)_apps.get(i); + if (app.matches(path)) { + pclass = app.generateClass(path); + break; + } + } + + // if we didn't find a matching application, we can stop now + if (pclass == null) { + return null; + } + + // otherwise look for a cached populator instance + ContextPopulator pop = (ContextPopulator)_pops.get(pclass); + if (pop == null) { + try { + Class pcl = Class.forName(pclass); + pop = (ContextPopulator)pcl.newInstance(); + + } catch (Throwable t) { + Log.log.warning("Unable to instantiate populator for " + + "matching application [path=" + path + + ", pclass=" + pclass + ", error=" + t + "]."); + // use a dummy in it's place so that we don't sit around + // all day freaking out about our inability to instantiate + // the proper populator class + pop = new DummyPopulator(); + } + _pops.put(pclass, pop); + } + + return pop; + } + + /** + * Because the URI can actually contain the protocol and server name, + * though it commonly doesn't, we need to be sure to remove that extra + * information before we look at the URI because we only want the path + * part of the URI. The JSDK unfortunately doesn't provide an easy + * mechanism to request just the path part of the URI. + */ + protected static String cleanupURI (String uri) + { + int dsidx = uri.indexOf("//"); + if (dsidx == -1) { + return uri; + } + + int sidx = uri.indexOf("/", dsidx + 2); + if (sidx == -1) { + Log.log.warning("Malformed URI?! [uri=" + uri + "]."); + return "/"; + } + + return uri.substring(sidx); + } + + public static void main (String[] args) + { + System.out.println(cleanupURI("/whowhere/viewtrip.wm")); + System.out.println(cleanupURI("http://samskivert.com/foo/bar/baz.wm")); + System.out.println(cleanupURI("http://samskivert.com")); + System.out.println(cleanupURI("http://samskivert.com/")); + System.out.println(cleanupURI("//samskivert.com/is/this/even/legal")); + } + + protected static class Application + { + public Application (String baseURI, String basePkg) + { + // remove any trailing slash + if (baseURI.endsWith("/")) { + _baseURI = baseURI.substring(0, baseURI.length()-1); + } else { + _baseURI = baseURI; + } + + // remove any trailing dot + if (basePkg.endsWith(".")) { + _basePkg = basePkg.substring(0, basePkg.length()-1); + } else { + _basePkg = basePkg; + } + } + + public boolean matches (String uri) + { + return uri.startsWith(_baseURI); + } + + public String generateClass (String uri) + { + // remove the base URI + uri = uri.substring(_baseURI.length()); + // convert slashes to dots + uri = StringUtil.replace(uri, "/", "."); + // remove the trailing file extension + uri = uri.substring(0, uri.length() - FILE_EXTENSION.length()); + // prepend the base package and we're all set + return _basePkg + uri; + } + + protected String _baseURI; + protected String _basePkg; + } + + protected ArrayList _apps = new ArrayList(); + protected HashMap _pops = new HashMap(); + + /** This is the key used in the context for error messages. */ + protected static final String ERROR_KEY = "error"; + + /** + * This is the key used in the context for data validation error + * messages. + */ + protected static final String DV_ERROR_KEY = "invalid_data"; + + /** + * This is the default file extension. + */ + protected static final String FILE_EXTENSION = ".wm"; +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/DummyPopulator.java b/projects/samskivert/src/java/com/samskivert/webmacro/DummyPopulator.java new file mode 100644 index 00000000..8fcf555a --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/DummyPopulator.java @@ -0,0 +1,19 @@ +// +// $Id: DummyPopulator.java,v 1.1 2001/02/15 01:44:34 mdb Exp $ + +package com.samskivert.webmacro; + +import org.webmacro.servlet.WebContext; + +/** + * The dummy populator is used as a placeholder for URIs that match no + * normal populator or for which the matching populator could not be + * instantiated. It does nothing. + */ +public class DummyPopulator implements ContextPopulator +{ + public void populate (WebContext context) throws Exception + { + // we're such a dummy that we do absolutely nothing. + } +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/ExceptionMap.java b/projects/samskivert/src/java/com/samskivert/webmacro/ExceptionMap.java index 915eec37..ff5afbee 100644 --- a/projects/samskivert/src/java/com/samskivert/webmacro/ExceptionMap.java +++ b/projects/samskivert/src/java/com/samskivert/webmacro/ExceptionMap.java @@ -1,19 +1,55 @@ // -// $Id: ExceptionMap.java,v 1.1 2001/02/13 20:00:28 mdb Exp $ +// $Id: ExceptionMap.java,v 1.2 2001/02/15 01:44:34 mdb Exp $ package com.samskivert.webmacro; import java.io.*; import java.util.*; +import com.samskivert.util.ConfigUtil; import com.samskivert.util.StringUtil; /** - * The exception map is used to load the exception to error message - * mapping information and to look up the appropriate response for a given - * exception instance. + * The exception map is used to map exceptions to error messages based on + * a static, server-wide configuration. * - * @see FriendlyServlet + *

The configuration file is loaded via the classpath. The file should + * be named exceptionmap.properties and placed in the + * classpath of the JVM in which the servlet is executed. The file should + * contain colon-separated mappings from exception classes to friendly + * error messages. For example: + * + *

+ * # Exception mappings (lines beginning with # are ignored)
+ * com.samskivert.webmacro.FriendlyException: An error occurred while \
+ * processing your request: {m}
+ *
+ * # lines ending with \ are continued on the next line
+ * java.sql.SQLException: The database is currently unavailable. Please \
+ * try your request again later.
+ *
+ * java.lang.Exception: An unexpected error occurred while processing \
+ * your request. Please try again later.
+ * 
+ * + * The message associated with the exception will be substituted into the + * error string in place of {m}. The exceptions should be + * listed in order of most to least specific, as the first mapping for + * which the exception to report is an instance of the listed exception + * will be used. + * + *

Note: These exception mappings are used for all requests + * (perhaps some day only for requests associated with a particular + * application). Regardless, this error handling mechanism should not be + * used for request specific errors. For example, an SQL exception + * reporting a duplicate key should probably be caught and reported + * specifically by the appropriate populator (it can still leverage the + * pattern of inserting the error message into the context as + * "error") rather than relying on the default SQL exception + * error message which is not likely to be meaningful for such a + * situation. + * + * @see DispatcherServlet */ public class ExceptionMap { @@ -33,13 +69,8 @@ public class ExceptionMap } // first try loading the properties file without a leading slash - InputStream config = getStream(PROPS_NAME); - if (config == null) { - // some JVMs require the leading slash, some don't - config = getStream("/" + PROPS_NAME); - } - - // still no props, then we complain + ClassLoader cld = ExceptionMap.class.getClassLoader(); + InputStream config = ConfigUtil.getStream(PROPS_NAME, cld); if (config == null) { Log.log.warning("Unable to load " + PROPS_NAME + " from CLASSPATH."); @@ -111,19 +142,6 @@ public class ExceptionMap return StringUtil.replace(msg, MESSAGE_MARKER, ex.getMessage()); } - protected static InputStream getStream (String path) - { - // first try using the classloader that loaded us - Class c = ExceptionMap.class; - InputStream in = c.getResourceAsStream(path); - if (null == in) { - // if that didn't work, try the system classloader - c = Class.class; - in = c.getResourceAsStream(path); - } - return in; - } - public static void main (String[] args) { ExceptionMap map = new ExceptionMap(); @@ -137,6 +155,6 @@ public class ExceptionMap static { init(); } protected static final String PROPS_NAME = "exceptionmap.properties"; - protected static final String DEFAULT_ERROR_MSG = "Error: {m}."; + protected static final String DEFAULT_ERROR_MSG = "Error: {m}"; protected static final String MESSAGE_MARKER = "{m}"; } diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/FormUtil.java b/projects/samskivert/src/java/com/samskivert/webmacro/FormUtil.java new file mode 100644 index 00000000..d9ef799f --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/FormUtil.java @@ -0,0 +1,31 @@ +// +// $Id: FormUtil.java,v 1.1 2001/02/15 01:44:34 mdb Exp $ + +package com.samskivert.webmacro; + +import org.webmacro.servlet.WebContext; + +/** + * The form util class provides handy functions for doing form-related + * stuff. + */ +public class FormUtil +{ + /** + * Fetches the supplied parameter from the request and converts it to + * an integer. If the parameter does not exist or is not a well-formed + * integer, a data validation exception is thrown with the supplied + * message. + */ + public static int requireIntParameter (WebContext context, String name, + String invalidDataMessage) + throws DataValidationException + { + String value = context.getForm(name); + try { + return Integer.parseInt(value); + } catch (NumberFormatException nfe) { + throw new DataValidationException(invalidDataMessage); + } + } +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyException.java b/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyException.java index 867446f7..d739008c 100644 --- a/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyException.java +++ b/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyException.java @@ -1,5 +1,5 @@ // -// $Id: FriendlyException.java,v 1.1 2001/02/13 18:49:41 mdb Exp $ +// $Id: FriendlyException.java,v 1.2 2001/02/15 01:44:34 mdb Exp $ package com.samskivert.webmacro; @@ -11,7 +11,7 @@ package com.samskivert.webmacro; * construct a friendly exception with the desired error message and throw * it during the call to populateContext. * - * @see FriendlyServlet + * @see DispatcherServlet */ public class FriendlyException extends Exception { diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyServlet.java b/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyServlet.java deleted file mode 100644 index 0a545f79..00000000 --- a/projects/samskivert/src/java/com/samskivert/webmacro/FriendlyServlet.java +++ /dev/null @@ -1,112 +0,0 @@ -// -// $Id: FriendlyServlet.java,v 1.2 2001/02/13 20:00:28 mdb Exp $ - -package com.samskivert.webmacro; - -import org.webmacro.*; -import org.webmacro.servlet.*; - -/** - * The friendly servlet adds a common error handling paradigm to the basic - * webmacro servlet. This paradigm is to catch any exceptions thrown in - * the populateContext method (used instead of the - * handle method) and to convert them into friendly error - * messages that are inserted into the context with the key - * "error" for easy display to the user in the resulting web - * page. - * - *

To make this work, the process of template selection is separated - * from the process of data generation. First the template is selected; if - * that process fails, a standard error response is given to the user as - * we have no template into which to substitute the template selection - * failure error message. Subsequently, the data is generated. If that - * fails, we can substitute a friendly error message into the selected - * template. Finally, the template is executed, which is a standard - * webmacro procedure. - * - *

The process of mapping exceptions to friendly error messages is - * done through a configuration file loaded via the classpath. The file - * should be named exceptionmap.properties and placed in the - * classpath of the JVM in which the servlet is executed. The file should - * contain colon-separated mappings from exception classes to friendly - * error messages. For example: - * - *

- * # Exception mappings (lines beginning with # are ignored)
- * com.samskivert.webmacro.FriendlyException: An error occurred while \
- * processing your request: {m}
- *
- * # lines ending with \ are continued on the next line
- * java.sql.SQLException: The database is currently unavailable. Please \
- * try your request again later.
- *
- * java.lang.Exception: An unexpected error occurred while processing \
- * your request. Please try again later.
- * 
- * - * The message associated with the exception will be substituted into the - * error string in place of {m}. The exceptions should be - * listed in order of most specific to least specific, for the first - * mapping for which the exception to report is an instance of the - * exception in the left hand side will be used. - * - * Note: These exception mappings are used for all servlets - * (perhaps some day only for servlets associated with a particular - * application identifier). Regardless, this error handling mechanism - * should not be used for servlet specific errors. For example, an SQL - * exception reporting a duplicate key should probably be caught and - * reported specifically by the servlet (it can still leverage the pattern - * of inserting the error message into the context as - * "error") rather than relying on the default SQL exception - * error message which is not likely to be meaningful for such a - * situation. - */ -public abstract class FriendlyServlet extends WMServlet -{ - public Template handle (WebContext ctx) throws HandlerException - { - // first we select the template - Template tmpl; - try { - tmpl = selectTemplate(ctx); - } catch (Exception e) { - throw new HandlerException("Unable to load template: " + e); - } - - // then we populate the context with data - try { - populateContext(ctx, tmpl); - } catch (Exception e) { - ctx.put(ERROR_KEY, ExceptionMap.getMessage(e)); - } - - return tmpl; - } - - /** - * Override this member function with code that selects and loads the - * proper template based on whatever criterion appropriate for this - * servlet. - * - * @param ctx The context of this request. - * - * @return The template to be used in generating the response. - */ - protected abstract Template selectTemplate (WebContext ctx) - throws Exception; - - /** - * Override this member function with code that populates the context - * with whatever data is appropriate for this servlet. Presumably this - * is where the real work of the servlet takes place. - * - * @param ctx The context of this request. - * @param tmpl The webmacro template previously selected by the - * invocation of the selectTemplate method. - */ - protected abstract void populateContext (WebContext ctx, Template tmpl) - throws Exception; - - /** This is the key used in the context for error messages. */ - protected static final String ERROR_KEY = "error"; -} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/Makefile b/projects/samskivert/src/java/com/samskivert/webmacro/Makefile index 3087464c..7a6bc4a6 100644 --- a/projects/samskivert/src/java/com/samskivert/webmacro/Makefile +++ b/projects/samskivert/src/java/com/samskivert/webmacro/Makefile @@ -1,12 +1,16 @@ # -# $Id: Makefile,v 1.2 2001/02/13 20:00:28 mdb Exp $ +# $Id: Makefile,v 1.3 2001/02/15 01:44:34 mdb Exp $ ROOT = ../../.. SRCS = \ + ContextPopulator.java \ + DataValidationException.java \ + DispatcherServlet.java \ + DummyPopulator.java \ ExceptionMap.java \ + FormUtil.java \ FriendlyException.java \ - FriendlyServlet.java \ Log.java \ include $(ROOT)/build/Makefile.java