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