// CGIplus.java
// Compile using: $ javac "CGIplus.java"
//
// The Java may be a bit brain-dead ... still very much the novice!
// This class can be expected to undergo refinement as expertise grows.
// Developed using the first-release JDK1.1 beta kit for OpenVMS Alpha.

import java.io.*;
import java.util.Vector;

/**
 * This class allows CGI scripting to be supported relatively simply using
 * Java.  Relies on the WASD HTTPd CGIplus environment providing the CGI
 * variables in a data stream rather than as process environment variables.
 * <P>
 * Hence scripts must be activated via mapping rules that execute them within
 * the CGIplus environment, although the scripts themselves do not have to
 * "persist" in the usual CGIplus script manner, just "System.exit(0)" when
 * finished processing (at the slight cost of destroying the subprocess).
 * <P>
 * Currently supports GET and "x-www-form-urlencoded" POST HTTP method scripts.
 * Will be expanded to more fully support POST processing as time permits.
 *
 * @version  1.0.0, 09-DEC-1997
 * @author   MGD
 */
public class CGIplus {

   private static int usageCount = 0,
                      cgiVarCount = 0,
                      formFieldCount = 0,
                      nextCgiVarCount = 0,
                      nextFormFieldCount = 0;

   private static FileReader bodyf = null;

   private static BufferedReader cgipi = null, 
                                 bodys = null; 

   private static Vector cgiVariables = null,
                         urlEncodedForm = null;

   private static String cgiPlusEof = null,
                         requestBody = null,
                         line = null;

//////////////////////////////////////////////////////////////////////////////

/**
 * Begin script processing, and synchronize persistant (CGIplus) scripts.
 * For the first call checks for the CGIPLUSEOF string (if not available
 * reports it as an error and exits), then opens the CGIPLUSIN stream.
 * Wait for a request to become available (first record read and discarded).
 * Then read series of records up until the first empty record (indicates end
 * of CGI variables), placing each of these records into a vector object for
 * later search and retrieval.
 *
 * @param none
 * @return none
 * @exception none
 */
   public void begin () {

      if (cgiPlusEof == null) {

         // get the end-of-file (script output) marker
         cgiPlusEof = System.getProperty("cgipluseof");
         if (cgiPlusEof == null) {
            System.out.println("Sorry ... from a CGIplus environment only!");
            System.exit(0);
         }

         try {
            cgipi = new BufferedReader(new FileReader("CGIPLUSIN:"));
         }
         catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
         }
      }

      try {
         // synchronize on the first line and discard
         line = cgipi.readLine();

         // get rest of CGI variables
         cgiVariables = new Vector(32);
         cgiVarCount = 0;
         while ((line = cgipi.readLine()) != null) {
            if (line.equals("")) break;
            cgiVariables.addElement(line);
            cgiVarCount++;
         }
      }
      catch (Exception e) {
         e.printStackTrace();
         System.exit(0);
      }

      usageCount++;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Conclude the response.
 * Write the CGIplus end-of-output value to the server.
 * Release any resources not relevant to next request (if CGIplus).
 *
 * @param none
 * @return none
 * @exception none
 */
   public void end () {

      if (cgiPlusEof == null) begin();

      System.out.print(cgiPlusEof);
      
      // allow the major resources to be GCed while quiescent (if CGIplus)

      cgiVariables = null;
      urlEncodedForm = null;

      // close input streams

      if (bodys != null) {
         try {
            bodys.close();
         }
         catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
         }
         bodys = null;
      }

      if (bodyf != null) {
         try {
            bodyf.close();
         }
         catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
         }
         bodyf = null;
      }
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Return the number of times the script has been used.
 * Relevant only when behaving as a persistant, CGIplus script.
 *
 * @param none
 * @return none
 * @exception none
 */
   public int getUsageCount () {

      return usageCount;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Return the value of the specified CGI variable name.
 * It is less expensive but not mandatory to supply the the "WWW_" prefix.
 * Returns null if the specified variable name does not exist.
 *
 * @param varName the name of the CGI variable
 * @return the CGI variable string
 * @exception none
 */
   public String getCgiVar (String varName) {

      int  cnt;

      if (cgiPlusEof == null) begin();

      if (!varName.startsWith("WWW_")) {
         varName = "WWW_" + varName;
      }
      for (cnt = 0; cnt < cgiVariables.size(); cnt++) {
         line = (String)cgiVariables.elementAt(cnt);
         if (!line.startsWith(varName+"=")) continue;
         return line.substring(varName.length()+1);
      }
      return null;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Successive calls return each CGI variable 'name=value' pair.
 * Returns null and resets when the CGI variables are exhausted.
 *
 * @param none
 * @return the 'name=value' string
 * @exception none
 */
   public String nextCgiVar () {

      if (cgiPlusEof == null) begin();

      if (nextCgiVarCount >= cgiVariables.size()) {
         nextCgiVarCount = 0;
         return null;
      }
      return (String)cgiVariables.elementAt(nextCgiVarCount++);
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Return the number of CGI variables available.
 *
 * @param none
 * @return count of CGI variables
 * @exception none
 */
   public int getCgiVarCount () {

      if (cgiPlusEof == null) begin();
      return cgiVarCount;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Output all CGI variable 'name=value' pairs (for debugging, etc.)
 *
 * @param none
 * @return none
 * @exception none
 */
   public void dumpCgiVar () {

      int  cnt;

      if (cgiPlusEof == null) begin();

      for (cnt = 0; cnt < cgiVariables.size(); cnt++) {
         System.out.print((String)cgiVariables.elementAt(cnt));
      }
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Returns the MIME content-type of the body of a POSTed request.
 *
 * @param none
 * @return body content-type
 * @exception none
 */
   public String getContentType () {

      return getCgiVar("WWW_CONTENT_TYPE");
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Tests whether the request is POSTed and "www-form-urlencoded".
 *
 * @param none
 * @return true or false
 * @exception none
 */
   public boolean isPOSTedForm () {

      String type, method;

      method = getCgiVar("WWW_REQUEST_METHOD");
      if (!method.equals("POST")) return false;
      type = getCgiVar("WWW_CONTENT_TYPE");
      return type.equals("application/x-www-form-urlencoded");
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Returns the content-length of the body for a POSTed request.
 *
 * @param none
 * @return length of POSTed body
 * @exception none
 */
   public int getContentLength () {

      return Integer.parseInt(getCgiVar("WWW_CONTENT_LENGTH"));
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Read a line from the body of the request (for POSTed requests).
 *
 * @param none
 * @return the line (or null if body exhausted)
 * @exception none
 */
   public String readBodyLine () {

      if (bodys == null) {

         try {
            bodys = new BufferedReader(bodyf = new FileReader("HTTP$INPUT:"));
         }
         catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
         }
      }

      try {
         return bodys.readLine();
      }
      catch (Exception e) {
         e.printStackTrace();
         System.exit(0);
      }
      return null;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Output all lines in request body (for debugging POSTed requests, etc.)
 *
 * @param none
 * @return none
 * @exception none
 */
   public void dumpBody () {

      if (cgiPlusEof == null) begin();
      while ((line = readBodyLine()) != null)
         System.out.print(line);
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Read entire request body into 'field=value' pairs.
 * For a 'x-www-form-urlencoded', POSTed request.
 *
 * @param none
 * @return none
 * @exception none
 */

   // forms tend to be on the small side, read entire body into one string!
   private void readUrlEncodedForm () {

      int idx1, idx2;

      if (cgiPlusEof == null) begin();

      line = getContentType();
      if (!line.equals("application/x-www-form-urlencoded")) {
         System.out.println("Not an \"application/x-www-form-urlencoded\" request.");
         System.exit(0);
      }

      requestBody = "";
      while ((line = readBodyLine()) != null) {
         requestBody += line;
      }

      urlEncodedForm = new Vector(32);

      for (idx1 = idx2 = 0;;)
      {
         idx2 = requestBody.indexOf('&',idx1);
         if (idx2 >= 0) {
            line = urlDecode(requestBody.substring(idx1,idx2));
            urlEncodedForm.addElement(line);
            idx1 = idx2 + 1;
            formFieldCount++;
         }
         else {
            if (idx1 < requestBody.length()) {
               line = urlDecode(requestBody.substring(idx1));
               urlEncodedForm.addElement(line);
               formFieldCount++;
            }
            break;
         }
      }
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Output all form 'field=value' pairs (for debugging, etc).
 * For a 'x-www-form-urlencoded', POSTed request.
 *
 * @param none
 * @return the 'name=value' string
 * @exception none
 */
   public void dumpForm () {

      int  cnt;

      if (urlEncodedForm == null) readUrlEncodedForm();
      if (urlEncodedForm == null) return;

      for (cnt = 0; cnt < urlEncodedForm.size(); cnt++) {
         System.out.print((String)urlEncodedForm.elementAt(cnt));
      }
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Return the value of the specified form field name. 
 * Returns null if the specified field name does not exist.
 * For a 'x-www-form-urlencoded', POSTed request.
 *
 * @param fieldName the name of the field
 * @return the (URL-decoded) field value
 * @exception none
 */
   public String getFormField (String fieldName) {

      int  cnt;

      if (cgiPlusEof == null) begin();
      if (urlEncodedForm == null) readUrlEncodedForm();

      for (cnt = 0; cnt < urlEncodedForm.size(); cnt++) {
         line = (String)urlEncodedForm.elementAt(cnt);
         if (!line.startsWith(fieldName+"=")) continue;
         return line.substring(fieldName.length()+1);
      }
      return null;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Successive calls return each form field 'name=value' pair.
 * Returns null and resets when the form fields are exhausted.
 * For a 'x-www-form-urlencoded', POSTed request.
 *
 * @param none
 * @return the 'name=value' string
 * @exception none
 */
   public String nextFormField () {

      if (cgiPlusEof == null) begin();
      if (urlEncodedForm == null) readUrlEncodedForm();

      if (nextFormFieldCount >= urlEncodedForm.size()) {
         nextFormFieldCount = 0;
         return null;
      }
      return (String)urlEncodedForm.elementAt(nextFormFieldCount++);
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Return the number of form fields.
 * For a 'x-www-form-urlencoded', POSTed request.
 *
 * @param none
 * @return count of fields in request
 * @exception none
 */
   public int getFormFieldCount () {

      if (cgiPlusEof == null) begin();
      if (urlEncodedForm == null) readUrlEncodedForm();
      if (urlEncodedForm == null) return 0;
      return formFieldCount;
   }

//////////////////////////////////////////////////////////////////////////////

/**
 * Decode the supplied URL-encoded string.
 * Converts '+' into ' ' and "%nn" hex-encoded values into their ASCII
 * characters.
 *
 * @param estr the url-encoded string
 * @return the decoded string
 * @exception none
 */
   public String urlDecode (String estr) {

      int  ridx, eidx;
      char  ch;
      char[] dstr;

      if (estr == null) return null;

      dstr = new char[estr.length()];
      ridx = 0;

      for (eidx = 0; eidx < estr.length(); eidx++)
      {
         ch = estr.charAt(eidx);
         if (ch == '+') {
            dstr[ridx++] = ' ';
         }
         else if (ch == '%') {
            try {
               dstr[ridx++] = (char)
                  Integer.parseInt(estr.substring(eidx+1,eidx+3), 16);
               eidx += 2;
            }
            catch (NumberFormatException e) {
               System.out.println(estr.substring(eidx+1,eidx+3) +
                                  " is an invalid hexadecimal code");
               e.printStackTrace();
               System.exit(0);
            }
         }
         else {
            dstr[ridx++] = ch;
         }
      }

      return String.valueOf(dstr,0,ridx);
   }

//////////////////////////////////////////////////////////////////////////////

}
