Producer & Consumer Message Handling
Audience Level
Intermediate to advanced
Summary
Two functions to implement basic message passing between ASP pages using a store of text files. A worked example follows the source. Recently I discovered file-based PHP Sessions and the functions below can be used in pretty much the same way but for ASP.
Source Code
Function: ServerMessageProducer()
Description:
Returns:
History:
20040928 1859BST v1 Andrew Urquhart Created
20050127 0247GMT v1.1 Andrew Urquhart Modified to load and save files using ADODB.Stream
*/
function ServerMessageProducer() {
try {
// PRIVATE METHOD DEFINITIONS
function getRandomString(intLength) {
try {
if (!intLength) {
throw new Error(1, "Required parameter \"intLength\" was not defined");
}
var strRand = [];
var strRandAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var strRandAlphabetLen = strRandAlphabet.length;
for (var tloop=0; tloop<intLength; ++tloop) {
strRand.push(strRandAlphabet.charAt(Math.floor(Math.random() * strRandAlphabetLen))); // Produce chars in ASCII range 0-9a-zA-Z
}
return strRand.join("");
}
catch (err) {
propError(err, arguments);
}
}
// PUBLIC METHOD DEFINITIONS
function getId() {
try {
return strId;
}
catch (err) {
propError(err, arguments);
}
}
function set(strFieldName, strFieldValue) {
try {
if (typeof strFieldName != "string" || objReBadInput.test(strFieldName)) {
throw new Error(1, "String expected for parameter \"name\" or string contained unsuitable characters");
}
if (typeof strFieldValue != "string") {
throw new Error(2, "String expected for parameter \"value\"");
}
if (blnSaved) {
throw new Error(3, "Unabled to set new values after message has been saved");
}
objMessage[strFieldName] = strFieldValue;
}
catch (err) {
propError(err, arguments);
}
}
function save() {
try {
if (blnSaved) {
throw new Error(1, "Message could not be saved - it has already been saved once before");
}
var objContent = [];
for (var i in objMessage) {
objContent.push("[name]");
objContent.push(i.replace(objReEncode, "\\$1"));
objContent.push("[value]");
objContent.push(objMessage[i].replace(objReEncode, "\\$1"));
}
var strContent = objContent.join("\r\n");
var objStream = Server.CreateObject("ADODB.Stream");
objStream.Open();
try {
objStream.CharSet = "UTF-8";
objStream.WriteText(strContent);
objStream.SaveToFile(strFilePath, adSaveCreateNotExist);
}
catch (err) {
throw err;
}
finally {
objStream.Close(); // Always close the stream regardless of what happens
}
blnSaved = true;
}
catch (err) {
propError(err, arguments);
}
}
// PRIVATE VARIABLES
var objMessage = [];
var strId = getRandomString(32); // The identifier for this message
var blnSaved = false;
var objReBadInput = RegExp().compile("^[\\s\\n\\r]*$", "gm");
var objReEncode = RegExp().compile("(\\[|\\])", "g");
var strFileRoot = Application("SERVERMESSAGES_PATH");
var strFilePath = strFileRoot + strId + ".dat";
// PUBLIC METHODS
this.getId = getId; // Only provide a method rather than a public property as this prevents people from externally setting the strId
this.set = set;
this.save = save;
}
catch (err) {
propError(err, arguments);
}
}
/*
Function: ServerMessageConsumer()
Description:
Returns:
History:
20040928 1858BST v1 Andrew Urquhart Created
*/
function ServerMessageConsumer(init_strId) {
try {
// PRIVATE METHOD DEFINITIONS
function clearUp() {
try {
if (blnDeleted) {
throw new Error(1, "Message already deleted");
}
if (!blnLoaded) {
throw new Error(2, "Unabled to delete a message that has not been loaded");
}
var strFilePath = strFileRoot + strId + ".dat";
var objFSO = Server.CreateObject("Scripting.FileSystemObject");
if (!objFSO.FileExists(strFilePath)) {
throw new Error(3, "Unable to delete message. Message does not exist for id=\"" + strId + "\"");
}
objFSO.DeleteFile(strFilePath, true);
blnDeleted = true;
}
catch (err) {
propError(err, arguments);
}
}
function load(param_strId) {
try {
if (typeof param_strId != "string") {
throw new Error(1, "String expected for parameter \"id\"");
}
if (blnDeleted) {
throw new Error(2, "Message no longer exists");
}
if (blnLoaded) {
throw new Error(3, "Message has already been loaded");
}
var strNewFilepath = strFileRoot + param_strId + ".dat";
var objFSO = Server.CreateObject("Scripting.FileSystemObject");
if (!objFSO.FileExists(strNewFilepath)) {
throw new Error(4, "Message not found for id=\"" + param_strId + "\"");
}
objFSO = null;
var objStream = Server.CreateObject("ADODB.Stream");
objStream.Open();
try {
objStream.CharSet = "UTF-8";
objStream.LoadFromFile(strNewFilepath);
var strFieldName = "";
var strFieldValue = "";
var intState = 0; // Means expecting fieldname delimiter next line
while (!objStream.EOS) {
var line = objStream.ReadText(adReadLine);
if (intState == 0 && objReFieldName.test(line)) {
intState = 1; // Means expecting fieldname next line
}
else if (intState == 1) {
if (objReBadInput.test(line)) {
throw new Error(5, "Saved message contained invalid characters. Message may be corrupt.");
}
strFieldName = line.replace(objReDecode, "$1");
intState = 2; // Means expecting fieldvalue delimiter next line
}
else if (intState == 2 && objReFieldValue.test(line)) {
intState = 3; // Means expecting fieldvalue next line
}
else if (intState == 3) {
strFieldValue += line;
intState = 4; // Means expecting more input for fieldvalue or fieldname delimiter
}
else if (intState == 4) {
if (objReFieldName.test(line)) {
// Reached end of input, save name value pair to message object and reset for next pair
objMessage[strFieldName] = strFieldValue.replace(objReDecode, "$1");
strFieldName = strFieldValue = "";
intState = 1; // Already seen fieldname delimiter line, expect field name next
}
else {
// More input for field value (NB: add newline that was consumed by ReadLine())
strFieldValue += "\r\n" + line;
}
}
}
if (intState == 4) {
// At end of file the save condition doesn't fire, cope with it here
objMessage[strFieldName] = strFieldValue.replace(objReDecode, "$1");
}
}
catch (err) {
throw new Error(err.number, "Loading failed:\r\n" + err.description);
}
finally {
// Loading a file overrides the default instantiated id and filepath values
blnLoaded = true;
strId = param_strId;
strFilePath = strNewFilepath;
objStream.Close(); // Always close the stream regardless of what happens
}
}
catch (err) {
propError(err, arguments);
}
}
// PUBLIC METHOD DEFINITIONS
function get(strFieldName) {
try {
if (typeof strFieldName != "string" || objReBadInput.test(strFieldName)) {
throw new Error(1, "String expected for parameter \"strFieldName\" or string contained unsuitable characters");
}
return objMessage[strFieldName];
}
catch (err) {
propError(err, arguments);
}
}
function getAll() {
try {
return objMessage;
}
catch (err) {
propError(err, arguments);
}
}
// PRIVATE VARIABLES
var objMessage = [];
var strId = null;
var blnLoaded = false;
var blnDeleted = false;
var objReBadInput = RegExp().compile("^[\\s\\n\\r]*$", "gm");
var objReDecode = RegExp().compile("\\\\(\\[|\\])", "g");
var objReFieldName = RegExp().compile("^\\[name\\]$");
var objReFieldValue = RegExp().compile("^\\[value\\]$");
var strFileRoot = Application("SERVERMESSAGES_PATH");
var strFilePath = strFileRoot + init_strId + ".dat";
// PUBLIC METHODS
this.get = get;
this.getAll = getAll;
// CONSTRUCTOR LOGIC
if (init_strId) {
if (typeof init_strId != "string") {
throw new Error(1, "String \"id\" parameter expected in constructor");
}
try {
load(init_strId);
// If load successful, 'strId' is no longer null
if (!strId) {
throw new Error(2, "Unable to read message id \"" + init_strId + "\" - unknown error");
}
}
catch (err) {
throw err;
}
finally {
// Always delete the message even the message wasn't read
clearUp();
}
}
else {
throw new Error(3, "Constructor expects parameter");
}
}
catch (err) {
propError(err, arguments);
}
}
Download
Example Usage
Here's an example from my contact form. When a user submits an invalid response (e.g the contact form message is blank) I save a message on the server and transfer processing from the form submission script back to the form display script like so:
objServerMessage.set("strSubject", strSubject);
objServerMessage.set("strName", strName);
objServerMessage.set("strEmail", strEmail);
objServerMessage.set("strMessage", strMessage);
objServerMessage.set("strErrorMessage", "[ YOUR ERROR MESSAGE TEXT HERE ]");
objServerMessage.save();
Response.Redirect("/contact/?errorMsgId=" + objServerMessage.getId());
After the redirect the form display script notices the querystring messageId, looks for the saved message, reads it then deletes it:
if (errorMsgId) {
var objServerMessage = null;
try {
objServerMessage = new ServerMessageConsumer(errorMsgId);
}
catch (err) {
// Message didn't exist or has expired
}
if (objServerMessage) {
// Re-populate form values.
// NB: Before Response.Write()'ing these variables you must Server.HTMLEncode() them first!
subject = objServerMessage.get("strSubject");
name = objServerMessage.get("strName");
email = objServerMessage.get("strEmail");
message = objServerMessage.get("strMessage");
var strErrorMessage = objServerMessage.get("strErrorMessage");
Response.Write("<h2>Submission Errors!</h2><div class=\"error\">" + strErrorMessage + "</div>");
}
}
Since there is a risk of messages not being automatically consumed I also include the following in the global.asa's Application_OnStart() to remove messages older than one day:
var objFSO = new ActiveXObject("Scripting.FileSystemObject");
var objFolder = objFSO.GetFolder(Application("SERVERMESSAGES_PATH"));
var intExpiryDate = new Date(new Date().valueOf() - 24*3600*1000).valueOf();
for (var fc=new Enumerator(objFolder.files); !fc.atEnd(); fc.moveNext()) {
var objFile = fc.item();
var intDateCreated = new Date(objFile.DateCreated).valueOf();
if (intDateCreated <= intExpiryDate) {
objFile.Delete();
}
}
Notes
If you encounter syntax errors with the ADO constants (i.e. “adReadLine”)
then you need to add the ADODB Metadata Type library declaration to the top of your
“global.asa”, e.g.: