diff --git a/projects/samskivert/src/java/com/samskivert/servlet/MessageManager.java b/projects/samskivert/src/java/com/samskivert/servlet/MessageManager.java new file mode 100644 index 00000000..928f611b --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/servlet/MessageManager.java @@ -0,0 +1,145 @@ +// +// $Id: MessageManager.java,v 1.1 2001/03/04 06:15:39 mdb Exp $ + +package com.samskivert.servlet; + +import java.text.MessageFormat; +import java.util.*; +import javax.servlet.http.HttpServletRequest; + +import com.samskivert.Log; + +/** + * The message manager handles the translation messages for a web + * application. The webapp should construct the message manager with the + * name of its message properties file and it can then make use of the + * message manager to generate locale specific messages for a request. + */ +public class MessageManager +{ + /** + * Constructs a message manager with the specified bundle path. The + * message manager will be instantiating a ResourceBundle + * with the supplied path, so it should conform to the naming + * conventions defined by that class. + * + * @see java.util.ResourceBundle + */ + public MessageManager (String bundlePath) + { + // keep this for later + _bundlePath = bundlePath; + } + + /** + * Looks up the message with the specified path in the resource bundle + * most appropriate for the locales described as preferred by the + * request. + */ + public String getMessage (HttpServletRequest req, String path) + { + try { + // use the locale preferred by the client if possible + ResourceBundle bundle = resolveBundle(req); + if (bundle != null) { + return bundle.getString(path); + } + + // if we couldn't find a bundle, things are way wacked out, + // but we've already complained about it so we just fall + // through and return the path back to the caller + + } catch (MissingResourceException mre) { + // if there's no translation for this path, complain about it + Log.warning("Missing translation message [path=" + path + "]."); + } + + return path; + } + + /** + * Looks up the message with the specified path in the resource bundle + * most appropriate for the locales described as preferred by the + * request, then substitutes the supplied arguments into that message + * using a MessageFormat object. + * + * @see java.text.MessageFormat + */ + public String getMessage (HttpServletRequest req, String path, + Object[] args) + { + try { + // use the locale preferred by the client if possible + ResourceBundle bundle = resolveBundle(req); + if (bundle != null) { + String msg = bundle.getString(path); + // we may cache message formatters later, but for now just + // use the static convenience function + return MessageFormat.format(msg, args); + } + + // if we couldn't find a bundle, things are way wacked out, + // but we've already complained about it so we just fall + // through and return the path back to the caller + + } catch (MissingResourceException mre) { + // if there's no translation for this path, complain about it + Log.warning("Missing translation message [path=" + path + "]."); + } + + return path; + } + + /** + * Finds the closest matching resource bundle for the locales + * specified as preferred by the client in the supplied http request. + */ + protected ResourceBundle resolveBundle (HttpServletRequest req) + throws MissingResourceException + { + Enumeration locales = req.getLocales(); + + while (locales.hasMoreElements()) { + Locale locale = (Locale)locales.nextElement(); + + try { + // java caches resource bundles, so we don't need to + // reinvent the wheel here. however, java also falls back + // from a specific bundle to a more general one if it + // can't find a specific bundle. that's real nice of it, + // but we want first to see whether or not we have exact + // matches on any of the preferred locales specified by + // the client. if we don't, then we can rely on java's + // fallback mechanisms + ResourceBundle bundle = + ResourceBundle.getBundle(_bundlePath, locale); + + // if it's an exact match, off we go + if (bundle.getLocale().equals(locale)) { + return bundle; + } + + } catch (MissingResourceException mre) { + // no need to freak out quite yet, see if we have + // something for one of the other preferred locales + } + } + + try { + // if we were unable to find an exact match for any of the + // user's preferred locales, take their most preferred and let + // java perform it's fallback logic on that one + return ResourceBundle.getBundle(_bundlePath, req.getLocale()); + + } catch (MissingResourceException mre) { + // if we were unable even to find a default bundle, we've got + // real problems. time to freak out + Log.warning("Unable to resolve any message bundle " + + "[bundlePath=" + _bundlePath + + ", locale=" + req.getLocale() + "]."); + return null; + } + } + + protected String _bundlePath; +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/Application.java b/projects/samskivert/src/java/com/samskivert/webmacro/Application.java new file mode 100644 index 00000000..bf1d5ab6 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/webmacro/Application.java @@ -0,0 +1,103 @@ +// +// $Id: Application.java,v 1.1 2001/03/04 06:15:39 mdb Exp $ + +package com.samskivert.webmacro; + +import javax.servlet.ServletContext; +import com.samskivert.servlet.MessageManager; +import com.samskivert.util.StringUtil; + +/** + * The servlet API defines the concept of a web application and associates + * certain attributes with it like document root and so on. This + * application class extends that concept by providing a base class that + * represents the web application. The application class is responsible + * for initializing services that will be used by the application's logic + * objects as well as cleaning them up when the application is shut down. + */ +public class Application +{ + /** + * This should be overridden by the application implementation to + * perform any necessary initialization. + */ + public void init (ServletContext context) + { + } + + /** + * This should be overridden by the application implementation to + * perform any necessary cleanup. + */ + public void shutdown () + { + } + + /** + * If an application wishes to make use of the translation facilities + * provided by the message manager, it can instantiate one and make it + * available through this member function. This allows framework + * components like the MsgTool to make use of the + * application's message bundles. + */ + public MessageManager getMessageManager () + { + return null; + } + + /** + * The default application implementation takes the base URI and base + * package as defined in the application declaration in the servlet + * configuration and uses those to determine whether or not a given + * URI maps to this application. + */ + public void setConfig (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; + } + } + + /** + * Returns true if the supplied URI should be handled by this + * application. + */ + public boolean matches (String uri) + { + return uri.startsWith(_baseURI); + } + + /** + * Given a request URI this generates the classname of the logic class + * that should handle the request. + */ + 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; + + /** + * This is the default file extension. + */ + protected static final String FILE_EXTENSION = ".wm"; +} diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java b/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java index 43d6d893..65e8e561 100644 --- a/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java +++ b/projects/samskivert/src/java/com/samskivert/webmacro/DispatcherServlet.java @@ -1,5 +1,5 @@ // -// $Id: DispatcherServlet.java,v 1.6 2001/03/03 21:20:13 mdb Exp $ +// $Id: DispatcherServlet.java,v 1.7 2001/03/04 06:15:39 mdb Exp $ package com.samskivert.webmacro; @@ -17,7 +17,9 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import org.webmacro.*; -import org.webmacro.servlet.*; +import org.webmacro.servlet.HandlerException; +import org.webmacro.servlet.WMServlet; +import org.webmacro.servlet.WebContext; import com.samskivert.Log; import com.samskivert.servlet.RedirectException; @@ -169,14 +171,31 @@ public class DispatcherServlet extends WMServlet continue; } - Application app = new Application(baseURI, basePkg); - // if an application class was specified, initialize it - if (appcl != null) { - app.init(appcl, getServletContext()); - } + // instantiate the specified application class + Application app; - // construct an application object and add it to our list - _apps.add(app); + try { + // if a custom application class was specified, + // instantiate one of those. otherwise use the default + if (appcl != null) { + Class appclass = Class.forName(appcl); + app = (Application)appclass.newInstance(); + } else { + app = new Application(); + } + + // now initialize the applicaiton + app.setConfig(baseURI, basePkg); + app.init(getServletContext()); + + // finally add it to our list + _apps.add(app); + + } catch (Throwable t) { + Log.warning("Error instantiating custom application " + + "[class=" + appcl + "]."); + Log.logStackTrace(t); + } } } } @@ -205,24 +224,33 @@ public class DispatcherServlet extends WMServlet // then we populate the context with data try { - Logic logic = selectLogic(ctx); - // if no logic 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 (logic != null) { - logic.invoke(ctx); + String path = cleanupURI(ctx.getRequest().getRequestURI()); + + // select the proper application for the request + Application app = selectApplication(path); + if (app != null) { + // insert the application into the web context in case the + // logic or a tool wishes to make use of it + ctx.put(APPLICATION_KEY, app); + + // resolve the appropriate logic class for this URI and + // execute it if it exists + Logic logic = resolveLogic(app, path); + if (logic != null) { + logic.invoke(ctx); + } } - + } catch (RedirectException re) { try { ctx.getResponse().sendRedirect(re.getRedirectURL()); + return null; } catch (IOException ioe) { throw new HandlerException("Unable to send redirect: " + ioe); } - } catch (DataValidationException dve) { - ctx.put(DV_ERROR_KEY, ExceptionMap.getMessage(dve)); + } catch (FriendlyException fe) { + ctx.put(ERROR_KEY, fe.getMessage()); } catch (Exception e) { ctx.put(ERROR_KEY, ExceptionMap.getMessage(e)); @@ -232,6 +260,18 @@ public class DispatcherServlet extends WMServlet return tmpl; } + /** + * Returns the reference to the application that is handling this + * request. + * + * @return The application in effect for this request or null if no + * application was selected to handle the request. + */ + public static Application getApplication (WebContext context) + { + return (Application)context.get(APPLICATION_KEY); + } + /** * This method is called to select the appropriate template for this * request. The default implementation simply loads the template using @@ -252,39 +292,37 @@ public class DispatcherServlet extends WMServlet } /** - * This method is called to select the appropriate logic for this - * request. The dispatcher configuration described in this class's - * documentation is consulted to map the URI to a logic class which is - * then instantiated and a single instance used to process all - * matching requests. + * Selects and returns the matching application for this request. * - * @param ctx The context of this request. - * - * @return The logic to be used in generating the response or null if - * no logic could be matched. + * @return the application that should handle this request or null if + * no matching application could be found. */ - protected Logic selectLogic (WebContext ctx) + protected Application selectApplication (String path) { - String path = cleanupURI(ctx.getRequest().getRequestURI()); - String lclass = null; - // Log.info("Loading logic [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)) { - lclass = app.generateClass(path); - break; + return app; } } - // if we didn't find a matching application, we can stop now - if (lclass == null) { - return null; - } + return null; + } - // otherwise look for a cached logic instance + /** + * This method is called to select the appropriate logic for this + * request URI. + * + * @return The logic to be used in generating the response or null if + * no logic could be matched. + */ + protected Logic resolveLogic (Application app, String path) + { + // look for a cached logic instance + String lclass = app.generateClass(path); Logic logic = (Logic)_logic.get(lclass); + if (logic == null) { try { Class pcl = Class.forName(lclass); @@ -337,107 +375,6 @@ public class DispatcherServlet extends WMServlet 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; - } - - public void init (String appclass, ServletContext context) - { - // keep track of this for later - _appclass = appclass; - - try { - // get the application class - Class appcl = Class.forName(appclass); - - // look up the init method - Class[] ptypes = new Class[] { ServletContext.class }; - Method initmeth = appcl.getDeclaredMethod("init", ptypes); - - // make sure it's static - if ((initmeth.getModifiers() & Modifier.STATIC) == 0) { - Log.warning("Application init() method not static " + - "[app=" + appclass + "]."); - return; - } - - // look up the shutdown method - _shutdownMethod = - appcl.getDeclaredMethod("shutdown", new Class[0]); - - // make sure it's static - if ((_shutdownMethod.getModifiers() & Modifier.STATIC) == 0) { - Log.warning("Application shutdown() method not static " + - "[app=" + appclass + "]."); - return; - } - - // invoke the init method - Object[] args = new Object[] { context }; - initmeth.invoke(null, args); - - } catch (NoSuchMethodError nsme) { - Log.warning("Application class missing init() method " + - "[app=" + appclass + "]."); - - } catch (Throwable t) { - Log.warning("Error initializing application [app=" + appclass + - ", error=" + t + "]."); - } - } - - public void shutdown () - { - if (_shutdownMethod != null) { - try { - _shutdownMethod.invoke(null, null); - } catch (Throwable t) { - Log.warning("Error shutting down application " + - "[app=" + _appclass + ", error=" + t + "]."); - Log.logStackTrace(t); - } - } - } - - protected String _baseURI; - protected String _basePkg; - - protected String _appclass; - protected Method _shutdownMethod; - } - protected ArrayList _apps = new ArrayList(); protected HashMap _logic = new HashMap(); @@ -445,13 +382,8 @@ public class DispatcherServlet extends WMServlet protected static final String ERROR_KEY = "error"; /** - * This is the key used in the context for data validation error - * messages. + * This is the key used to store a reference back to the dispatcher + * servlet in our web context. */ - protected static final String DV_ERROR_KEY = "invalid_data"; - - /** - * This is the default file extension. - */ - protected static final String FILE_EXTENSION = ".wm"; + protected static final String APPLICATION_KEY = "%_app_%"; } diff --git a/projects/samskivert/src/java/com/samskivert/webmacro/MsgTool.java b/projects/samskivert/src/java/com/samskivert/webmacro/MsgTool.java index 3ad90837..7ded2b40 100644 --- a/projects/samskivert/src/java/com/samskivert/webmacro/MsgTool.java +++ b/projects/samskivert/src/java/com/samskivert/webmacro/MsgTool.java @@ -1,18 +1,15 @@ // -// $Id: MsgTool.java,v 1.1 2001/03/03 21:21:28 mdb Exp $ +// $Id: MsgTool.java,v 1.2 2001/03/04 06:15:39 mdb Exp $ package com.samskivert.webmacro; -import java.text.MessageFormat; -import java.util.MissingResourceException; -import java.util.ResourceBundle; - import org.webmacro.*; import org.webmacro.servlet.WebContext; import org.webmacro.util.PropertyObject; import org.webmacro.util.PropertyMethod; import com.samskivert.Log; +import com.samskivert.servlet.MessageManager; /** * The message tool maps a set of appliation messages (translation @@ -30,19 +27,27 @@ public class MsgTool implements ContextTool try { WebContext wctx = (WebContext)ctx; - // first fetch our messages resource bundle - final ResourceBundle messages = - ResourceBundle.getBundle(MESSAGE_FILE_NAME); + // get a handle on the application in effect for this request + Application app = DispatcherServlet.getApplication(wctx); + if (app == null) { + String err = "No application in effect for this request. " + + "Can't resolve messages without an application."; + throw new InvalidContextException(err); + } + + // get the message manager from the application + MessageManager msgmgr = app.getMessageManager(); + if (msgmgr == null) { + String err = "Application did not provide a message " + + "manager. Can't resolve messages without one."; + throw new InvalidContextException(err); + } // then create a wrapper that allows webmacro to access it - return new Wrapper(messages); - - } catch (MissingResourceException mre) { - Log.warning("Unable to load message bundle: " + mre); - return new Wrapper(null); + return new Wrapper(msgmgr); } catch (ClassCastException cce) { - throw new InvalidContextException("error.requires_webcontext"); + throw new InvalidContextException("MsgTool requires a WebContext"); } } @@ -53,9 +58,9 @@ public class MsgTool implements ContextTool public static class Wrapper implements PropertyObject { - public Wrapper (ResourceBundle bundle) + public Wrapper (MessageManager msgmgr) { - _bundle = bundle; + _msgmgr = msgmgr; } public Object getProperty (Context context, Object[] names, int offset) @@ -64,6 +69,7 @@ public class MsgTool implements ContextTool StringBuffer path = new StringBuffer(); boolean bogus = false; int lastidx = names.length-1; + WebContext wctx = (WebContext)context; // reconstruct the path to the property for (int i = offset; i < names.length; i++) { @@ -95,21 +101,20 @@ public class MsgTool implements ContextTool "path: " + path); } - // look up the translated message - String msg = _bundle.getString(path.toString()); - - // if the last component is a property method, we want to - // substitute it's arguments into the returned string using a - // message formatter + // if the last component is a property method, we want to use + // it's arguments when looking up the message if (names[lastidx] instanceof PropertyMethod) { PropertyMethod pm = (PropertyMethod)names[lastidx]; // we may cache message formatters later, but for now just // use the static convenience function Object[] args = pm.getArguments(context); - msg = MessageFormat.format(msg, args); - } + return _msgmgr.getMessage(wctx.getRequest(), + path.toString(), args); - return msg; + } else { + // otherwise just look up the path + return _msgmgr.getMessage(wctx.getRequest(), path.toString()); + } } public boolean setProperty (Context context, Object[] names, int offset, @@ -120,12 +125,6 @@ public class MsgTool implements ContextTool "is not supported!"); } - protected ResourceBundle _bundle; + protected MessageManager _msgmgr; } - - /** - * The file name of the resource file that contains our translation - * strings. - */ - protected static final String MESSAGE_FILE_NAME = "messages"; }