Wandora modules framework
Wandora contains a modular framework for setting up server applications. This can be used as part of a standard web app, or as a basis for a custom server application. It can also be used in the Embedded HTTP server. It could be used for other purposes than just server applications too but that is its primary purpose.
Contents |
Introduction
The idea behind the framework is that the server application consists of several individual modules, each of which does one specific thing. The modules often have dependencies between them, but some may also perform a completely isolated task.
As an example, a typical application might consist of the following modules:
- A topic map manager that provides a topic map for other modules and handles things related to the topic map.
- An Apache Velocity engine module which handles interfacing with Velocity.
- A template manager module which handles common tasks related to page templates.
- Several template modules, each representing one page template. These will register themselves to the template manager so other modules can find them, and use the Velocity Engine module to render the page. The templates could also be other than Velocity templates, but currently Velocity is the only supported template engine.
- A server module that receives HTTP requests and forwards them to other modules. Currently there are two options for this. One which interfaces a standard servlet container, such as Apache Tomcat, and one which interfaces with the Embedded HTTP server.
- Several action modules, these are the logical entry points for outside users. They receive the requests from the server module and then process them. The action possibly does something on the server side and then gives back a result, typically by utilising one of the templates. Some of the action modules may utilise the topic map manager module to do something with the topic map it provides.
There are several other modules that an application could also utilise. Some examples are a database module which connects to a relational database, a module that sends email, modules that restrict access based on user authentication, and others.
Many of the modules work behind abstract interfaces with the idea that the implementation of a required module can easily be changed. The above list of modules already mentioned one such example: the two different options for the server module. Some others have been defined with an interface but currently there is only one implementation, like the templates. They could use any templating engine but currently Apache Velocity is the only supported one.
Example web app
The samples/ModulesWebAppSample folder contains an example web app that can easily be deployed in Apache Tomcat or some other servlet container.
To use the sample web app, copy the entire folder into your webapps folder of the servlet container. Refer to the documentation of your servlet container on how to install web apps. Apache Tomcat at least has a folder called webapps directly under its installation folder where you just copy the entire ModulesWebAppSample folder. Then copy the compiled classes into the WEB-INF/classes folder inside the sample web app folder. To do this, just copy everything under Wandora's build/classes folder in there. The WEB-INF/lib folder should already contain the libraries needed by the sample web app, but if you add more functionality, you may need to copy more libraries here.
Next you may have to modify some configuration files. The WEB-INF/web.xml is the servlet configuration file, which is used by your servlet container to initialise the servlet. There's a section there that tells the servlet what modulesconfig.xml file to use to initialise the modules framework.
<init-param> <param-name>modulesconfig</param-name> <param-value>${catalina.base}/webapps/ModulesWebAppSample/WEB-INF/modulesconfig.xml</param-value> </init-param>
Change the param-value if needed. If you copied the ModulesWebAppSample folder directly under your webapps folder, and you're using Apache Tomcat with more or less standard settings, you shouldn't need to change anything. Otherwise, set the absolute path to the modulesconfig.xml here. For more information about the web.xml file, refer to the servlet container documentation.
The WEB-INF/modulesconfig.xml file is the configuration file for the modules framework. This too should not require any editing if you have a basic Apache Tomcat setup. Otherwise, you may need to change some settings at the start of the file. The configuration file sets some variables, which may be referred to later using the syntax ${variableName}. The servletHome variable is preset by the servlet that loads this file. The configuration file section contains more information about the modulesconfig.xml file as a whole.
<variable key="home">${servletHome}</variable>
- This line sets the webapp home directory. It should point to the WEB-INF folder, with no slash or backslash at the end. The servletHome variable is preset by the servlet that loads this file and should work in most cases.
<variable key="port">8080</variable>
- This is the port number for the server.
<variable key="urlbase">http://localhost:${port}/ModulesWebAppSample/wandora</variable>
- This is the URL for the servlet.
<variable key="staticbase">http://localhost:${port}/ModulesWebAppSample/</variable>
- This is the base of the URL for the static content used by the webapp, such as the CSS file.
<variable key="topicMapFile">${home}/ArtofNoise.wpr</variable>
- This is the Wandora topic map project file to load. By default, it uses the Art of Noise sample topic map.
<variable key="defaultSI">http://www.wandora.org/mp3/mp3</variable>
- This is the subject identifier of the default topic, if nothing else is specified in the http request.
The rest of the modulesconfig.xml file should use the initial variables to set everything else and you shouldn't need to change anything for the basic sample web app. Naturally, if you wish to customise the sample, you may have to modify all the other modules as well.
You may have to restart your servlet container too. After that, you should be able to browse the topic map by going to http://localhost:8080/ModulesWebAppSample/wandora or whatever you set as the urlbase of your web app.
Embedded server
The Embedded HTTP server is based on the modules framework. The framework consists of a root module manager which loads module bundles from subdirectories. The root manager has its own xml configuration file at resources/server/baseconfig.xml which sets up the basic environment for the module bundles. Each module bundle is in its own subdirectory with its own config.xml configuration file and any other files it needs, such as templates, data files, and images and style sheets. The module bundles may import modules from the root manager so that they don't need to recreate all the commonly needed modules. The root manager also sets some variables relating to the child module bundle paths. But for the most part, each module bundle operates as a separate module collection, and most everything described on this page about modules and their configuration applies to module bundles too.
Importing modules is done using the <import> element. For example, using the following in the config.xml of a module bundle imports a VelocityEngineModule from the baseconfig.xml of the parent manager into the manager of the child bundle. After this, modules in the child bundle can use VelocityEngineModule in other modules. This should be placed on the same level as all the module definitions, not inside a module definition.
<import class="org.wandora.modules.servlet.VelocityEngineModule"></import>
Note that the class specified here must be the class used when looking for the module. This is usually an interface, rather than the implementation. The actual module imported will then be some module that implements the specified class. If you try to import using the actual implementing class, the module will not be found. Although, some modules may require other modules based on a specific implementation too, this depends on the module that is dependant on the other module.
Configuration file
Basic structure
All the modules are defined in a configuration file, typically called modulesconfig.xml. This is an XML file with a fairly simple structure. The root element is <options> and under it each module is listed in <module> elements. The <module> element contains the Java class name of the module, which must implement the Module interface. Parameters for the module can be provided as child elements of the <module> element in <param> elements. For example, the following includes two modules, with the latter one containing some initialisation parameters for the module.
<?xml version="1.0" encoding="UTF-8"?> <options> <module class="org.wandora.modules.LoggingModule"></module> <module class="org.wandora.modules.topicmap.SimpleTopicMapManager"> <param key="topicMap">topicmap.wpr</param> <param key="autoSave">true</param> </module> </options>
Module dependency solving
Some module may require other modules to be present. All the required modules must be included in the config file. Their order does not matter, the modules are automatically loaded in such an order that modules that require others are loaded after the modules they need. This also means that there cannot be cyclic dependencies between the modules, although there are ways to work around this.
Dependencies are solved automatically based on the required module class or interface. The config file just needs to contain a module of that class or one implementing, or extending, it. But in some cases there may be more than one valid option for the dependency and you need to specify which to use. There are two ways to solve this.
First, you may name modules and then explicitly specify which module to use. Specify the name of the module in a name attribute in the <module> element. Then in the module that needs to use the other module, add a child element <useService> which specifies the service. Like this:
<module class="org.wandora.modules.topicmap.SimpleTopicMapManager" name="tmmanager"> <param key="topicMap">topicmap.wpr</param> <param key="autoSave">true</param> </module> <module class="org.wandora.modules.topicmap.ViewTopicAction"> <useService service="org.wandora.modules.topicmap.TopicMapManager" value="tmmanager"></useService> <param key="action">topic</param> <param key="templateKey">viewtopic</param> </module>
In the service attribute you must specify which service this relates to and then in the value attribute you specify the name of the module to use.
The second option to resolve the conflict is to specify a priority for the modules. You do this with a priority attribute in the <module> element. Each module by default has priority 0. A module with a higher priority will be chosen over a module with a lower priority in the automatic dependency solving. A module with a negative priority will never be automatically used, it can only be used by explicitly specifying so with the <useService> element as shown above. The <useService> will always override any priorities.
In the following example, the second topic map manager would not automatically be chosen for other modules. If any module wishes to use that, it must be explicitly specified with the <useService> element as shown above.
<module class="org.wandora.modules.topicmap.SimpleTopicMapManager"> <param key="topicMap">topicmap1.wpr</param> <param key="autoSave">true</param> </module> <module class="org.wandora.modules.topicmap.SimpleTopicMapManager" priority="-1"> <param key="topicMap">topicmap2.wpr</param> </module>
Also, the modules themselves may employ other means to specify what to use. As an example, all Template modules register themselves to a TemplateManager. Modules which need templates then don't depend directly on the Template module but instead on the TemplateManager which has its own mechanisms for getting the correct Template module.
Variables
You can specify variables directly under the root <options> element using a <variable> element.
<variable key="home">/webapp/webapphome</variable>
The variable needs a key, which is how it is referred to elsewhere, and a value. The key is specified in the key attribute and the value is the contents of the whole element. This variable can then be used elsewhere in the config file when you give parameters to modules or specify other variables. This is done using a dollar sign and curly brackets and writing the key of the variable in the curly brackets. You could specify another variable using the previous one like this.
<variable key="images">${home}/images</variable>
Similarly you can use the dollar and curly brackets inside <param> elements when specifying module parameters.
List of high level modules
Below is a list of high level modules that are ready to use as is with short descriptions of what they do. The lower level abstract modules are described in the module development section and the user control modules in the user control section.
org.wandora.modules package
- DefaultReplacementsModule
- The replacements system is a much lighter option to a full template engine. It's a simple search and replace based system that can be used by other modules to make any of their string values dynamic. The AbstractAction automatically uses any replacement module it can find, with the DefaultReplacementsModule being the only current implementation.
- EmailModule
- This module provides email services for others. You the SMTP server and any relevant details in the parameters.
- GenericDatabaseInterface
- This module provides relational database services. It uses JDBC and as such supports a wide range of different database servers. The server details are specified in the parameters.
- LoggingModule
- This module provides logging services for other modules. You should always have a logging module included. Depending on how you use the modules framework, one might be automatically included outside the config file.
- NetAuthenticatorModule
- If your server tries to request a resource through http, and the request must be authenticated with a user and a password, you can use this to add an authenticator for it. Note that this is not user authentication for incoming requests, this is for providing login details for requests the server initiates to other servers.
- ScopedModuleManager
- This is a child module manager that is also a module and can thus be placed inside another module manager. This way you can load another collection of modules inside the parent module manager. The child module manager may import modules from the parent for use inside the child configuration.
org.wandora.modules.cache package
- DiskCacheModule
- This module provides caching services for other modules that may need them. The results of other modules may be cached on the disk to speed up identical requests in the future. Obviously this is not suitable for all kinds of modules but may be very beneficial in some cases.
org.wandora.modules.servlet package
This package modules related to responding to HTTP requests. These types of modules are called actions, each of them perform some kind of action and then returns the result.
- ChainedAction
- This module can be used to chain two or more other actions together so that they are executed in sequence in response to a single http request. Typically only one of the actions returns anything and the others cause side effects on the server side.
- GenericTemplateAction
- This is one of the most useful modules. It runs a template, at the time a Velocity template but in future it could be some other kind of template too, through a template engine and then returns the result of that. The template context is initialised with values specified in the config file, with optionally passing on values from the HTTP request as well. As such, in many cases you can use this action directly without any making your own version of it. Custom application logic can instead be performed using the objects passed to the template in the template context.
- ImageResizeAction
- This module can be used to resize images server side to fit specified dimensions. This way you can have a repository of original images which are then resized to smaller dimensions before sent to the client web browser. Several image profiles can be specified in the module parameters, each with different image dimensions. The images can optionally be also cropped to fit the dimensions and a watermark can be stamped on the image. A cache should be used with this module so that the resizing process is done only once per each image and image profile combination.
- RedirectAction
- This module can return an HTTP redirect code so as to redirect the browser to a different address.
- RequestForwarder
- This module forwards the HTTP requests to another address server side and then returns the result of this. This can be used if you have a service in the server internal network that is inaccessible from outside and you wish to expose it. You may want to place some usage restrictions as well in such a case.
- SendEmailAction
- This module can send email, using an EmailModule, in response to an HTTP request. This module extends GenericTemplateAction, the sent email is the result of the processed template. This action doesn't respond to the HTTP request in any way so you should chain this action with some other that does using the ChainedAction module.
- TemplateManager
- This module keeps track of all templates and gives them to other modules that need them.
- VelocityEngineModule
- This module provides the Apache Velocity engine used to process Velocity templates.
- VelocityTemplate
- This module specifies a single Velocity template. It automatically registers itself to the TemplateManager. Other modules can request this template using the template key specified in parameters.
org.wandora.modules.topicmap package
- SimpleTopicMapManager
- This module provides a topic map for other modules to use. The topic map is loaded from a Wandora project file or an XTM file. The module can also automatically save the topic map, if it's being modified by other modules.
- ViewTopicAction
- This is a GenericTemplateAction which tailored for serving pages about topics. Among other things, it finds the needed topic based on the HTTP request parameters and adds it to the template context.
- WandoraTopicMapManager
- This is the other topic map manager. It connects to a running Wandora application and takes the topic map from there. Typically only used with the Embedded HTTP server which itself is running inside Wandora. This way, any modifications to the topic map in the Wandora application are immediately reflected in the server.
Access control
Access control is implemented using action contexts, not to be confused with template contexts used with Velocity templates. Each action binds to a specific context and then falls under whatever access restrictions are specified for that context. Different type of restrictions are placed by different context implementations.
Each context actually implements the ServletModule interface and looks to other actions as if it's the module receiving the HTTP requests. And, in a way, it is, it's just receiving them from another similar module and doing something with the request in between. As such, you will need to make sure that each context module and each action module use the right ServletModule. You do this by using priorities and/or explicitly specifying the module to use as described module dependency solving section. Typically you will want to give a high priority to the final context so that your actions automatically use that. Then use the explicit naming of modules for the contexts themselves. If different actions are to bind to different contexts, you will have to explicitly specify that in each action.
Many of the access control modules require a UserStore where details about access privileges are stored for all users. Currently there are three options for this.
- StaticUserStore
- This user store reads all user details from the config file itself and does not permit changing them. This is mostly meant for development as it's quick to setup and can then later be replaced with a more suitable solution.
- FileUserStore
- This module stores user details in a file in json format. The module permits changing users details and overwriting the existing file. This module should only really be used when the users are fairly static and there is a small number of them.
- DatabaseUserStore
- This module stores user details in a relational database and requires a database module to provide the database services.
Below is a list of other modules relating to access control. These modules are all in the package org.wandora.modules.usercontrol.
- GenericContext
- This is the base class for most other context modules, but you can also use it on its own. In particular, using the urlPrefix parameter, you can control which http requests are directed into this context based on the path of the request. You may also use the contextPath parameter to change the default path on the server the actions use. It defaults to the directory containing the server modulesconfig.xml but can be changed to, for example, a subdirectory of that using a GenericContext. Because GenericContext is the base class of most other contexts, you can use these features with other contexts as well.
- BasicUserAuthenticator
- This module is basic in the sense that it performs the authentication using the BASIC www authentication scheme and it is also basic in functionality. It stores the passwords of users in plain text so it should never be used in production environment, at least not when user details are collected from a large unspecified group of users. It may be suitable for situations where you have only one or a few users, as in a small group of administrators, that need to be authenticated, and the risks of storing passwords on the server in plain text is of no concern. Even then, you should encrypt the connection using https, otherwise your password will also be transmitted in plain text.
- FrequencyRestrictedContext
- This module places frequency restrictions on the context. A specific user can use the service only a certain number of times in a specific time span. As in, only 10 requests per minute, 1000 requests per month or something similar.
- RoleRestrictedContext
- This module only allows users with a specific role defined in their user details. Note that BasicUserAuthenticator can do a similar thing in the more common simple cases and you don't need a separate RoleRestrictedContext at all.
- SourceRestrictedContext
- This module only allows access from certain IP addresses.
In addition to the modules mentioned above, there is also a ChangeUserAction which can be used to change the properties or roles of a user.
Module development
Module hierarchy
You can make your own modules by implementing the Module interface or extending one of the abstract classes partially implementing it. Obviously you will need extend a higher level class or implement a certain interface if you wish to make your class do a specific task in the framework. All actions need to derive from AbstractAction but there are also higher level options like GenericTemplateAction. In fact GenericTemplateAction can even work without any extension if you place the application specific code in the template itself. Below is a list of the most common extension points. More details of specific methods can be found in the javadoc.
- Module
- The root interface of the module class hierarchy. This is the most generic option available. But in most cases you should instead extend the AbstractAction instead which has basic implementations of all the methods.
- AbstractModule
- This is the most generic of the abstract classes implementing Module. All methods of the Module interface are implemented, but they don't really do anything useful without overriding some methods or adding functionality in other ways.
- ScriptModule
- This is a direct extension of AbstractModule which provides some scripting features in the modulesconfig.xml file for the module. Modules deriving from this can have scripts specified in the config which are ran when the module is initialised, stopped or started. You may want derive your class from this instead of AbstractModule to get these features, even if you don't immediately plan to use them. They can later be used to add triggers when a module is started or stopped.
- ServletModule.RequestListener
- This is the interface for action classes that respond to http requests.
- AbstractAction
- This is an abstract class that implements the ServletModule.RequestListener interface and can thus respond to http requests. It also extends ScriptModule, and thus AbstractModule, so it has basic module implementation as well. This is basically the most generic abstract class to extend for action type modules.
- CachedAction
- This is a direct extension of AbstractAction and provides caching options for the action. If your action can benefit from having action results cached, you should consider extending this action. Caching can always be also turned off in the config even if you do extend from this class.
- GenericTemplateAction
- This is a direct extension of CachedAction and adds template handling. This class doesn't have any abstract methods and is useful on its own without any extensions. It will take an http request, use the template specified in the config with the context also specified in the config and then return the result of running the template through the template engine. If you only require minimal application logic which can be placed in the template, then you can just use this class directly instead of extending it. Otherwise extend the class and add some application logic and then add more things in the context if needed.
Module life-cycle
The instantiation, initialisation, starting, stopping and dependency resolution is all handled by the ModuleManager. The first thing in the life-cycle of a module is it being instantiated with the parameterless constructor, which modules should have. It is possible to not have this constructor in a module, but then it cannot be instantiated by the module manager automatically, meaning that you cannot use it in the config file at all. You can still add it in the module manager in your own custom code.
The constructor should not perform any big operations. It is assumed that instantiating the module is a fast operation and practically cannot fail, i.e. it should not throw any exceptions. There are other points in the life-cycle of the module where more complicated initialisation should be done.
Next the init method of the module will be called. The init method is given the parameters specified in the config file. What should happen in the init method is to process the parameters, store them in internal fields if needed, and validate that their values are sensible. Nothing time consuming should be done here, or anything with permanent side effects. The init method of every module will be called, even if the module is never going to be started, so at this point you do not know if the module will ever actually be expected to do anything. However, you may throw a ModuleException to indicate that the initialisation failed. For example, if some required parameters were not provided or the values of the parameters aren't sensible.
Next method in the life-cycle is getDependencies. In here you can specify what modules your module depends on. You do this by calling the requireModule method of the manager, which will immediately throw an exception if no suitable module is found. You can also use the optionalModule method to specify an optional dependency. This will not throw an exception if the needed module is missing, but if it exists, it makes sure that the needed module is started before your module. Note that the module dependencies may depend on initialisation parameters.
After the dependencies have been solved, modules can be started. The module manager will call the start method. This is the place where you can start executing the actual job of the module. The start method itself should not block for too long, so you should do any potentially time consuming tasks asynchronously.
Finally modules are stopped by calling the stop method. You should stop all threads spawned by your module, perform any cleaning up duties, dereference large data structures so that they may be garbage collected and return your module in a state where it can be started again with the start method, which may or may not happen.
Refer to the javadoc for more details on all the specific methods and their parameters.