/* **************************************************************************
 %name: %
 %version: %
 %date_modified: %

 Copyright (c) 1997,1998 Novell, Inc. All Rights Reserved.

 THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND TREATIES.
 USE AND REDISTRIBUTION OF THIS WORK IS SUBJECT TO THE LICENSE AGREEMENT
 ACCOMPANYING THE SOFTWARE DEVELOPMENT KIT (SDK) THAT CONTAINS THIS WORK.
 PURSUANT TO THE SDK LICENSE AGREEMENT, NOVELL HEREBY GRANTS TO DEVELOPER A
 ROYALTY-FREE, NON-EXCLUSIVE LICENSE TO INCLUDE NOVELL'S SAMPLE CODE IN ITS
 PRODUCT. NOVELL GRANTS DEVELOPER WORLDWIDE DISTRIBUTION RIGHTS TO MARKET,
 DISTRIBUTE, OR SELL NOVELL'S SAMPLE CODE AS A COMPONENT OF DEVELOPER'S
 PRODUCTS. NOVELL SHALL HAVE NO OBLIGATIONS TO DEVELOPER OR DEVELOPER'S
 CUSTOMERS WITH RESPECT TO THIS CODE.
****************************************************************************/

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

import com.novell.service.session.*;
import com.novell.service.session.util.Debug;

/**
 * This is a generic shell that accepts commands from an input stream
 * and executes these commands.  There is a current JNDI context associated
 * with the shell, to facilitate relative context operations.  There are
 * general-purpose methods that will resolve absolute names, relative
 * composite names, and relative atomic names.  There is a mechanism for
 * registering and deregistering commands.  There are methods that allow
 * access to the input and output streams associated with the shell.
 */
public class SessionShell implements Runnable
{
   static final String quoteChar = "\"";
   static final String commentChars = "#;";

   /**
    * Stream from which commands are read.
    */
   InputStream i;

   /**
    * Stream to which results are output
    */
   OutputStream o;

   /**
    * If this flag is true, and this shell is not a child shell, upon
    * reading the last command, the input will be redirected from whatever
    * source it is retrieving commands, and will retrieve commands from
    * System.in.  This allows the specification of a setup script to
    * acheive a certain state before giving control to the keyboard.
    */
   boolean continueWStdIn;

   /**
    * If this flag is true, and an exception is thrown inside a command,
    * a stack trace is printed for that exception.
    */
   boolean verbose;

   Hashtable cmds = new Hashtable ();
   Session currSession;
   String prompt = "";
   boolean shouldExit = false;
   int nestedLevel = 0;

   /**
    * Constructs a child shell, copying all parameters from the parent, as
    * needed.
    *
    * @param      shell          (in) parent shell.
    * @param      initCtx        (in) an optional initial context.  If null,
    *                            then the current context is copied over
    *                            from the parent shell.
    * @param      i              (in) input stream for the child.  If null,
    *                            then the input stream of the parent is
    *                            used.
    * @param      o              (in) output stream for the child.  If null,
    *                            then the output stream of the parent is
    *                            used.
    */
   public SessionShell (
      SessionShell shell,
      Session initSession,
      InputStream i,
      OutputStream o)
   {
      this.i = (i == null ? shell.i : i);
      this.o = (o == null ? shell.o : o);
      this.continueWStdIn = shell.continueWStdIn;
      this.verbose = shell.verbose;

      if (initSession != null)
      {
         this.currSession = initSession;
         this.prompt = "";
      }
      else
      {
         this.currSession = shell.currSession;
         this.prompt = shell.prompt;
      }

      this.cmds = (Hashtable) shell.cmds.clone ();
      this.nestedLevel = shell.nestedLevel + 1;
   }

   /**
    * Constructs a new shell with an input stream.
    *
    * <p>All output will be written to a sink, and not displayed.
    *
    * @param      initCtx        (in) context to use as root of the shell
    * @param      i              (in) input stream to read commands from
    */
   public SessionShell (Session initSession, InputStream i)
   {
      this (initSession, i, null);
   }

   /**
    * Constructs a new shell with an input stream and an output stream.
    *
    * @param      initSession        (in) context to use as root of the shell
    * @param      i              (in) input stream to read commands from
    * @param      o              (in) output stream to write results to
    *                            (can be null)
    */
   public SessionShell (Session initSession, InputStream i, OutputStream o)
   {
      this (initSession, i, o, false, false);
   }

   /**
    * Constructs a new shell with an input stream, an output stream, and
    * other options.
    *
    * @param      initSession        (in) context to use as root of the shell
    * @param      i              (in) input stream to read commands from
    * @param      o              (in) output stream to write results to
    *                            (can be null)
    * @param      continueWStdIn (in) If this flag is true, and this shell
    *                            is not a child shell, upon
    *                            reading the last command, the input
    *                            will be redirected from whatever
    *                            source it is retrieving commands,
    *                            and will retrieve commands from
    *                            System.in.
    * @param      verbose        (in) If this flag is true, and an
    *                            exception is thrown inside a command,
    *                            a stack trace is printed for that
    *                            exception.
    */
   public SessionShell (
      Session initSession,
      InputStream i,
      OutputStream o,
      boolean continueWStdIn,
      boolean verbose)
   {
      this.currSession = initSession;

      this.i = i;
      this.o = (o == null ? new NullOutputStream () : o);

      this.continueWStdIn = continueWStdIn;
      this.verbose = verbose;
   }

   /**
    * Prints the exception, handling nested exceptions up to 5 levels.
    *
    * @param      cmdName        (in) command name that generated the
    *                            original exception.
    * @param      output         (in) output stream to write the exception
    *                            to.
    * @param      e              (in) exception to print.
    */
   private void printException (String cmdName, PrintWriter output,
         Throwable e)
   {
      // one exception has already happened, pass in level as 1

      printException (cmdName, output, e, 1);
   }

   /**
    * Prints the exception, handling nested exceptions up to 5 levels.
    *
    * <p>When exceptions occur, when trying to print an exception out,
    * this method is called recursively, with a 5 level deep limit.
    *
    * @param      cmdName        (in) command name that generated the
    *                            original exception.
    * @param      output         (in) output stream to write the exception
    *                            to.
    * @param      e              (in) exception to print.
    * @param      level          (in) current recursion level.
    */
   private void printException (String cmdName, PrintWriter output,
         Throwable e, int level)
   {
      // we have to put a try/catch around the exception
      //   printing code, because an exception can
      //   be generated while printing the toString() of
      //   the original exception
      try
      {
         if (level < 2)
            output.print (cmdName + ": ");

         output.print ("exception: ");

         // if the verbose option is specified, then print a
         //   stack trace
         if (verbose)
         {
            Debug.dumpException(e, output);
         }
         else
         {
            output.println (e);
            if (true == Debug.getOutputToFile())
               Debug.dumpException(e);
         }

         /*
         FileOutputStream fos = new FileOutputStream("shell.out", true);
         OutputStreamWriter osw = new OutputStreamWriter(fos);
         PrintWriter pw = new PrintWriter(osw);

         pw.println("---" + new Date().toString());
         Debug.dumpException(e, pw);

         pw.flush();
         pw.close();
         */
      }
      catch (Throwable realBad)
      {
         if (level >= 5)
         {
            output.println ("error: over 5 nested exceptions");
            output.println ("last exception thrown: " +
                  realBad.getClass ().getName ());
         }
         else
            printException (cmdName, output, realBad, level + 1);
      }
   }

   /**
    * Main control loop of the shell.  This loop will only exit when
    * a ShellException is thrown from a command with the 'exitFlag'
    * set true.
    */
   public void run ()
   {
      // get a print stream, so we can print messages
      PrintWriter output = new PrintWriter (o);
      LineReader lineReader = new LineReader (new InputStreamReader(i));
      boolean done = false;

loop: while (!done)
      {
         // print prompt
         output.print (prompt + ">");
         output.flush ();

         String cmdLine = null;

         // read in the command line
         try
         {
            cmdLine = lineReader.readLine ();
         }
         catch (IOException e)
         {
            output.println ("fatal: IOException when reading input...");
            break loop;
         }

         // if not EOF yet
         if (cmdLine != null)
         {
            boolean doneParsing = false;
            Vector tokens = new Vector ();
            int tokenCount = 0;
            String cmdName = null;
            String token;
            int quoteParsePos = 0;

            // check for comment characters, but not inside quotes
            if (cmdLine.length () > 0)
            {
               boolean insideQuote = false;
               char[] chars = cmdLine.toCharArray ();

               for (int i = 0; i < chars.length; i++)
               {
                  if (SessionShell.quoteChar.charAt (0) == chars[i])
                     insideQuote = !insideQuote;

                  if (!insideQuote &&
                      SessionShell.commentChars.indexOf (chars[i]) != -1)
                  {
                     cmdLine = cmdLine.substring (0, i);
                     break;
                  }
               }
            }

            StringTokenizer t = new StringTokenizer (cmdLine);

            while (!(!t.hasMoreTokens () || doneParsing))
            {
               token = t.nextToken ();

               // a token that starts with the quote signifies the beginning
               //   of a quoted item
               if (token.startsWith (SessionShell.quoteChar))
               {
                  // go back to the raw command line, and pull the literal
                  //   between the quotes.
                  quoteParsePos = cmdLine.indexOf (token, quoteParsePos) + 1;
                  int endQuotePos = cmdLine.indexOf (SessionShell.quoteChar,
                        quoteParsePos);

                  if (endQuotePos < 0)
                  {
                     output.println ("error: invalid quotes in command");
                     continue loop;
                  }

                  // pull off any more tokens until we get a token with
                  //   a quote in it -- that signifies end of quotation
                  // Side effect: "Trees and more words"xxx_not_seen
                  //   will look like this token: 'Trees and more words'
                  if (token.lastIndexOf (SessionShell.quoteChar) < 1)
                  {
                     while (t.hasMoreTokens ())
                     {
                        if (t.nextToken ().indexOf (SessionShell.quoteChar) != -1)
                           break;
                     }
                  }

                  token = cmdLine.substring (quoteParsePos, endQuotePos);
               }

               tokenCount++;

               // the first token is the command name
               if (tokenCount == 1)
                  cmdName = token;
               // all other tokens are added to a Vector
               else
                  tokens.addElement (token);
            }

            // if a command was entered, then handle it, otherwise just
            //   go to the prompt again
            if (tokenCount > 0)
            {
               // search for entered command
               Command command = (Command) cmds.get (cmdName);
               String[] cmdArgs;

               if (command == null)
               {
                  output.println ("error: unrecognized command: " + cmdName);
               }
               else
               {
                  try
                  {
                     // pack all args in a String[]
                     cmdArgs = new String[tokenCount - 1];

                     for (int i = 0; i < tokenCount - 1; i++)
                        cmdArgs[i] = (String) tokens.elementAt (i);

                     command.execute (this, cmdArgs);

                     // if the verbose option is specified, then print an
                     //   affirmative message saying that the command was
                     //   successful
                     if (verbose)
                        output.println (cmdName + ": sucessful");
                  }
                  catch (ShellException e)
                  {
                     // catch special shell exception, check to see if it
                     //   means that we should exit the shell
                     String message = e.getMessage ();

                     if (message != null)
                        output.println (cmdName + ": " + e.getMessage ());

                     done = e.shouldExit ();
                  }
                  catch (Exception e)
                  {
                     // display exception, verbose flag is taken into account
                     // if an exception is thrown when printing an exception,
                     //   then this method recurses to print the offending
                     //   exception
                     printException (cmdName, output, e);
                  }
               }
            }
         }
         // EOF signifies end of the input stream
         else
         {
            // if continueWStdIn flag is true and this shell is not
            //   a child shell, then redirect input from input stream
            //   to System.in
            if (continueWStdIn && nestedLevel == 0)
            {
               lineReader = new LineReader(
                     new InputStreamReader(i = System.in));

               continueWStdIn = false;
            }
            else
               done = true;
         }
      }

      if (nestedLevel == 0)
         output.println ("exiting SessionShell...");

      output.flush ();
   }

   /**
    * Binds a command to a string command name in the shell.
    */
   public void addCommand (String cmdName, Command cmd)
   {
      cmds.put (cmdName, cmd);
   }

   /**
    * Unbinds a command from a string command name in the shell.
    */
   public Command removeCommand (String cmdName)
   {
      return ((Command) cmds.remove (cmdName));
   }

   /**
    * Returns the command bound to the specified command name.
    */
   public Command getCommand (String cmdName)
   {
      return ((Command) cmds.get (cmdName));
   }

   /**
    * Returns all command names bound to the shell.
    */
   public String[] getCommandNames ()
   {
      String[] arr = new String[cmds.size ()];
      Enumeration enum = cmds.keys ();
      int i = 0;

      while (enum.hasMoreElements ())
         arr[i++] = (String) enum.nextElement ();

      return (arr);
   }

   /**
    * Clears all bound commands in the shell.
    */
   public void clearCommands ()
   {
      cmds.clear ();
   }

   /**
    * Get current session.
    */
   public Session getCurrSession ()
   {
      return (currSession);
   }

   /**
    * Set current session.
    */
   public void setCurrSession (Session session)
   {
      currSession = session;
   }

   /**
    * Set prompt.
    */
   public String setPrompt (String prompt)
   {
      String save = this.prompt;

      this.prompt = prompt;

      return (save);
   }

   /**
    * Return verbose state.
    */
   public boolean getVerbose ()
   {
      return (verbose);
   }

   /**
    * Set verbose state.
    */
   public void setVerbose (boolean verbose)
   {
      this.verbose = verbose;
   }

   /**
    * Get input stream attached to shell.
    */
   public InputStream getInputStream ()
   {
      return (i);
   }

   /**
    * Get output stream attached to shell.
    */
   public OutputStream getOutputStream ()
   {
      return (o);
   }
}
