Copyright © 2002 by Jason D. Hildebrand
"Webware for Python" is a suite of software components for developing object-oriented, web-based applications. It is an open-source framework which includes an application server, servlets, Python Server Pages (PSP), an object-relational mapper (MiddleKit) and can be used in both Linux/Unix and Windows environments. This paper gives an overview of the main Webware components and uses examples to describe how they may be used to develop web applications.
Thanks to the Webware developers. Much of the information in this paper was drawn from
Webware is a framework which aims to provide a comfortable and powerful environment for developing web applications. It is object oriented in design and encourages object-oriented development. It is an open source project distributed under a Python-style license (i.e. it may be used, modified and distributed; derivatives are not required to be open-source). Webware runs equally well on both Unix and Windows platforms. It is being used in the real-world in several commercial and non-commercial websites.
Tip: Since Webware is written in Python, your Webware applications and extensions can take advantage of the many existing Python libraries for handling internet data and protocols.
Webware is structured as a collection of components or modules, called kits in Webware-speak. These kits are designed to work well with each other, but also to be useful independently as much as possible. This modular design allows alternate implementations of existing kits to be written and allows new kits to extend Webware's functionality.
Anyone interested in developing a web application will certainly want to make use of the WebKit, which provides a multithreaded application server with session support and the ability to execute servlets.
The MiddleKit is used to implement the "business objects" or "domain objects" as they are often called, which make up the middle layer in a typical three tier architecture, between the user interface (i.e. web page) and the storage (i.e. relational database). MiddleKit allows the developer to create a hierarchy of classes, and provides automatic persistence of these objects to a relational database. This will be discussed in more detail below.
There are several additional kits which this paper does not address. The best way to become familiar with them is to visit the Webware Home Page.
The PSP kit allows the user to write HTML pages with embedded Python code in a fashion similar to PHP, but with some key advantages.
UserKit supports users stored in files or using MiddleKit, and operations such as login, logout, listing logged-in users and check user roles/permissions. This kit is still in alpha, but may be used by early adopters.
TaskKit provides a framework for the scheduling and management of tasks which can be triggered periodically or at specific times.
FunFormKit is a package for generating and validating HTML forms
FormKit is a newly released framework which offers some improvements over the FunFormKit
Cheetah is a python-powered template engine which integrates tightly with Webware
The application server is a long-running process which listens on a TCP/IP port (by default port 8086). The Webware application server does not take the place of your webserver; it runs parallel to it. You can configure your webserver to forward certain requests to the application server (for example anything under http://www.mydomain.org/app), and handle other requests normally directly. There are several advantages to having an application server as opposed to developing an application using plain CGI:
Servlets stay resident in memory and may be re-used for many requests
Database connections may be maintained, eliminating connection setup/teardown times
The web server and application server may be run on different machines to increase performance
The following diagram illustrates the relationship between the webserver and the application server:
Note: If your site contains a lot of static content (i.e. images), the web server may be used to serve such content directly, since it can do this more efficiently than the application server.
The latest release can be downloaded from http://webware.sourceforge.net or you can use CVS to get the latest development version.
Upon unpacking and installing Webware (see the README file and the Webware documentation for more details), you can either create your application files in the same directory tree, or you can create your application in a new directory tree, separate from the Webware source files. I recommend the second approach, as it makes it easier to upgrade to new versions of Webware. To do this, run the command
python bin/MakeAppWorkDir.py /path/to/workdirwhich creates the following directory structure:
workdir/ Cache/ used by Webware
Configs/ Application.config edit these to alter your configuration
ErrorMsgs/ Webware stores error messages here
Logs/ Webware stores logs here
MyContext/ Sample context data (i.e. Servlets) is placed here;
you can modify it to create your own application
Sessions/ Session data is stored here
AppServer Starts the appserver on Unix
AppServer.bat Starts the appserver on Windows
Launch.py Used by AppServer[.bat]
NTService.py Win NT/2000 Service version of AppServer
WebKit.cgi install in your cgi-bin dir
OneShot.cgi install in your cgi-bin dir to use One-shot mode
The application server's namespace is divided into contexts. For example, a request for
http://www.mydomain.org/cgi-bin/WebKit.cgi/MyContext/Hellowill cause the application server to look in the MyContext context for a servlet called Hello. The current implementation of a context is a directory, so the application server will look for the file MyContext/Hello relative to your working directory.
It is possible to define a default context in the Application.config file. If this default is defined and the request does not include a context, the application server will check for the servlet in the default context.
There are several ways to get your webserver to forward requests to Webware's application server. The easiest method is to set up is CGI, whereby the webserver is configured to call a CGI script which in turn contacts the application server on port 8086 to handle the request. The WebKit.cgi script does exactly this.
Webware also includes the OneShot.cgi script which starts the application server, passes it a single request and then shuts it down. Since the application server needs to be restarted to pick up changes in the source files, this adapter is convenient to use when developing Webware applications.
For deployment, mod_webkit is commonly used with Apache to achieve better performance. mod_webkit is an Apache module which communicates with the Webware application server directly, thereby avoiding the overhead of starting an external CGI script.
If you have installed Webware and placed the Webkit.cgi in your cgi-bin directory (don't forget to adjust the paths at the top of this script), you can test out Webware by first starting the application server, and then requesting the URL
Important: Don't include the ".py" extension in URLs. The application server knows to add the extension when looking for servlets. Additional Webware kits may be installed to handle other types of files, too, so you could later switch to some templating system which uses a different extension without having to change all your URLs. In general, it is better to avoid exposing any particular technology in your URLs, since this means that switching to a different technology will force you to change them.
Note: See the Webware documentation for more detailed installation instructions.
We'll start with a simple servlet as an example:
from WebKit.Page import Page
The first thing to notice is that this servlet inherits from the Page class. A servlet must inherit from WebKit.Servlet or one of its subclasses (WebKit.HTTPServlet or WebKit.Page).
So how does writeContent get called in the above example? When the application receives a request it locates the relevant servlet, instantiates it (if no existing instance is available in the cache) and invokes three of its methods:
Page implements respond by calling the method writeHTML, which in turn calls writeHead and writeBody, which in turn call some other functions to generate the page output. These method calls are represented in the diagram below.
Typically, a web developer will implement a SitePage class which all the other servlets subclass. The SitePage may define the general look and feel of a page, such as menus, borders and colors (perhaps by referencing an external stylesheet), and may also provide any convenience methods which its subclasses will find useful. This is an excellent way to reduce maintenance costs, as the look and feel of your entire website can be changed by modifying SitePage. Of course, you can extend the idea by having two or more layers of base classes, so that different sections of your site can use different layouts and can be modified independently.
For example, let's say we'd like to implement a menu bar along the left side of our pages. A nice way to do this would be to encapsulate this into a MenuPage class, from which other servlets can inherit. In MenuPage we'd override the writeBodyParts method to call writePreContent and writePostContent functions before and after the content is written (see diagram below). These two new functions can set up an HTML table (or perhaps two nested HTML tables) so that output written from within writeSidebar will appear within a menu on the left side of the page, and output from writeContent will appear within the main area of the page.
Note: This assumes that the person doing the site design is familiar with programming, or can at least work closely with the programmer to divide sections of the HTML page into logical units (menu, content, etc.). If this is not the case, you may want to investigate templating technologies such as PSP and/or Cheetah, possibly in combination with servlets.
You will want your application to be able to respond to data that the user has entered. Although you may do this "by hand" by examining the request object in writeContent or elsewhere, Webware applications typically use actions to keep things a bit more structured.
Here is a simple example which demonstrates the use of the request fields and actions:
from WebKit.Page import Page ValidationError = 'ValidationError' class Test(Page): def writeContent(self,msg=''): self.writeln(''' %s<BR> <form method="Post" action="Test"> <input type="text" name="value1"> <input type="text" name="value2"> <input type="submit" name="_action_add" value="Add"> <input type="submit" name="_action_multiply" value="Multiply"> ''' % msg ) def actions(self): return Page.actions(self) + ["add", "multiply"] def validate(self): req = self.request() if not req.field('value1') or not req.field('value1'): raise ValidationError, "Please enter two numbers." try: value1 = float(req.field('value1')) value2 = float(req.field('value2')) except ValueError: raise ValidationError, "Only numbers may be entered." return ( value1, value2 ) def add(self): try: value1, value2 = self.validate() self.write( "<body>The sum is %f<BR>" % ( value1 + value2 )) self.write( '<a href="Test">Play again</a></body>') except ValidationError, e: self.writeContent(e) def multiply(self): try: value1, value2 = self.validate() self.write( "<body>The product is %f<BR>" % ( value1 + value2 )) self.write( '<a href="Test">Play again</a></body>') except ValidationError, e: self.writeContent(e)
Note: Both GET and POST variables are accessed through the same functions.
self.response().setCookie(name,value)to set the value of a named cookie,
self.request().hasCookie(name)to check if a cookie is available, and
self.request().cookie(name)to retrieve its value. Check the Webware Wiki for examples on how to set permanent cookies.
Sessions are used to maintain user-specific data between requests. In Webware, they can be used in three modes: file, memory and dynamic. In file mode, session data is serialized to disk after every transaction. In memory mode all sessions are kept in memory between requests. In dynamic mode (the default) recently used sessions are kept in memory and other sessions are saved to disk after a user-defined amount of time, so that only active sessions use up memory. You can also limit the number of sessions which are held in memory. These settings are located in the workdir/Configs/Application.config file.
Tip: No matter which type of sessions you are using, all sessions are saved to disk when the application server is stopped. This means you can restart the application server without losing the current sessions.
There are two ways that the application server can link a specific browser to its session. The default method is to store the session id within a cookie (the application server does this automatically). If your application should work without requiring the user to enable cookies, you might want to instead use the second method, which embeds the session id within the URL. The setting UseAutomaticPathSessions enables this mode.
There is a security consideration if you enable UseAutomaticPathSessions. If you provide external links from your website to other websites, these websites will be able to see your session ids in the HTTP_REFERER field (this field is passed by most browsers and contains the URL from which the link was activated). One way around this is to avoid using any direct external links. Replace external links with links to a redirect servlet, passing the target URL in the query string. The redirect servlet should then use an HTTP redirect to cause the user's browser to go to the external URL.
Sessions are easy to use. Use
self.session().setValue(name,value)to set a value,
self.session().hasValue(name)to test if a value has been set, and
self.session().value(name)to retrieve the value of a session variable.
Tip: Session variables are not limited to simple variables. You can store sequences and dictionaries, too, or complicated nested data structures. In general, using the current session implementation, you can store any data which can be pickled by the standard Python module cPickle.
There are other things you can do with servlets, such as forward a request to a different servlet, call a method of one servlet from another, or include the output of another servlet. After becoming familiar with the basics, it is good idea to read through Webware/WebKit/Page.py to see what is all possible.
The MiddleKit is a framework for implementing a hierarchy of business or domain objects. These are the objects that are created, organized and manipulated and stored by the application. Traditionally such information is stored in relational database, and web applications execute queries to retrieve the data and display it.
Even though some databases support object-oriented extensions, some "glue" is usually needed to make the information in the database available to the rest of the application as true objects. The MiddleKit is an object-relational mapping (ORM), a layer of software which attempts to bridge the gap between object-oriented Python and relational databases.
Some of the benefits of using MiddleKit are that writing code to manipulate database objects becomes less cumbersome and less error-prone, and that since the database access is isolated in one layer, it is reasonably straightforward to migrate to a different database in the future. It should also be noted that MiddleKit objects can be accessed from stand-alone Python (i.e. not through the WebKit application server). This makes it convenient for debugging, running automated tests on Middle Objects, or running maintenance scripts which manipulate the objects in some way.
MiddleKit currently supports MySQL and MS SQL Server. Adding support for other databases is straightforward, and work is being done to support PostgreSQL.
Although MiddleKit is still considered beta, it has already been used in several commercial websites. You may run into a few rough edges, but with source code in hand you have the means to help polish these smooth.
MiddleKit allows you to design a model, a hierarchy of classes. This model is specified in a comma-separated file (CSV), which is most easily manipulated with the help of a spreadsheet program. I recommend using Gnumeric to create this file.
MiddleKit can use this blueprint to generate SQL for creating the database schema, and Python classes corresponding to each class you defined in the model.
As an example, we will design a system for managing a bookstore. It will keep track of employees, customers, purchases, the items the store sells, and the shelves on which the items are displayed.
Assume the following directory structure. It makes sense to put this directory somewhere in your PYTHONPATH, so that you can import the modules from your web application. Don't put it under your servlet directories, though, since you don't want to make it externally accessible.
Middle/ BookStore.mkmodel/ contains model information
Classes.csv class definitions
Samples.csv sample data to be inserted into database
Settings.config config settings (see MiddleKit docs)
__init__.py create this empty file so that the directory
can be used as a Python package
In this directory structure, BookStore is the name I chose for the model, but this name is only really used in a few places.
We start with a few classes to represent employees and customers:
Table 1. Part of Classes.csv
|purchased||list of Bought|
The Person class is an abstract class. We will never instantiate it directly; it is only useful as a base class for Customer and Employee, because those two classes will have many common attributes (name and address fields). We set isAbstract=1 in the Extras column so that MiddleKit knows not to create a database table to hold Person instances.
Parentheses are used to specify the base class, if any. Multiple inheritance is not supported. You are free to name the attributes however you like, but keep in mind that accessor methods will be generated for each attribute. Because of this, attributes are conventionally namedLikeThis. For an attribute fooBar, MiddleKit will generate the accessor method object.fooBar() for retrieving the value and object.setFooBar() for setting the value.
A browse through the source code shows support string, int, bool, float, date, datetime, decimal, enum and long types. Personally I've only used string, int and bool extensively, so I don't know if support for the other types is complete or only partially implemented. We will see below that you can also create attributes whose types are user-defined classes.
The isRequired field specifies whether the attribute may be left blank (i.e. None in Python or NULL in SQL). The min and max fields, if given, specify constraints for numerical types (int,float, etc.). For strings, max specifies the maximum field width.
Employee extends Person by adding an employee id. Customer is a subclass of Person which keeps track of the items a customer has purchased. We will define the Bought class below.
Table 2. Another part of Classes.csv
The Bought class represents a transaction in which a Customer buys an Item from the store. The customer attribute is a reference to an instance of the Customer class which we just defined. The item attribute is a reference to an instance of the Item class, which we define below.
Look again at the purchased attribute of the Customer class above. In MiddleKit, these list attributes are implemented using back references. Those familiar with SQL know that it is not possible to store a list of entities in a single column. Lists in SQL are normally implemented using foreign keys and/or cross-reference tables. MiddleKit hides some of this from the user, but it is still helpful to know what is going on.
MiddleKit will not create a column corresponding to the purchased attribute in the Customer class. Rather, when you execute cust.purchased() (for some instance cust), MiddleKit will query the database for instances of Bought where the customer attribute corresponds to cust. In MiddleKit speak, Bought.customer is a back reference for Customer.purchased.
The onDeleteOther='cascade' specifies that if the referenced object (i.e. the customer) is deleted, we would like the Bought object to be deleted as well. By default, MiddleKit will not allow a referenced object to be deleted.
MiddleKit does not create back reference attributes automatically; it is up to you to add them.
Table 3. The last part of Classes.csv
|items||list of Item|
|purchasedBy||list of Bought|
The Shelf class represents a shelf in the store upon which the items are displayed (we assume simplistically that all items of a certain kind will be on the same shelf). The Shelf.items attribute will give us a list of Items on that shelf. Note that the back reference Item.shelf is required for this to work.
Item is an abstract class; it provides a base class for the Book and CompactDisc classes.
In general, a 1 to N relationship, such as that between items and shelves, is implemented by a list attribute in one entity and a back reference in the other. Remember that a list attribute does not correspond to a SQL column; it is more like a macro which relies on the corresponding back reference to produce its result.
An N to N relationship, such as that between customers and items, is implemented by an intermediate object (in our example, the Bought class). Each of the primary objects contains a list attribute, and the intermediate class contains a back reference to each. Note that the intermediate object may contain other attributes as well, if this makes sense (consider the Bought.date attribute).
To generate the SQL, base classes and skeleton class files from the Model, enter the command
python /path/to/Webware/MiddleKit/Design/Generate.py --db MySQL --model BookStorefrom the Middle directory. MiddleKit will generate the file GeneratedSQL/Create.sql which contains the SQL statements for creating the database schema. With MySQL you can execute this script with the command:
For each (non abstract) class in the model, MiddleKit will create a base class in the Middle/GeneratedPy directory which contains the attribute accessors, and a subclass in the Middle which you can customize. For example, for the Customer class, MiddleKit generates Middle/GeneratedPy/GenCustomer.py, which you should not edit, and Middle/Customer.py which you are free to customize. If you re-generate the classes, MiddleKit will only overwrite files under Middle/GeneratedPy.
You may provide the file Middle/BookStore.mkmodel/Samples.csv in the format as described below. If this file exists, MiddleKit will generate the file MiddleKit/GeneratedSQL/Insert.sql containing SQL insert statements. This is convenient for inserting sample data or migrating data to MiddleKit. Here is an example:
Table 4. Obligatory Python-related Sample Data
|A.E.J. Elliot O.V.E.||30 Days in the Samarkand Desert||7.9||120||0||Shelf.2|
|I. Gentleman||101 Ways to Start a Fight||4.95||57||0||Shelf.4|
|Charles Dikkens||Rarnaby Budge||12.99||230||0||Shelf.3|
|Edmund Wells||David Coperfield||15.79||234||0||Shelf.3|
|Olsen||Olsen's Standard Book of British Birds||10.5||157||1||Shelf.1|
|A. Git||Ethel the Aardvark goes Quantity Surveying||7.5||37||1||Shelf.3|
|Mary Queen of Scots||Soothing Sounds of Nature||12.95||1||Shelf.5|
|Nature||Books about Nature|
|Do it Yourself||Try this at home|
Note: You can leave out attributes as long as they do not have the isRequired flag set.
Note: Objects are assigned ids in order starting from 1. Use these numbers if you need to refer to another object. If you need to refer to a subclass, use the form classname.nn, as in the example.
At runtime, you need to instantiate an object store. You call methods on the object store to fetch objects, to add new objects to the store or to delete existing objects.
Normally there is one global object store instance for the entire application.
You can create a module called Store.py which creates the
object store instance when the module is imported. Since Python will only
execute this initialization code once, it will be safe in a multithreaded
environment such as WebKit. All other modules (i.e. servlets) which require
access to the store simply import Store.py and use the
from MiddleKit.Run.MySQLObjectStore import MySQLObjectStore
store = MySQLObjectStore(user='jdhildeb')
The object store caches objects in memory to improve performance, and keeps track of when object attributes are modified. These modifications are saved to the database when
Here are some examples of how to fetch Middle Objects.
Fetch all Book and CompactDisc objects and print their titles:
items = store.fetchObjectsOfClass( "Item" ) for item in items: print items.title()
Fetch books whose titles start with 'D':
books = store.fetchObjectsOfClass( "Book" ) books = [ book for book in books if book.title() == 'D' ]
Note: Note that this is quite inefficient if the list of books is large. It is possible to optimize this operation by passing a where clause to the SQL database.
The following example passes an extra "where clause" to the database. This is more efficient; since only relevant objects are returned by the database there is less processing in Python-land.
books = store.fetchObjectsOfClass( "Book", clauses="where title like 'D%'" )
Print a list of all shelves and the items on each shelf:
shelves = store.fetchObjectsOfClass('Shelf') for shelf in shelves: print shelf.name() for item in shelf.items(): print "\t" + item.title()
Each object is given a serial number which is unique among all objects in its class (it is not unique among all objects in the store).
num = book.serialNum()
Important: Objects are only assigned permanent serial numbers after they have been added to the store. Newly created objects have negative (temporary) serial numbers.
Fetch the book corresponding to a specific serial number:
book = store.fetchObject( "Book", 4 )
When using inheritance it is often convenient to have an object reference which is unique among all objects in the store. MiddleKit has such an id, which is a 64-bit long integer (the high 32 bits are the class id, and the low 32 bits are the serial number):
Note: These 64-bit object references are also used internally to represent object references. If you are debugging a MiddleKit application you'll find it difficult to figure out which object is being referenced until you decode the number into its components (class id and serial number).
If you have a sqlObjRef, it is easy to fetch the corresponding object:
obj = store.fetchObjRef( objref )
Tip: It is possible to pass each of these fetch methods a default value to return if the specified object is not found. Where class names are passed, it is also possible to pass the Python class itself, instead of the class name as a string. You'll find more shortcuts and niceties if you browse through Webware/Middle/Run/SQLObjectStore.py.
In this example, we add a new shelf, and then create a new CD to put on the shelf:
import sys,os sys.path.insert(1, '/path/to/my/app/Middle') from CompactDisc import CompactDisc from Shelf import Shelf shelf = Shelf() shelf.setName('Comedy') store.addObject(shelf) cd = CompactDisc() cd.setTitle('And Now for Something Completely Different') cd.setArtist('Monty Python') cd.setPrice(10.78) shelf.addToItems(cd) store.saveChanges()
Note: It is not necessary to call store.addObject(cd). The new shelf was added to the store, and so anything added to the shelf will be automatically added to the store, too. However, it doesn't hurt if you add it explicitly.
Important: Don't forget to call store.saveChanges() to make your changes persistent.
it is important to have your Middle directory (the one containing your Middle classes) in your Python path, and not to access the modules as (for example) Middle.Shelf. If a module is imported via two different paths, comparisons for classes may not produce the correct results, which can lead to runtime exceptions.
To modify an objects, simply fetch it and change its attributes. Don't forget to save the changes.
# inflation items = store.fetchObjectsOfClass("Item") for item in items: item.setPrice( item.price() * 1.07 ) store.saveChanges()
For objects which are not referenced by any other objects in the store, deletion is straightforward:
book = store.fetchObject('Book',1) store.deleteObject( book ) store.saveChanges()
However, MiddleKit will by default not allow you to delete an object if another object has a reference to it. For instance, you may not delete a Book instance if it is referenced by a Bought object. MiddleKit will raise a MiddleKit.Run.ObjectStore.DeleteReferencedError if you attempt to do so.
Keeping in mind how lists and back references work, this means that you cannot delete a container object such as a Shelf instance if there are any items referenced by it (i.e. if there are items on the shelf).
As a developer, you have a few ways to delete such objects. You can
delete all referenced objects explicitly before deleting an object, or
use the onDeleteOther or onDeleteSelf settings (as in the case of the Bought class, above) to specify how MiddleKit should handle deletion of referenced or referencing objects: deny, detach or cascade.
In our example book store, we set onDeleteOther='cascade' on the Bought.customer attribute. This means that when the other object (i.e. the referenced customer) is deleted, the Bought object will also be deleted. So the following code will succeed without raising an exception and will also delete the Bought object.
customer = store.fetchObject('Customer', 1 ) store.deleteObject( customer ) store.saveChanges()
Since WebKit is multithreaded and handles many requests simultaneously, it is possible that the same Middle Object will be modified by multiple requests such that it is left in an inconsistent state. MiddleKit does not have transactional capabilities in the sense that modifications to Middle Objects are isolated (i.e. invisible) to other WebKit threads.
The likelihood of such badness happening depends on the design of your application. You should consider if it is possible for two users to manipulate the same object at the same time. Allowing users to modify only objects which they own or objects for which they have write permissions will reduce the chance that things can go wrong.
If it is likely that two users will attempt to manipulate the same object, you may want to implement a locking mechanism to prevent problems from occurring.
Note: It should be noted that such issues of concurrency are not specific to MiddleKit; they present challenges in any web development environment.
If you modify your MiddleKit model (Classes.csv), you will need to re-generate the classes. If you already have some data in the database, you will need to dump and reload the data, and possibly manipulating the data to fit the new schema. There are a few ways to make this process less painful.
I've made an enhancement to MiddleKit which allows the store to dump all of its objects in the same format as the Samples.csv file. This allows you to dump the object database, re-generate the base classes, re-create the database schema and then reload the data. If you have (for example) added new attributes with the isRequired setting, you may have to tweak Samples.csv by hand before reloading. I hope that this feature will be in the official Webware release by the time you read this.
According to other Webware users, mysqldiff can be a valuable tool when undergoing changes to database schema. I've never tried it.
The Classes.csv file is a formal description of your class hierarchy, and this can buy you much more than just object persistence. Since much metainformation is contained in the model, it is possible to use this information to write some parts of your application generically.
For example, it is possible to generate HTML forms on-the-fly for any Middle Object, since you know the types and lengths of the object's attributes. Chuck Esterbrook (the original Webware author) is reported to be working on an object browser: a web interface which allows one to browse and modify objects in a MiddleKit store.
Consult the MiddleKit Users' Guide to get started, and use the MiddleKit source code to learn more.
The Webware for Python home page is at http://webware.sourceforge.net
The webware Wiki page contains many useful tips and examples which have not yet been included in the documentation. It can be found at http://webware.colorstudy.net/twiki/bin/view/Webware
Join the webware-discuss mailing list. Instructions are on the Webware home page.