Servlet Plugins
When using a file server object with the HTTP servlet framework, it is possible to associate a special purpose handler or plugin with requests against files with a particular extension. When a request is made against such a file, the plugin is used as an intermediary for the creation of a servlet to handle that request. The plugin can return a servlet which was loaded into the application at startup, or might also load the servlet from the file or otherwise generate a servlet on the fly.
This feature means that the functionality of an application can to a degree be extended but without the need to have such functionality hardwired into the application itself. The functionality of an application might even be extended or reduced at run time by the simple act of adding or removing files from the file system. This eliminates the need to restart an application everytime a change is required.
Python Plugin
To support implementations of HTTP servlets being contained within files residing in the file system, as opposed to being hardwired into the application itself, the PythonPlugin class is provided. This gives greater flexibility as it would not be necessary to restart the application to add in new functionality. The plugin can also detect when a servlet file has been modified and automatically reload it as necessary.
So that the Python import mechanism can find these files, they should be given a '.py' extension. This mapping is however not built in and it is necessary to register the extension as being associated with the particular plugin in question.
1 filesrvr = netsvc.FileServer(os.getcwd())
2 filesrvr.plugin(".py", netsvc.PythonPlugin())
The effect of this registration will be that whenever a file with extension '.py' is requested by a HTTP client, the plugin object will be executed as a callable object, with the HTTP session object and the name of the file being passed as arguments. In this case, the PythonPlugin object will import the file as if it is a Python module, obtain from it a reference to the HTTP servlet and then create an instance of the HTTP servlet to service the request.
The main difference when writing a servlet to be contained in a file and loaded in this way, as opposed to one which is hardwired into the actual application, is that it is necessary to provide a hook for creating an instance of the servlet. This is done by providing a definition within the file of the symbol __servlet__.
1 import netsvc
2
3 class HttpServlet(netsvc.HttpServlet):
4 def processRequest(self):
5 if self.requestMethod() != "GET":
6 self.sendError(400)
7 else:
8 self.sendResponse(200)
9 self.sendHeader("Content-Type", "text/plain")
10 self.endHeaders()
11 self.sendContent("Hi there.")
12 self.endContent()
13
14 __servlet__ = HttpServlet
In the simplest case, the symbol __servlet__ can be defined to be a reference to the actual servlet type. The PythonPlugin object will execute __servlet__ with the expectation it is a callable object, supplying it with a single argument of the HTTP session object. In the above case this will immediately result in an instance of the servlet being created.
An alternative might be that __servlet__ be defined as a function. This would allow one of a number of servlets to be chosen based on specific criteria, such as the time of day or whether the service is operational.
1 def __servlet__():
2 return HttpServlet
If a servlet requires additional arguments to be supplied along with the HTTP session object, a proxy object could instead be defined which transparently supplies the additional arguments. For example, a servlet designed to facilitate directory browsing might be supplied the name of the directory in which the servlet file resided, with the servlet generating a directory listing of files contained in the directory.
1 class ServletProxy:
2 def __call__(self, session):
3 directory = os.path.dirname(__file__)
4 return BrowseServlet(session, directory)
5
6 __servlet__ = ServletProxy()
Note that an instance of the servlet is created for each request. That is, unlike other similar systems available for Python, an instance of a servlet object is not cached and reused. If you need to maintain state between requests, such information should be factored out into a distinct service agent object.
Module Caching
In the case of the PythonPlugin object, when the file containing the actual servlet is read in, it is compiled into Python byte code and cached. This means that a subsequent request against that servlet will use the cached byte code and will not reread and recompile the file. So that is isn't necessary to stop and start the application if the file is changed, upon each subsequent request a check is made to see if the file has since been modified. If the file has been modified, it will be reread and recompiled ensuring that changes made to the file are visible.
Note that this mechanism will only detect if the actual servlet file has been modified. If that servlet file imports other modules using the Python import command and it is those other modules which have been changed, the cached servlet will still be used. This is acceptable where the other modules contain core program logic on which other parts of the application are dependent, but not in the case where the separate module contains a servlet base class defining the structure of a web page and it is the structure of the web page which you wish to change.
To cater for this situation, a special mechanism is provided for importing of modules which define servlet base classes or functionality related to the presentation of a web page. When this mechanism is used, that the servlet file is dependent on the module is recorded and a servlet file will be reread and recompiled, as will the module it depends on, when only the module had changed.
1 import os
2 import netsvc
3
4 cache = netsvc.ModuleCache()
5 directory = os.path.dirname(__file__)
6 _template = cache.importModule("_template", directory)
7
8 class HttpServlet(_template.PageServlet):
9 def writeContent(self):
10 self.writeln("Hi there.")
11
12 __servlet__ = HttpServlet
This means that the structure of a page can be defined in a common place, with each servlet file only defining the content specific to that page. The module caching mechanism should however only be used for this purpose. It is also recommended that for a particular module file, you not mix this mechanism and the standard Python import system, but use this system exclusively.
Note that a module imported in this way can use the same mechanism to import further modules with the dependence on those additional modules also being considered when the initial file is requested. Be aware however, that if files are located on a different machine to that which the application is running on and the clocks are not sychronised properly, updates may not always be detected correctly.
Writing a Plugin
A plugin can be any callable object, so long as it accepts as arguments when called, a HTTP session object and the name of a file which is the target of the request. The plugin may therefore be a type, a function, or an object which overrides the __call__ method. Which approach is used will depend on whether state needs to be preserved between invocations of the plugin. If no state needs to be preserved, a simple function may be the most approrpiate.
1 def factory(session, file):
2 return netsvc.FileServlet(session,file)
3
4 filesrvr.plugin(".txt", factory)
If a HTTP servlet when being constructed takes the same arguments as those passed to the plugin, ie., the HTTP session object and the name of a file, the servlet itself might instead be registered as the plugin.
1 filesrvr.plugin(".txt", netsvc.FileServlet)
Where state needs to be preserved, registration of an actual object instance which can hold the state, may be a better approach.
1 class Plugin:
2 def __call__(self, session, file):
3 return netsvc.FileServlet(session, file)
4
5 filesrvr.plugin(".txt", Plugin())
Plugin Aliasing
When a plugin is registered, the filename extension specified must appear in the URL used by the HTTP client when accessing that resource. This has the perhaps unwanted effect of exposing details about how the web pages are implemented. This may limit to what extent you can easily change the implementation later on, but may also give a malicious user ideas about how they may remotely break into your system.
For these reasons, although a servlet file might be required to use the extension '.py', if that servlet file always produces HTML, it may be preferable that that resource always be accessed by using a ".html" extension. An ability to do this can also be useful in the case where a resource is initially stored as a static file with a '.html' extension, but is later changed to be dynamically generated using a servlet. In this later case, the name of the resource can remain the same, and no references to the resource need to be changed.
To facilitate use of an alias, an optional argument can be supplied to the 'plugin()' member function defining the alternate extension the resource should be identified with. If you wanted all servlet files accessible using a particular instance of a file server object to be accessed using a '.html' extension instead of the '.py' extension, the string '.html' would be supplied as the optional third argument to the 'plugin()' member function.
1 filesrvr.plugin(".py", netsvc.PythonPlugin(), ".html")
2 filesrvr.hide(".py")
Note that if the 'hide()' member function isn't also called with the '.py' extension, the servlet file would still be accessible with a '.html' extension, but a request against the '.py' extension would yield the actual Python source code. This would not be an issue if the plugin had at the same time also been registered for '.py' files, but without the alias.
If the '.py' file is hidden, if the servlet file was called 'login.py', it would be accessable as 'login.html', but an attempt to use 'login.py' would result in a HTTP error response indicating that the file could not be found. If the '.py' file isn't hidden, but the plugin is registered twice, once without an alias and once with the alias '.html', both 'login.py' and 'login.html' would work.
If the servlet files are providing the roles of CGI scripts, it may be desirable for the files to use no extension at all. That is, the file should be accessed as 'login' instead of 'login.py'. If this is the case, rather than '.html', an empty string can be provided.
1 filesrvr.plugin(".py", netsvc.PythonPlugin(), "")
Be aware that the optional argument to 'plugin()' defining the alias is actually treated as a filename suffix and not strictly as an extension. What this means is that that argument need not start with '.', but can be any arbitrary string in which the name of a resource ends. This means it is actually possible to synthesis new resources as long as they derive from an actual file.
One use of this is a plugin which returns a servlet which generates a thumbnail version of an image. For example, if an image file was originally called 'holiday.gif', a request against 'holiday-thumbnail.gif' could me made to generate a thumbnail image on the fly.
1 def factory(session, file):
2 return ThumbnailServlet(session, file)
3
4 filesrvr.plugin(".gif", factory, "-thumbnail.gif")
