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