content: /* The contents of this file are subject to the terms
   * of the Common Development and Distribution License
   * (the License). You may not use this file except in
   * compliance with the License.
   *
   * You can obtain a copy of the License at
   * http://www.sun.com/cddl/cddl.html or
   * install_dir/legal/LICENSE
   * See the License for the specific language governing
   * permission and limitations under the License.
   *
   * When distributing Covered Code, include this CDDL
   * Header Notice in each file and include the License file
   * at install_dir/legal/LICENSE.
   * If applicable, add the following below the CDDL Header,
   * with the fields enclosed by brackets [] replaced by
   * your own identifying information:
   * "Portions Copyrighted [year] [name of copyright owner]"
   *
   * $Id$
   *
   * Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
   */
  package com.sun.faban.harness.webclient;
  
  import com.sun.faban.harness.common.BenchmarkDescription;
  import com.sun.faban.harness.common.Config;
  import com.sun.faban.harness.common.RunId;
  import com.sun.faban.harness.engine.RunQ;
  import com.sun.faban.harness.security.AccessController;
  import org.apache.commons.fileupload.DiskFileUpload;
  import org.apache.commons.fileupload.FileItem;
  import org.apache.commons.fileupload.FileUploadException;
  import org.xml.sax.SAXException;
  import org.xml.sax.SAXParseException;
  
  import javax.servlet.ServletException;
  import javax.servlet.ServletResponse;
  import javax.servlet.http.HttpServlet;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.xml.parsers.ParserConfigurationException;
  import javax.xml.parsers.SAXParser;
  import javax.xml.parsers.SAXParserFactory;
  import java.io.*;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.List;
  import java.util.StringTokenizer;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
  /**
   * The Submitter servlet is used to submit a benchmark run from the CLI.
   *
   * @author Akara Sucharitakul
   */
  public class CLIServlet extends HttpServlet {
  
      static final int TAIL = 0;
      static final int FOLLOW = 1;
  
      static Logger logger = Logger.getLogger(CLIServlet.class.getName());
  
      String[] getPathComponents(HttpServletRequest request) {
          String pathInfo = request.getPathInfo();
  
          StringTokenizer pathTokens = null;
          int tokenCount = 0;
          if (pathInfo != null) {
              pathTokens = new StringTokenizer(pathInfo, "/");
              tokenCount = pathTokens.countTokens();
          }
          String[] comps = new String[tokenCount + 1];
          comps[0] = request.getServletPath();
          int i = 1;
          while (pathTokens != null && pathTokens.hasMoreTokens()) {
              comps[i] = pathTokens.nextToken();
              if (comps[i] != null && comps[i].length() > 0)
                  ++i;
          }
  
          if (i != comps.length) {
              String[] comps0 = new String[i];
              System.arraycopy(comps, 0, comps0, 0, i);
              comps = comps0;
          }
          return comps;
      }
  
      /**
       * Lists pending runs, obtains status, or show logs of a particular run.<ol>
       * <li>Pending: http://..../pending/</li>
       * <li>Status:  http://..../status/${runid}</li>
       * <li>Logs:    http://..../logs/${runid}</li>
       * <li>Tail Logs: http://..../logs/${runid}/tail</li>
       * <li>Follow Logs: http://..../logs/${runid}/follow</li>
       * <li>Combination of tail and follow, postfix /tail/follow</li>
       * </ol>.
       * @param request The request object
       * @param response The response object
       * @throws ServletException Error executing servlet
       * @throws IOException I/O error
       */
      public void doGet(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
  
          String[] reqC = getPathComponents(request);
          if ("/status".equals(reqC[0])) {
              sendStatus(reqC, response);
          } else if ("/pending".equals(reqC[0])) {
              sendPending(response);
          } else if ("/logs".equals(reqC[0])) {
              sendLogs(reqC, response);
          } else {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Request string " + reqC[0] + " not understood!");
          }
      }
  
      /**
       * Submits new runs or kills runs. For submission, the POST request must be
       * a multi-part POST. The first parts contain user name and password
       * information (if security is enabled). Each subsequent part contains the
       * run configuration file. The configuration file is not used for kill
       * requests.
       * <br><br>
       * Path to call this servlet is http://.../submit/${benchmark}/${profile}
       * and http://.../kill/${runId}.
       *
       * @param request The mime multi-part post request
       * @param response The response object
       * @throws ServletException Error executing servlet
       * @throws IOException I/O error
       */
      public void doPost(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
  
          // Path to call this servlet is http://...../${benchmark}/${profile}
          // And it is a post request with optional user, password, and
          // all the config files.
          String[] reqC = getPathComponents(request);
          if ("/submit".equals(reqC[0])) {
              doSubmit(reqC, request, response);
          } else if ("/kill".equals(reqC[0])) {
              doKill(reqC, request, response);
          } else {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Request string " + reqC[0] + " not understood!");
          }
      }
  
      private void sendPending(HttpServletResponse response) throws IOException {
          String[] pending = RunQ.listPending();
          if (pending == null) {
              response.sendError(HttpServletResponse.SC_NO_CONTENT,
                      "No pending runs");
          } else {
              Writer w = response.getWriter();
              for (int i = 0; i < pending.length; i++)
                  w.write(pending[i] + '\n');
              w.flush();
              w.close();
          }
      }
  
      private void sendStatus(String[] reqC, HttpServletResponse response)
              throws IOException {
          if (reqC.length < 2) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                                          "Missing RunId.");
              return;
          }
          String runId = reqC[1];
          String status = RunResult.getStatus(new RunId(runId));
          if (status == null) {
                  response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                          "No such runId: " + runId);
          } else {
              Writer w = response.getWriter();
              w.write(status + '\n');
              w.flush();
              w.close();
          }
      }
  
      private void sendLogs(String[] reqC, HttpServletResponse response)
              throws ServletException, IOException {
          if (reqC.length < 2) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                                          "Missing RunId.");
              return;
          }
          RunId runId = new RunId(reqC[1]);
          boolean[] options = new boolean[2];
          options[TAIL] = false;
          options[FOLLOW] = false;
          for (int i = 2; i < reqC.length; i++) {
              if ("tail".equals(reqC[i])) {
                  options[TAIL] = true;
              } else if ("follow".equals(reqC[i])) {
                  options[FOLLOW] = true;
              } else {
                  response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Invalid option \"" + reqC[i] + "\"."); ;
                  return;
              }
          }
          File logFile = new File(Config.OUT_DIR + runId, "log.xml");
          String status = null;
          response.setContentType("text/plain");
          PrintWriter out = response.getWriter();
          while (!logFile.exists()) {
              String[] pending = RunQ.listPending();
              if (pending == null) {
                  response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                              "RunId " + runId +" not found");
                  return;
              }
              boolean queued = false;
              for (String run : pending) {
                  if (run.equals(runId.toString())) {
                      if (status == null) {
                          status = "QUEUED";
                          out.println(status);
                          response.flushBuffer();
                      }
                      queued = true;
                      try {
                          Thread.sleep(1000);  // Check back in one sec.
                      } catch (InterruptedException e) {
                          //Noop, just look it up again.
                      }
                      break;
                  }
              }
              if (!queued) { // Either never queued or deleted from queue.
                  // Check for 10x, 100ms each to allow for start time.
                  for (int i = 0; i < 10; i++) {
                      if (logFile.exists()) {
                          status = "STARTED";
                          break;
                      }
                      try {
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          logger.log(Level.WARNING,
                                  "Interrupted checking existence of log file.");
                      }
                  }
  
                  if (!"STARTED".equals(status)) {
                      if ("QUEUED".equals(status)) { // was queued before
                          status = "DELETED";
                          out.println(status);
                          out.flush();
                          out.close();
                          return;
                      } else { // Never queued or just removed.
                          response.sendError(HttpServletResponse.SC_NOT_FOUND,
                                              "RunId " + runId +" not found");
                          return;
                      }
                  }
              }
          }
  
          LogOutputHandler handler = new LogOutputHandler(response, options);
          InputStream logInput;
          if (options[FOLLOW]) {
              // The XMLInputStream reads streaming XML and does not EOF.
              XMLInputStream input = new XMLInputStream(logFile);
              input.addEOFListener(handler);
              logInput = input;
          } else {
              logInput = new FileInputStream(logFile);
          }
          try {
              SAXParserFactory sFact = SAXParserFactory.newInstance();
              sFact.setFeature("http://xml.org/sax/features/validation", false);
              sFact.setFeature("http://apache.org/xml/features/" +
                      "allow-java-encodings", true);
              sFact.setFeature("http://apache.org/xml/features/nonvalidating/" +
                      "load-dtd-grammar", false);
              sFact.setFeature("http://apache.org/xml/features/nonvalidating/" +
                      "load-external-dtd", false);
              SAXParser parser = sFact.newSAXParser();
              parser.parse(logInput, handler);
              handler.xmlComplete = true; // If we get here, the XML is good.
          } catch (ParserConfigurationException e) {
              throw new ServletException(e);
          } catch (SAXParseException e) {
              Throwable t = e.getCause();
              // If it is caused by an IOException, we'll just throw it.
              if (t != null) {
                  if (t instanceof IOException)
                      throw (IOException) t;
                  else if (options[FOLLOW])
                      throw new ServletException(t);
              } else if (options[FOLLOW]) {
                  throw new ServletException(e);
              }
          } catch (SAXException e) {
              throw new ServletException(e);
          } finally {
              if (options[TAIL] && !options[FOLLOW]) // tail not yet printed
                  handler.eof();
          }
      }
  
      private void doKill(String[] reqC, HttpServletRequest request,
                          HttpServletResponse response) throws IOException {
          if (reqC.length < 2) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Missing RunId.");
              return;
          }
          RunId runId = new RunId(reqC[1]);
          String user = request.getParameter("sun");
          String password = request.getParameter("sp");
  
          // Check the status of the run
          boolean found = false;
          boolean queued = false;
          String terminateStatus = null;
          RunResult result = RunResult.getInstance(runId);
          if (result != null) {
              found = true;
              if ( "COMPLETED".equals(result.status) ||
                      "FAILED".equals(result.status) ||
                      "KILLED".equals(result.status) ) {
                  terminateStatus = result.status;
              }
          } else { // If not found, look in queue
              String[] pending = RunQ.listPending();
              if (pending != null) {
                  for (String run : pending) {
                      if (run.equals(runId.toString())) {
                          found = true;
                          queued = true;
                          break;
                      }
                  }
              }
          }
  
          if (found && terminateStatus == null) { // not yet terminated
  
              // First authenticate the user and make sure he/she is the CLI user.
              boolean hasPermission = true;
              if (Config.SECURITY_ENABLED) {
                  if (Config.CLI_SUBMITTER == null ||
                          Config.CLI_SUBMITTER.length() == 0 ||
                          !Config.CLI_SUBMITTER.equals(user)) {
                      hasPermission = false;
                  }
                  if (Config.SUBMIT_PASSWORD == null ||
                          Config.SUBMIT_PASSWORD.length() == 0 ||
                          !Config.SUBMIT_PASSWORD.equals(password)) {
                      hasPermission = false;
                  }
                  if (AccessController.isKillAllowed(user, runId.toString())) {
                      hasPermission = false;
                  }
              }
  
              if (hasPermission) {
  
                  // No matter of status, the run may be running by now.
                  // So check for active runs first.
                  if (RunQ.getHandle().killCurrentRun(runId.toString(), user)
                          != null) {
                      terminateStatus = "KILLING";
                  } else { // Or the run may have already terminated...
                      result = RunResult.getInstance(runId);
                      if (result != null) {
                          if ( "COMPLETED".equals(result.status) ||
                                  "FAILED".equals(result.status) ||
                                  "KILLED".equals(result.status) ) {
                              terminateStatus = result.status;
                          }
                      } else if (queued) { // Or it still is in the queue
                          RunQ.getHandle().deleteRun(runId.toString());
                          terminateStatus = "DELETED";
                      }
                  }
              } else {
                  if (queued) // Run was removed in the meantime
                      terminateStatus = "DELETED";
                  else
                      terminateStatus = "DENIED";
              }
          }
  
          if (!found) {
              response.sendError(HttpServletResponse.SC_NOT_FOUND,
                      "No such runId: " + runId);
          } else {
              Writer w = response.getWriter();
              w.write(terminateStatus + '\n');
              w.flush();
              w.close();
          }
      }
  
      private void doSubmit(String[] reqC, HttpServletRequest request,
                            HttpServletResponse response)
              throws ServletException, IOException {
          if (reqC.length < 3) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Benchmark and profile not provided in request.");
              return;
          }
          // first is the bench name
          BenchmarkDescription desc =
                  BenchmarkDescription.getDescription(reqC[1]);
          if (desc == null) {
              response.sendError(HttpServletResponse.SC_BAD_REQUEST,
                      "Benchmark " + reqC[1] + " not deployed.");
              return;
          }
          try {
              String user = null;
              String password = null;
              boolean hasPermission = true;
  
              ArrayList<String> runIdList = new ArrayList<String>();
  
              DiskFileUpload fu = new DiskFileUpload();
              // No maximum size
              fu.setSizeMax(-1);
              // maximum size that will be stored in memory
              fu.setSizeThreshold(8192);
              // the location for saving data larger than getSizeThreshold()
              fu.setRepositoryPath(Config.TMP_DIR);
  
              List fileItems = null;
              try {
                  fileItems = fu.parseRequest(request);
              } catch (FileUploadException e) {
                  throw new ServletException(e);
              }
  
              for (Iterator i = fileItems.iterator(); i.hasNext();) {
                  FileItem item = (FileItem) i.next();
                  String fieldName = item.getFieldName();
                  if (item.isFormField()) {
                      if ("sun".equals(fieldName)) {
                          user = item.getString();
                      } else if ("sp".equals(fieldName)) {
                          password = item.getString();
                      }
                      continue;
                  }
                  if (reqC[2] == null) // No profile
                      break;
  
                  if (desc == null)
                      break;
  
                  if (!"configfile".equals(fieldName))
                      continue;
  
                  if (Config.SECURITY_ENABLED) {
                      if (Config.CLI_SUBMITTER == null ||
                              Config.CLI_SUBMITTER.length() == 0 ||
                              !Config.CLI_SUBMITTER.equals(user)) {
                          hasPermission = false;
                          break;
                      }
                      if (Config.SUBMIT_PASSWORD == null ||
                              Config.SUBMIT_PASSWORD.length() == 0 ||
                              !Config.SUBMIT_PASSWORD.equals(password)) {
                          hasPermission = false;
                          break;
                      }
                  }
  
                  String usrDir = Config.PROFILES_DIR + reqC[2];
                  File dir = new File(usrDir);
                  if(dir.exists()) {
                      if(!dir.isDirectory()) {
                           logger.severe(usrDir +
                                          " should be a directory");
                          dir.delete();
                          logger.fine(dir + " deleted");
                      }
                      else
                          logger.fine("Saving parameter file to" +
                                      usrDir);
                  }
                  else {
                      logger.fine("Creating new profile directory for " +
                                  reqC[2]);
                      if(dir.mkdirs())
                          logger.fine("Created new profile directory " +
                                      usrDir);
                      else
                          logger.severe("Failed to create profile " +
                                        "directory " + usrDir);
                  }
  
  
                  // Save the latest config file into the profile directory
                  String dstFile = Config.PROFILES_DIR + reqC[2] +
                          File.separator + desc.configFileName + "." +
                          desc.shortName;
  
                  item.write(new File(dstFile));
                  runIdList.add(RunQ.getHandle().addRun(user, reqC[2], desc));
              }
  
              response.setContentType("text/plain");
              Writer writer = response.getWriter();
  
              if (!hasPermission) {
                  response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                  writer.write("Permission denied!\n");
              }
  
              if (runIdList.size() == 0)
                  writer.write("No runs submitted.\n");
              for (String newRunId : runIdList) {
                  writer.write(newRunId);
              }
  
              writer.flush();
              writer.close();
          } catch (ServletException e) {
              logger.log(Level.SEVERE, e.getMessage(), e);
              throw e;
          } catch (IOException e) {
              logger.log(Level.SEVERE, e.getMessage(), e);
              throw e;
          } catch (Exception e) {
              logger.log(Level.SEVERE, e.getMessage(), e);
              throw new ServletException(e);
          }
      }
  
      private static class LogOutputHandler extends LogParseHandler
              implements XMLInputStream.EOFListener {
  
          private ServletResponse response;
          private PrintWriter writer;
          private boolean[] options;
  
          LogRecordDetail detail = new LogRecordDetail();
          ExceptionRecord exception = new ExceptionRecord();
          StackFrame frame = new StackFrame();
          ArrayList stackFrames = new ArrayList();
          private CircularBuffer<LogRecord> recordBuffer;
  
          LogOutputHandler(PrintWriter writer, boolean[] options) {
              super(null, null, null);
              this.writer = writer;
              this.options = options;
              if (options[TAIL])
                  recordBuffer = new CircularBuffer<LogRecord>(10);
          }
  
          LogOutputHandler(ServletResponse response, boolean[] options)
                  throws IOException {
              this(response.getWriter(), options);
              this.response = response;
          }
  
          private void flush() {
              if (response != null)
                  try {
                      response.flushBuffer();
                  } catch (IOException e) {
                      // Noop. If a client socket closes, we just don't care.
                  }
              else
                  writer.flush();
          }
  
          /**
           * The processRecord method allows subclasses to define
           * how a record should be processed.
           *
           * @throws org.xml.sax.SAXException If the processing should stop.
           */
          public void processRecord() throws SAXException {
              if (options[TAIL]) {
                  recordBuffer.add(logRecord);
                  logRecord = new LogRecord(); // Don't reuse LogRecord if kept
              } else {
                  printRecord(logRecord);
              }
          }
  
          /**
           * Formats a multi-line message into text line breaks
           * for readability.
           *
           * @param message The message to be formatted.
           * @return The new formatted message.
           */
          @Override String formatMessage(String message) {
              int idx = message.indexOf("<br>");
              if (idx == -1) // If there's no <br>, don't even hassle.
                  return message;
              StringBuffer msg = new StringBuffer(message);
              String crlf = "\n";
              while (idx != -1) {
                  msg.replace(idx, idx + 4, crlf);
                  idx = msg.indexOf("<br>", idx + crlf.length());
              }
              return msg.toString();
          }
  
          /**
           * The processDetail method allows subclasses to process
           * the exceptions not processed by default. This is called
           * from endElement.
           *
           * @param qName The element qName
           * @throws org.xml.sax.SAXException If the processing should stop.
           */
          public void processDetail(String qName) throws SAXException {
              if ("millis".equals(qName))
                  detail.millis = buffer.toString().trim();
              else if ("sequence".equals(qName))
                  detail.sequence = buffer.toString().trim();
              else if ("logger".equals(qName))
                  detail.logger = buffer.toString().trim();
              else if ("message".equals(qName))
                  exception.message = buffer.toString().trim();
              else if ("class".equals(qName))
                  frame.clazz = buffer.toString().trim();
              else if ("method".equals(qName))
                  frame.method = buffer.toString().trim();
              else if ("line".equals(qName))
                  frame.line = buffer.toString().trim();
              else if ("frame".equals(qName)) {
                  stackFrames.add(frame);
                  frame = new RecordHandler.StackFrame();
              } else if ("exception".equals(qName)) {
                  RecordHandler.StackFrame[] frameArray =
                          new RecordHandler.StackFrame[stackFrames.size()];
                  exception.stackFrames =
                          (RecordHandler.StackFrame[]) stackFrames.toArray(frameArray);
                  stackFrames.clear();
                  logRecord.exceptionFlag = true;
                  logRecord.exception = exception;
                  exception = new ExceptionRecord();
              }
          }
  
          /**
           * Prints the html result of the parsing to the servlet output.
           */
          public void printHtml() {
              // We never print in html. So this is a noop here.
          }
  
          /**
           * Gets called if and when eof is hit.
           */
          public void eof() {
              if (options[TAIL]) {
                  int size = recordBuffer.size();
                  for (int i = 0; i < size; i++)
                      printRecord(recordBuffer.get(i));
                  options[TAIL] = false;
                  recordBuffer = null;
              }
              flush();
          }
  
          private void printRecord(LogRecord r) {
              // Print only the time, not the date.
              int timeIdx = r.date.indexOf('T') + 1;
              writer.println(r.date.substring(timeIdx) +
                          ':' + r.level + ':' + formatMessage(r.message));
              if (r.exception != null) {
                  writer.println(formatMessage(r.exception.message));
                  for (StackFrame s : r.exception.stackFrames) {
                      writer.println("    at " + s.clazz + '.' + s.method +
                                  " (" + s.line + ')');
                  }
                  r.exception = null;
              }
          }
      }
  
      static class CircularBuffer<E> {
  
          private int head = 0;
          private boolean wrapped = false;
          private int size = 0;
          private Object[] buffer;
  
          CircularBuffer(int capacity) {
              buffer = new Object[capacity];
          }
  
          void add(E object) {
              buffer[head] = object;
              moveHead();
              if (size < buffer.length)
                  ++size;
          }
  
          private void moveHead() {
              ++head;
              if (head >= buffer.length) {
                  head = 0;
                  wrapped = true;
              }
          }
  
          E get(int idx) {
              if (wrapped)
                  idx += head;
              if (idx >= buffer.length)
                  idx -= buffer.length;
              return (E) buffer[idx];
          }
  
          int size() {
              return size;
          }
      }
  }
  