com.tek42.perforce.parse.AbstractPerforceTemplate.java Source code

Java tutorial

Introduction

Here is the source code for com.tek42.perforce.parse.AbstractPerforceTemplate.java

Source

/*
 *   P4Java - java integration with Perforce SCM
 *   Copyright (C) 2007-,  Mike Wille, Tek42
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Lesser General Public
 *   License as published by the Free Software Foundation; either
 *   version 2.1 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public
 *   License along with this library; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *   You can contact the author at:
 *
 *   Web:   http://tek42.com
 *   Email:   mike@tek42.com
 *   Mail:   755 W Big Beaver Road
 *         Suite 1110
 *         Troy, MI 48084
 */

package com.tek42.perforce.parse;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FilterWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.slf4j.Logger;

import com.tek42.perforce.Depot;
import com.tek42.perforce.PerforceException;
import com.tek42.perforce.process.Executor;
import hudson.plugins.perforce.PerforceSCM;
import hudson.plugins.perforce.utils.TimedStreamCloser;
import java.io.InputStream;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang.time.StopWatch;

/**
 * Provides default functionality for interacting with Perforce using the template design pattern.
 * 
 * @author Mike Wille
 */
public abstract class AbstractPerforceTemplate {
    private static final String p4errors[] = new String[] { "Connect to server failed; check $P4PORT",
            "Perforce password (P4PASSWD) invalid or unset.",
            "Password not allowed at this server security level, use 'p4 login'",
            "Can't create a new user - over license quota.", "Client '*' can only be used from host '*'",
            "Access for user '", "Your session has expired, please login again.",
            "You don't have permission for this operation.", "Password invalid.", "The authenticity of", };

    private static final int P4_EXECUTOR_CHECK_PERIOD = 2000;

    @SuppressWarnings("unused")
    private transient Logger logger; // Obsolete field, present just to keep demarshaller happy
    @SuppressWarnings("unused")
    private transient String errors[]; // Obsolete field, present just to keep demarshaller happy

    private final Depot depot;
    final transient String maxError = "Request too large";

    public AbstractPerforceTemplate(Depot depot) {
        this.depot = depot;
    }

    public Logger getLogger() {
        if (depot.getLogger() != null) {
            return depot.getLogger();
        } else {
            return LoggerFactory.getLogger(this.getClass());
        }
    }

    /**
    * Parses lines of formatted text for a list of values. Tokenizes each line into columns and adds the column
    * specified by index to the list.
    * 
    * @param response   The response from perforce to parse
    * @param index      The column index to add to the list
    * @return   A List of strings parsed from the response
    */
    protected List<String> parseList(StringBuilder response, int index) {
        StringTokenizer lines = new StringTokenizer(response.toString(), "\n\r");
        List<String> list = new ArrayList<String>(100);
        while (lines.hasMoreElements()) {
            StringTokenizer columns = new StringTokenizer(lines.nextToken());
            for (int column = 0; column < index; column++) {
                columns.nextToken();
            }
            list.add(columns.nextToken());
        }
        return list;

    }

    /**
     * Check to see if the perforce request resulted in a "too many results" error.  If so, special handling needs
     * to happen.
     *
     * @param response The response from perforce
     * @return   True if the limit was reached, false otherwise.
     */
    protected boolean hitMax(StringBuilder response) {
        return response.toString().startsWith(maxError);
    }

    /**
     * Used to filter the response from perforce so the API can throw out 
     * useless lines and thus save memory during large operations.
     * ie. synced/refreshed lines from 'p4 sync'
     */
    public abstract static class ResponseFilter {
        public abstract boolean accept(String line);

        public boolean reject(String line) {
            return !accept(line);
        }
    }

    /**
     * Adds any extra parameters that need to be applied to all perforce commands. For example, adding the login ticket
     * to authenticate with.
     * 
     * @param cmd
     *            String array that will be executed
     * @return A (possibly) modified string array to be executed in place of the original.
     */
    protected String[] getExtraParams(String cmd[]) {
        String ticket = depot.getP4Ticket();

        if (ticket != null) {
            // Insert the ticket for the password if tickets are being used...
            String newCmds[] = new String[cmd.length + 2];
            newCmds[0] = getP4Exe();
            newCmds[1] = "-P";
            newCmds[2] = ticket;
            for (int i = 3; (i - 2) < cmd.length; i++) {
                newCmds[i] = cmd[i - 2];
            }
            cmd = newCmds;
        } else {
            cmd[0] = getP4Exe();
        }
        return cmd;
    }

    /**
     * Handles the IO for opening a process, writing to it, flushing, closing, and then handling any errors.
     * 
     * @param object   The perforce object to save
     * @param builder   The builder responsible for saving the object
     * @throws PerforceException   If there is any errors thrown from perforce
     */
    @SuppressWarnings("unchecked")
    protected void saveToPerforce(Object object, Builder builder) throws PerforceException {
        boolean loop = false;
        boolean attemptLogin = true;

        //StringBuilder response = new StringBuilder();
        do {
            int mesgIndex = -1;//, count = 0;
            Executor p4 = depot.getExecFactory().newExecutor();
            String debugCmd = "";
            try {
                String cmds[] = getExtraParams(builder.getSaveCmd(getP4Exe(), object));

                // for exception reporting...
                for (String cm : cmds) {
                    debugCmd += cm + " ";
                }

                // back to our regularly scheduled programming...
                p4.exec(cmds);
                BufferedReader reader = p4.getReader();

                // Maintain a log of what was sent to p4 on std input
                final StringBuilder log = new StringBuilder();

                // Conditional use of std input for saving the perforce entity
                if (builder.requiresStandardInput()) {
                    BufferedWriter writer = p4.getWriter();
                    Writer fwriter = new FilterWriter(writer) {
                        public void write(String str) throws IOException {
                            log.append(str);
                            out.write(str);
                        }
                    };
                    builder.save(object, fwriter);
                    fwriter.flush();
                    fwriter.close();
                }

                String line;
                StringBuilder error = new StringBuilder();
                StringBuilder info = new StringBuilder();
                int exitCode = 0;

                while ((line = reader.readLine()) != null) {

                    // Check for authentication errors...
                    if (mesgIndex == -1)
                        mesgIndex = checkAuthnErrors(line);

                    if (mesgIndex != -1) {
                        error.append(line);

                    } else if (line.startsWith("error")) {
                        if (!line.trim().equals("") && (line.indexOf("up-to-date") < 0)
                                && (line.indexOf("no file(s) to resolve") < 0)) {
                            error.append(line.substring(6));
                        }

                    } else if (line.startsWith("exit")) {
                        exitCode = Integer.parseInt(line.substring(line.indexOf(" ") + 1, line.length()));

                    } else {
                        if (line.indexOf(":") > -1)
                            info.append(line.substring(line.indexOf(":")));
                        else
                            info.append(line);
                    }
                }
                reader.close();

                loop = false;
                // If we failed to execute because of an authentication issue, try a p4 login.
                if (mesgIndex == 1 || mesgIndex == 2 || mesgIndex == 6 || mesgIndex == 9) {
                    if (attemptLogin) {
                        // password is unset means that perforce isn't using the environment var P4PASSWD
                        // Instead it is using tickets. We must attempt to login via p4 login, then
                        // retry this cmd.
                        p4.close();
                        trustIfSSL();
                        login();
                        loop = true;
                        attemptLogin = false;
                        mesgIndex = -1; // cancel this error for now
                        continue;
                    }
                }

                if (mesgIndex != -1 || exitCode != 0) {
                    if (error.length() != 0) {
                        error.append("\nFor Command: ").append(debugCmd);
                        if (log.length() > 0) {
                            error.append("\nWith Data:\n===================\n");
                            error.append(log);
                            error.append("\n===================\n");
                        }
                        throw new PerforceException(error.toString());
                    }
                    throw new PerforceException(info.toString());
                }

            } catch (IOException e) {
                throw new PerforceException("Failed to open connection to perforce", e);
            } finally {
                try {
                    p4.getWriter().close();
                } catch (IOException e) {
                    //failed to close pipe, but we can't do much about that
                }
                try {
                    p4.getReader().close();
                } catch (IOException e) {
                    //failed to close pipe, but we can't do much about that
                }
                p4.close();
            }
        } while (loop);
    }

    /**
    * Executes a perforce command and returns the output as a StringBuilder.
    * 
    * @param cmd   The perforce commands to execute.  Each command and argument is it's own array element
    * @return   The response from perforce as a stringbuilder
    * @throws PerforceException   If perforce throws any errors
    */
    protected StringBuilder getPerforceResponse(String cmd[]) throws PerforceException {
        return getPerforceResponse(cmd, new ResponseFilter() {
            @Override
            public boolean accept(String line) {
                return true;
            }
        });
    }

    protected StringBuilder getPerforceResponse(String origcmd[], ResponseFilter filter) throws PerforceException {
        // TODO: Create a way to wildcard portions of the error checking.  Add method to check for these errors.
        boolean loop = false;
        boolean attemptLogin = true;

        List<String> lines = null;
        int totalLength = 0;

        do {
            int mesgIndex = -1, count = 0;
            Executor p4 = depot.getExecFactory().newExecutor();

            String debugCmd = "";
            // get entire cmd to execute
            String cmd[] = getExtraParams(origcmd);

            // setup information for logging...
            for (String cm : cmd) {
                debugCmd += cm + " ";
            }

            // Perform execution and IO
            p4.exec(cmd);
            BufferedReader reader = p4.getReader();
            String line = null;
            totalLength = 0;
            lines = new ArrayList<String>(1024);
            TimedStreamCloser timedStreamCloser = null;
            try {
                PerforceSCM.PerforceSCMDescriptor scmDescr = PerforceSCM.getInstance();
                p4.getWriter().close();
                int timeout = -1;
                if (scmDescr.hasP4ReadlineTimeout()) { // Implementation with timeout
                    timeout = scmDescr.getP4ReadLineTimeout();
                }
                timedStreamCloser = new TimedStreamCloser(p4.getInputStream(), timeout);
                timedStreamCloser.start();

                while ((line = reader.readLine()) != null) {
                    timedStreamCloser.reset();
                    // only check for errors if we have not found one already
                    if (mesgIndex == -1)
                        mesgIndex = checkAuthnErrors(line);
                    if (filter.reject(line))
                        continue;
                    lines.add(line);
                    totalLength += line.length();
                    count++;
                }
                if (timedStreamCloser.timedOut()) {
                    throw new PerforceException("Perforce operation timed out after " + timeout + " seconds.");
                }
            } catch (IOException ioe) {
                //this is generally not anything to worry about.  The underlying
                //perforce process terminated and that causes java to be angry
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw, true);
                ioe.printStackTrace(pw);
                pw.flush();
                sw.flush();
                getLogger().warn("Perforce process terminated suddenly");
                getLogger().warn(sw.toString());

                try {
                    p4.getWriter().close();
                } catch (IOException e) {
                    getLogger().warn("Write pipe failed to close.");
                }

                try {
                    p4.getReader().close();
                } catch (IOException e) {
                    getLogger().warn("Read pipe failed to close.");
                }

                p4.close();

                // If the project was interrupted, p4 needs to be killed
                // or it will continue running. In the worst case it will
                // still synchronize gigabytes of data into the workspace
                p4.kill();
            } finally {
                if (timedStreamCloser != null)
                    timedStreamCloser.interrupt();
                try {
                    p4.getWriter().close();
                } catch (IOException e) {
                    getLogger().warn("Write pipe failed to close.");
                }
                try {
                    p4.getReader().close();
                } catch (IOException e) {
                    getLogger().warn("Read pipe failed to close.");
                }
                p4.close();
            }
            loop = false;
            // If we failed to execute because of an authentication issue, try a p4 login.
            if (attemptLogin && (mesgIndex == 1 || mesgIndex == 2 || mesgIndex == 6 || mesgIndex == 9)) {
                // password is unset means that perforce isn't using the environment var P4PASSWD
                // Instead it is using tickets. We must attempt to login via p4 login, then
                // retry this cmd.
                p4.close();
                trustIfSSL();
                login();
                loop = true;
                attemptLogin = false;
                continue;
            }

            // We aren't using the exact message because we want to add the username for more info
            if (mesgIndex == 4)
                throw new PerforceException(
                        "Access for user '" + depot.getUser() + "' has not been enabled by 'p4 protect'");
            if (mesgIndex != -1)
                throw new PerforceException(p4errors[mesgIndex]);
            if (count == 0)
                throw new PerforceException("No output for: " + debugCmd);
        } while (loop);

        StringBuilder response = new StringBuilder(totalLength + lines.size());
        for (String line : lines) {
            response.append(line);
            response.append("\n");
        }

        return response;
    }

    /**
     * Executes a p4 command and returns the output as list of lines.
     * 
     * TODO Introduce a method that handles prefixed messages (i.e. "p4 -s <sub-command>"),
     * and can thus stop reading once if reads the "exit: <exit-code>" line, which
     * should avoid the "expected" Exception at EOF.
     * 
     * @param cmd
     *      The perforce command to execute.  The command and arguments are
     *      each in their own array element (e.g. cmd = {"p4", "info"}).
     * @return
     *      The response from perforce as a list
     * @throws PerforceException 
     */
    protected List<String> getRawPerforceResponseLines(String cmd[]) throws PerforceException {
        List<String> lines = new ArrayList<String>(1024);

        Executor p4 = depot.getExecFactory().newExecutor();
        String debugCmd = "";
        // get entire cmd to execute
        cmd = getExtraParams(cmd);

        // setup information for logging...
        for (String cm : cmd) {
            debugCmd += cm + " ";
        }

        // Perform execution and IO
        p4.exec(cmd);

        try {
            BufferedReader reader = p4.getReader();
            p4.getWriter().close();
            String line = null;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException ioe) {
            //this is generally not anything to worry about.  The underlying
            //perforce process terminated and that causes java to be angry.

            // TODO Given the above comment, should we bother to log a warning?
            // See this blog for a discussion of IOException with message "Write end dead" from pipes:
            //      http://techtavern.wordpress.com/2008/07/16/whats-this-ioexception-write-end-dead/

            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw, true);
            ioe.printStackTrace(pw);
            pw.flush();
            sw.flush();
            getLogger().warn("IOException reading from Perforce process (may just be EOF)");
            getLogger().warn(sw.toString());
        } finally {
            try {
                p4.getWriter().close();
            } catch (IOException e) {
                getLogger().warn("Write pipe failed to close.");
            }
            try {
                p4.getReader().close();
            } catch (IOException e) {
                getLogger().warn("Read pipe failed to close.");
            }
            p4.close();
        }

        return lines;
    }

    /**
     * Used by calls that make use of p4.exe's python dictionary output format.
     * @param cmd
     * @return
     * @throws PerforceException
     */

    protected byte[] getRawPerforceResponseBytes(String cmd[]) throws PerforceException {
        List<Byte> bytes = new ArrayList<Byte>(1024);

        Executor p4 = depot.getExecFactory().newExecutor();
        String debugCmd = "";
        // get entire cmd to execute
        cmd = getExtraParams(cmd);

        // setup information for logging...
        for (String cm : cmd) {
            debugCmd += cm + " ";
        }

        // Perform execution and IO
        p4.exec(cmd);

        try {
            byte[] cbuf = new byte[1024];
            InputStream input = p4.getInputStream();
            p4.getWriter().close();
            int readCount = -1;
            while ((readCount = input.read(cbuf, 0, 1024)) != -1) {
                for (int i = 0; i < readCount; i++) {
                    bytes.add(new Byte((byte) (cbuf[i] & 0xff)));
                }
            }
        } catch (IOException ioe) {
            //this is generally not anything to worry about.  The underlying
            //perforce process terminated and that causes java to be angry.

            // TODO Given the above comment, should we bother to log a warning?
            // See this blog for a discussion of IOException with message "Write end dead" from pipes:
            //      http://techtavern.wordpress.com/2008/07/16/whats-this-ioexception-write-end-dead/

            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw, true);
            ioe.printStackTrace(pw);
            pw.flush();
            sw.flush();
            getLogger().warn("IOException reading from Perforce process (may just be EOF)");
            getLogger().warn(sw.toString());
        } finally {
            try {
                p4.getWriter().close();
            } catch (IOException e) {
                getLogger().warn("Write pipe failed to close.");
            }
            try {
                p4.getReader().close();
            } catch (IOException e) {
                getLogger().warn("Read pipe failed to close.");
            }
            p4.close();
        }
        byte[] byteArray = new byte[bytes.size()];
        for (int i = 0; i < bytes.size(); i++) {
            byteArray[i] = bytes.get(i).byteValue();
        }
        return byteArray;
    }

    /**
    * Tries to perform a p4 login if the security level on the server is set to level 3 and no ticket was set via
    * depot.setP4Ticket().
    * <p>
    * Unfortunately, this likely doesn't work on windows.
    * 
    * @throws PerforceException   If perforce throws any errors
    */
    protected void login() throws PerforceException {

        try {
            // try the default location for p4 executable
            String ticket = null;
            try {
                ticket = p4Login(getP4Exe());
            } catch (PerforceException e) {
                // Strange error under hudson's execution of unit tests.  It appears
                // that the environment is not setup correctly from within hudson.  The sh shell
                // cannot find the p4 executable.  So we'll try again with a hard coded path.
                // Though, I don't believe this problem exists outside of the build environment, 
                // and wouldn't normally worry, I still want to be able to test security level 3
                // from the automated build...
                getLogger().warn("Login with '" + getP4Exe() + "' failed: " + e.getMessage());
                try {
                    ticket = p4Login("/usr/bin/p4");
                } catch (PerforceException e1) {
                    // throw the original exception and not the one caused by the workaround
                    getLogger().warn("Attempt to workaround p4 executable location failed", e1);
                    throw e;
                }
            }

            // if we obtained a ticket, save it for later use. Our environment setup by Depot can't usually
            // see the .p4tickets file.
            if (ticket != null && !ticket.contains("Enter password:")) {
                getLogger().warn("Using p4 issued ticket.");
                depot.setP4Ticket(ticket);
            }

        } catch (IOException e) {
            throw new PerforceException("Unable to login via p4 login due to IOException: " + e.getMessage());
        }
    }

    /**
     * Read the last line of output which should be the ticket.
     * 
     * @param p4Exe the perforce executable with or without full path information
     * @return the p4 ticket
     * @throws IOException if an I/O error prevents this from working
     * @throws PerforceException if the execution of the p4Exe fails
     */
    private String p4Login(String p4Exe) throws IOException, PerforceException {
        Executor login = depot.getExecFactory().newExecutor();
        login.exec(new String[] { p4Exe, "login", "-a", "-p" });

        try {
            // "echo" the password for the p4 process to read
            BufferedWriter writer = login.getWriter();
            try {
                writer.write(depot.getPassword() + "\n");
            } finally {
                // help the writer move the data
                writer.flush();
            }
            // read the ticket from the output
            String ticket = null;
            BufferedReader reader = login.getReader();
            String line;
            // The line matching ^[0-9A-F]{32}$ will be the ticket
            while ((line = reader.readLine()) != null) {
                int error = checkAuthnErrors(line);
                if (error != -1)
                    throw new PerforceException("Login attempt failed: " + line);
                if (line.trim().matches("^[0-9A-F]{32}$"))
                    ticket = line;
            }

            return ticket;
        } finally {
            login.close();
        }
    }

    /**
     * Trust the perforce server if using SSL
     */
    private void trustIfSSL() throws PerforceException {
        Executor trust = depot.getExecFactory().newExecutor();
        String p4Port = depot.getPort();
        if (p4Port.toLowerCase().startsWith("ssl:")) {
            trust.exec(new String[] { getP4Exe(), "-p", depot.getPort(), "trust", "-y" });
            try {
                trust.getWriter().close();
                BufferedReader reader = trust.getReader();
                String line;
                // The line matching ^[0-9A-F]{32}$ will be the ticket
                while ((line = reader.readLine()) != null) {
                    int error = checkAuthnErrors(line);
                    if (error != -1)
                        throw new PerforceException("Trust attempt failed: " + line);
                }
            } catch (IOException e) {
                throw new PerforceException("Could not establish ssl trust with perforce server", e);
            }
            trust.close();
        }
    }

    /**
     * Check for authentication errors.
     * 
     * @param line the perforce response line
     * @return the index in the p4errors array or -1
     */
    private int checkAuthnErrors(String line) {
        for (int i = 0; i < p4errors.length; i++) {
            if (line.indexOf(p4errors[i]) != -1)
                return i;

        }
        return -1;
    }

    protected String getP4Exe() {
        return depot.getExecutable();
    }
}