com.sun.faban.harness.webclient.CLIServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.sun.faban.harness.webclient.CLIServlet.java

Source

/* 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;
        }
    }
}