Norman came to me with some discussion about the work he is doing on the
fast-fail material, and I want to summarize the back-channel discussions.
The first topic that came up was how to have somewhat better control over
how the handler chain is executed. Norman had not seen the Servlet Filter
API, with its approach to chaining, so I introduced him to that API, and
with some input he is adapting those ideas to our code. His initial work
can be seen in today's commits.
Once you start down this road, you realize why the Servlet Filter API had to
deal with wrapped response objects, since you don't want downstream handlers
to have direct access to writing a response. Norman ran into a problem with
simply having handlers provide a response object, since a few handlers
introduce a separate issue: modality.
Currently, I/O comes into the SMTP server, which takes each line, assumes
that it is of the form:
<command> <arg-list>
and passes it along to be dispatched to the handler chain for that command.
The problem occurs when some command, e.g., AUTH or DATA, wants to perform
some I/O in a unique modality, such as the AUTH command providing its own
prompt and wanting to read the response.
My strawman is to replace the assumption with --- for lack of a better
term --- an I/O handler. The normal I/O handler is just the command
dispatcher. Each line as it comes in is handed off to the I/O handler (aka
command dispatcher) to process, and the response found in the session is
written back over the socket. But when we need to enter a nested state,
things change just slightly.
Let's use the AUTH command as an example. It currently does:
onCommand:
prompt user
read credential
process credential
return
Notice the problematic nested I/O modality. The new proposal would
eliminate this nested modality by breaking the operation up into two states
instead of one, and providing a new I/O handler:
onCommand:
push credential handler onto I/O stack
set SMTP response
return
Credential Handler:
pop I/O handler stack
process credential
set SMTP response
return
This concept may be easier to understand with some code, so I wrote a simple
and quite raw program to illustrate the ideas.
Please keep in mind that this is just a strawman and starting point. The
key idea is inverting the I/O modalities, breaking them up into managable
states maintained by a simple state machine with a stack to simplify state
transition. I have identified a few states: command, auth-credentials and
message, but I haven't really gone looking for others. Since all of the
states transition are nested, a stack-based FSM makes sense.
This code just points out a direction, and is compatible with supporting
both blocking and non-blocking I/O. It is not intended to show the details
that would be necessary, for example, to handle inserting the additional
data transformation code necessary for accumulating the message after the
DATA command registers a new I/O handler. Nor did I try to show buffering
interactions. But you will notice that there isn't any I/O anywhere except
for the test driver. If all of our protocol handlers and data transform
code were incorporated into this model, we'd be golden for java.nio.
My thought is that we travel down this road for all except for DATA as Phase
I, which gives us 90% of the support for a fraction of the effort, and then
having gotten more experience with our implementation, we move onto DATA as
Phase II.
--- Noel
-------------------------------------------------------------
This is just a test driver, somewhat following the pattern we have in our
protocol handling code.
public class IODemo {
public static void main(String[] _) {
// create a session object
IOSession session = new IOSession();
// initially, I/O will be delivered to the CommandDispatcher
// as with the Servlet API, an efficient singleton pattern
// is followed.
session.pushIOObject(CommandDispatcher.singleton);
// provide some initial prompt. For us, this would
// not exist, and would be provided by the
// ConnectionHandler
System.out.print("Welcome.\r\n>");
// just loop, pulling data. This code is the adapter
// between blocking I/O and the purely push model code
// further in.
while (!session.quit()) {
byte[] data = new byte[80];
try {
System.in.read(data);
// we naively ask the session to just
// process the data.
session.process(data);
// and render whatever response we get back
System.out.print(session.getResponse());
}
catch (java.io.IOException ioe) {
System.out.println(ioe.getMessage());
}
}
}
}
-------------------------------------------------------------
This is the I/O session for my sample program. It just tracks four
properties:
- is it time to stop?
- attributes as a Map
- a response string
- the "I/O handler" stack
and otherwise just exposes the process(byte[]) method. That method just
takes whatever IOObject is currently TOS and calls it. For my convenience,
and no other reason, at this point I convert the data to a String. For
JAMES, we would not do that here, and would want to deal with buffering and
any conversion further on, so this should stay raw bytes.
import java.util.Map;
import java.util.HashMap;
public class IOSession {
private Map attributes;
public Map getAttributes() { return (attributes == null) ? (attributes =
new HashMap()) : attributes; }
private boolean running = true;
public void stop() { running = false; }
public boolean quit() { return !running; }
private String response;
public void setResponse(String r) {
response = r;
}
public String getResponse() {
return response;
}
public void process(byte[] data) {
getIOObject().dataReceived(this, new String(data));
}
private java.util.List ioObject = new java.util.ArrayList(2);
public void pushIOObject(IOObject io) {
ioObject.add(0, io);
}
public void popIOObject() {
ioObject.remove(0);
}
public IOObject getIOObject() {
return (IOObject) ioObject.get(0);
}
}
-------------------------------------------------------------
This is the normal "IO Handler", which takes line oriented data, and
dispatches to appropriate commands. Here, I just hardcode the list of
commands, and call singletons to handle them. I'm not even particularly
careful in this Q&D code.
There are three commands: QUIT, SET and SHOW. Anythng else results in a
generic prompt. The only interesting command, for our purposes, is the SET
command.
public class CommandDispatcher implements IOObject {
static public final CommandDispatcher singleton = new
CommandDispatcher();
public void dataReceived(IOSession session, String data) {
if (data.startsWith("QUIT")) {
session.setResponse("Received QUIT command.\r\n");
session.stop();
} else if (data.startsWith("SET")) {
SetAttribute.singleton.onCommand(session,
data.substring("SET".length()));
} else if (data.startsWith("SHOW")) {
ShowAttributes.singleton.onCommand(session, data);
}
else session.setResponse("Commands are SHOW, SET and QUIT\r\n");
}
}
-------------------------------------------------------------
public class SetAttribute implements CommandHandler {
// SET <keyname>
// we want a value, but we have to prompt for it, and
// then receive it, resulting in an I/O modality.
public void onCommand(IOSession session, String data) {
// push the new I/O object into the session
session.pushIOObject(attrSetter);
// hold onto the current attribute name
session.getAttributes().put(WORKING_KEY, data);
// provide a prompt
session.setResponse("Attribute value: ");
// that's it, we're outta here ...
// but the new data will come straight to our I/O object
}
// This is the "fancy bit". This was pushed onto the I/O stack
// by the onCommand method so that the next available data would
// come to us here. That's should be the data in response to our
// prompt.
static private final IOObject attrSetter = new IOObject() {
public void dataReceived(IOSession session, String data) {
// we're done, so go ahead and pop the I/O stack
session.popIOObject();
// retrieve our key, take our data, set the pair as an
attribute
session.getAttributes().put(session.getAttributes().get(WORKING_KEY),
data);
// report our success
session.setResponse("Set attributes[" +
session.getAttributes().get(WORKING_KEY) + "] = " + data + "\r\n");
// and cleanup
session.getAttributes().remove(WORKING_KEY);
}
};
static public final SetAttribute singleton = new SetAttribute();
static private final String WORKING_KEY = "working.key";
}
-------------------------------------------------------------
-------------------------------------------------------------
The rest of this is just remaining supporting code, most of which I won't
bother to comment within or upon.
-------------------------------------------------------------
-------------------------------------------------------------
A trivial, polymorphic interface for receiving data. Again, it is a String
here only for my convenience. For "real" use, this should stay as bytes.
interface IOObject {
void dataReceived(IOSession session, String data);
}
-------------------------------------------------------------
interface CommandHandler {
void onCommand(IOSession session, String data);
}
-------------------------------------------------------------
public class ShowAttributes implements CommandHandler {
static public final ShowAttributes singleton = new ShowAttributes();
public void onCommand(IOSession session, String data) {
session.setResponse("Attribute values: " +
session.getAttributes().toString() + "\r\n");
}
}
-------------------------------------------------------------
---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]