$Id$
by Mel Martinez, melaquias@yahoo.com
Jasper is currently used in a variety of ways. Primarily, it is the JSP compiler/container for the Tomcat Servlet Engine. The default entry mechanism used for this is the JspInterceptor class. In addition, Jasper is also used as a standalone JSP command-line compiler, via the 'jspc' implementation. Finally, Jasper can also be used as a portable, plug-in JSP compiler in any compatible servlet engine via JspServlet. The latter adapter is used to plug Jasper into a variety of servlet engine products from commercial servlet engines such as WebLogic Server to open-source alternative servlet engines such as Jetty. The motivation for using Jasper portably like this is to either replace a known, defective COTS JSP compiler that can not be repaired due to lack of source code availability, or to provide JSP compilation services for a servlet engine that does not provide it's own.
These disparate implementations have different requirements for such services such as class-loading, name mangling, java compilation, etc. Currently, the JSP API tightly couples many of these services together in ways that are not the most flexible, and that make it difficult to optimize Jasper usage for different implementations. For example, in the current scheme, the name Mangler and JSP Compiler are implemented in the same class, making it difficult to separately modify either's behavior. Further, the Java compiler access and utilization is performed by the JSP Compiler, making it dificult to separate out that service.
This particular proposed refactoring attempts to address these and other issues by implementing the Abstract Factory pattern using a 'Toolkit' metaphor. Implementation classes for various services used by Jasper and Jasper Adapter Implementations will be provided via factory methods in a base toolkit class or it's subclasses. This decouples the user of each service from having to have knowledge about the instantiation or initialization of that service, except where necessary to provide customization specific to a particular implementation.
Factories provide several benefits and are used frequently in many standard
Java APIs, including java.lang.Runtime, java.util.Calendar, the AWT Toolkit
and, of course the Java Foundation Classes (Swing) API. Thus, the
use of factories is familiar to most Java developers. In particular,
a factory pattern is very easy to retrofit into existing code gracefully
since in many cases the only thing needed is to replace a direct constructor
instantiation (i.e. use of 'new') with a factory method invocation.
Also, a particular adapter implementation can make use of just the services
provided by the toolkit that it finds useful and that won't break current
functionality.
The refactoring will be done, for now, via a package hierarchy under
'org.apache.jasper34' to avoid conflict with the existing API.
At some future point, we may vote to merge the refactored code back under
'org.apache.jasper'. Thus, everywhere within this proposal,
and the accompanying uml model and code snippets, the use of the string
'jasper34' may at some point be replaced with 'jasper'. Within this
hierarchy, we introduce the package 'org.apache.jasper34.toolkit'.
This package contains the base JasperToolkit class which is the basis for
the abstract factory pattern we want to use.
The JasperToolkit class provides two public, static methods: getProperty(String) and createToolkit(String). The getProperty() method provides useful configuration control by facilitating easy access to configuration properties in the following order of precedence:
Second, the JasperToolkit.createToolkit(String) method provides a factory for accessing the relevant JasperToolkit subclass implementation. A particular implementation is referenced by a String name parameter used to map configuration properties a particular subclass implementation. That is, for a given JasperToolkit subclass, a name is assigned and, using a simple naming convention, various implementation classes can be specified in order to override the defaults used for the various services provided by the base class. This mechanism allows the choice of which implementation class to be used for a given service to be changed by simply setting a configuration property or alternatively, to specify a whole family of service implementation classes by specifying a particular name for the family. In the case of all services provided by the base JasperToolkit, in the absence of a specified implementation for a given name, the factory method will always default to returning a default implementation. If this is not desirable for a particular implementation, then that factory should be overridden in a toolkit subclass.
In addition to the two static methods described above, the base JasperToolkit
implementation provides default instance method factories for various services
common to most Jasper implementations. The following sorts of services
have been identified as useful for most Jasper implementations and thus
are accessible through the base JasperToolkit class:
service | description | configuration property syntax |
toolkit | The JasperToolkit subclass used by the implementation. | org.apache.jasper34.toolkit.toolkit.name |
jspfactory | The JspFactory implementation used by the engine. | org.apache.jasper34.toolkit.jspfactory.name |
pagehandler | The object responsible for life-cycle of one JSP. | org.apache.jasper34.toolkit.pagehandler.name |
requesthandler | The object responsible for the life-cycle of one request/response interaction with a JSP | org.apache.jasper34.toolkit.pagehandler.name |
jspcompiler | Compiles JSP source to .java source. | org.apache.jasper34.toolkit.jspcompiler.name |
javacompiler | Compiles .java source to .class bytecode | org.apache.jasper34.toolkit.javaompiler.name |
jspmangler | Creates names for the .java and .class files and packages generated from the jsp. | org.apache.jasper34.toolkit.jspmangler.name |
modificationchecker | Ascertains whether a JSP needs to be recompiled into it's target .class form or not. | org.apache.jasper34.toolkit.modificationchecker.name |
cache | A commonly-used performance optimization. | org.apache.jasper34.toolkit.cache.name |
The syntax for specifying each configuration property is thus fairly straightforward:
org.apache.jasper34.toolkit.<service>.<name> = <fully-qualified class name>where the value must specify a fully-qualified class name for a Java class that implements all requirements (interface, initialization) for the particular service. The particular requirements expected of each service are documented with the factory method responsible for instantiating the particular service.
For example, the servlet-based Jasper adapter implementation
might require particular implementation of the page handler and request
handler services. The name used for the implementation would be "JspServlet".
Because JspServlet also requires additional services not listed above (for
example, class loading), we will also require a new implementation of the
JasperToolkit itself. Let us presume that we will put all our special,
JspServlet-specific implementation classes into the package: org.apache.jasper34.servlet.*.
Further, let us decide to use some special Cache implementation that is
known to be super spiffy and happens to satisfy the Cache interface.
Then our configuration properties (specified in the Toolkit.properties
file) become like so:
# setup Jasper Toolkit for use by JspServlet implementation.
org.apache.jasper34.toolkit.toolkit.JspServlet = \
org.apache.jasper34.servlet.JspServletToolkit
org.apache.jasper34.toolkit.pagehandler.JspServlet = \
org.apache.jasper34.servlet.JspServletPageHandler
org.apache.jasper34.toolkit.requesthandler.JspServlet = \
org.apache.jasper34.servlet.JspServletRequestHandler
org.apache.jasper34.toolkit.cache.JspServlet = \
org.somegroup.somepackage.util.SuperDuperCache
# ... and so on
Any services not specified to use a particular class will result
in use of whatever the default base JasperToolkit methods instantiate.
Thus it is not necessary to specify every service for every implementation
family. In fact, it is not even necessary to specify a subclass of
JasperToolkit if the base toolkit class suffices and all you need to override
is one or two of the services it provides. It is only necessary to
use a subclass of JasperToolkit when the implementation requires additional
services beyond those provided by JasperToolkit or if it needs to change
the initialization contract for a particular service.
This approach provides a simple, cleanly-demarked boundary between the base JasperToolkit, which provides static configuration of it's services and sub-classes like JspServletToolkit, which can optionally override factory methods to utilitize run-time data for configuration of services. For example, the JspServletToolkit is to be initialized with the ServletConfig object so that it can access runtime init-parameters and use those, where appropriate, to resolve service configuration information.
Toolkit utilization will generally be like so: A particular Jasper
adapter implementation, such as JspServlet or JspInterceptor, will ask
the toolkit factory for an instance of the appropriate JasperToolkit
by name.
public void init(ServletConfig cfg){
super.init(cfg);
}
private JspServletToolkit getToolkit(){
if(toolkit==null){
toolkit = (JspServletToolkit)JasperToolkit.createJasperToolkit(
"JspServlet",getServletConfig());
}
return toolkit;
}
The toolkit factory will cache and return the same instance of a toolkit for a given name, so it is not generally necessary to preserve a reference to a toolkit in the client code but that may help performance slightly. Note that this is NOT a true singleton pattern since the same class can be instantiated with multiple names. The reason for this is to allow true mix-and-match configuration of different service implementation classes together under a given toolkit name by simply building an appropriate set of properties to describe the implementation. For the developer, the only concern is to avoid careless use of statics.
Once the appropriate toolkit has been accessed, it can be used to access
the various services as needed. For example, in the JspServlet implementation,
it could be used to create a cache for storing frequently used jsp page
handlers like so:
public Cache getCache(){
if(jsps == null){
int cache_size = CacheDefaults.JSP_CACHE_SIZE;
String sz = cfg.getParameter(JSP_CACHE_SIZE_PARAM);
if(sz!=null){
try{
cache_size = Integer.parseInt(sz);
}catch(NumberFormatException e){
cache_size = CacheDefaults.JSP_CACHE_SIZE;
}
}
jsps = getToolkit().createCache(cache_size, cache_size/4);
}
return jsps;
}
Given the above Cache, the service() method of JspServlet becomes
as simple as:
public void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
try {
String includeUri = (String) request.getAttribute(
JspConstants.INC_SERVLET_PATH);
String jspUri;
if(includeUri == null){
jspUri = request.getServletPath();
}else{
jspUri = includeUri;
}
JspServletPageHandler jph = (JspServletPageHandler)
getCache().get(jspUri);
if (jph == null) {
jph = (JspServletPageHandler)
getToolkit().createJspPageHandler(jspUri);
getCache().put(jspUri, jph);
jph.setParent(this);
}
jph.service(request, response);
} catch (Throwable t) {
//deal with exceptions, blah blah...;
}
}
That is about the extent of the JspServlet implementation's scope
of responsibility, barring a few details here and there.
The proposal currently calls for the creation of the following packages,
in addition to the org.apache.jasper34.toolkit package described above:
package | description |
org.apache.jasper34.resources | contains the default Toolkit.properties file |
org.apache.jasper34.servlet | contains the JspServlet implementation |
org.apache.jasper34.util | contains various utility classes and interfaces. |
Currently, the util package contains both classes specific to
Jasper implementations, such as JspMangler, and more generalized utilities
such as the Cache interface and some Cache implementations. The latter
classes may be good candidates for the jakarta commons project.
I am welcome to suggestions on package rearranging.
This effort is being developed in Together and the start of
a UML class model in javadoc form is available to look at here.
This should (hopefully) reveal many details of the strategy that
are not yet explained clearly in this document. The classes stubbed
out via the model, including some actual implemented code are in the src/jasper34
directory of the tomcat 3.3 cvs distribution. None of this code is
yet in the compilable state yet, as many of the interfaces are still being
developed. More than a few constants are currently in flux as to
where (in what class) they will ultimately reside.