/******************************************************************************* * Copyright (c) 2004, 2006 * Thomas Hallgren, Kenneth Olwing, Mitch Sonies * Pontus Rydin, Nils Unden, Peer Torngren * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the individual * copyright holders listed above, as Initial Contributors under such license. * The text of such license is available at www.eclipse.org. *******************************************************************************/ package org.eclipse.buckminster.p4.internal; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.buckminster.p4.Messages; import org.eclipse.buckminster.p4.P4Plugin; import org.eclipse.buckminster.p4.internal.DepotObject.ViewEntry; import org.eclipse.buckminster.p4.preferences.Client; import org.eclipse.buckminster.runtime.BuckminsterException; import org.eclipse.buckminster.runtime.IOUtils; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.osgi.util.NLS; import org.osgi.service.prefs.BackingStoreException; /** * @author thhal */ public class Connection extends PropertyScope { /** * Maximum number of arguments before the P4Client will switch into using <code>-x <argument file></code> * notation. */ public static final int ARG_FILE_TRESHOLD = 32; private static DateFormat s_dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); //$NON-NLS-1$ private static Pattern s_depotPattern = Pattern.compile("^Depot (\\w+) .*"); //$NON-NLS-1$ private static Pattern s_tzPattern = Pattern.compile("(-?)(\\d{1,2}):?(\\d{2})$"); //$NON-NLS-1$ private static TimeZone parseTimeZone(String serverDate) { if(serverDate == null) return TimeZone.getDefault(); String[] dateSplit = serverDate.split("\\s+"); //$NON-NLS-1$ if(dateSplit.length != 4) return TimeZone.getDefault(); String tzLabel = dateSplit[3]; Matcher tzMatch = s_tzPattern.matcher(dateSplit[2]); if(!tzMatch.matches()) return TimeZone.getDefault(); boolean negative = "-".equals(tzMatch.group(1)); //$NON-NLS-1$ int hours = Integer.parseInt(tzMatch.group(2)); int minutes = Integer.parseInt(tzMatch.group(3)); int seconds = hours * 3600 + minutes * 60; if(negative) seconds = -seconds; int myZone = seconds * 1000; String myDate = dateSplit[0] + ' ' + dateSplit[1]; TimeZone okByOffset = null; String[] ids = TimeZone.getAvailableIDs(); synchronized(s_dateFormat) { try { for(String id : ids) { TimeZone candidate = TimeZone.getTimeZone(id); // Parse a Date using this candidate as the TimeZone. Then // check if that Date is in Daylight saving time. If it is, // then we need to add the DSTSavings before comparing the // raw offset. // s_dateFormat.setTimeZone(candidate); Date testDate = s_dateFormat.parse(myDate); int rawOffset = candidate.getRawOffset(); boolean isDST = candidate.inDaylightTime(testDate); if(isDST) rawOffset += candidate.getDSTSavings(); if(rawOffset == myZone) { String displayName = candidate.getDisplayName(isDST, TimeZone.SHORT); if(displayName.equals(tzLabel)) return candidate; okByOffset = candidate; } } } catch(ParseException e) { return TimeZone.getDefault(); } } return okByOffset == null ? TimeZone.getDefault() : okByOffset; } private final String m_address; private final String m_charset; private final Client m_clientPrefs; private ConnectionInfo m_info; public Connection(DepotURI depotURI) { this(depotURI.getScope(), depotURI.getClient(), depotURI.getAddress()); } public Connection(Map<String, ? extends Object> scope, Client clientPrefs, String address) { super(scope); m_charset = null; m_clientPrefs = clientPrefs; m_address = address; } public List<Map<String, String>> exec(String cmd) throws CoreException { return this.exec(cmd, null, null); } public List<Map<String, String>> exec(String cmd, String args[]) throws CoreException { return this.exec(cmd, args, null); } public List<Map<String, String>> exec(String cmd, String args[], Map<String, String> cmdInput) throws CoreException { List<String> argv = new ArrayList<String>(); // argv.add("C:\\tools\\eclipse\\plugins\\com.perforce.p4api_2004.2.3122\\win32exec.exe"); argv.add(P4Plugin.getDefault().getP4Binary()); argv.add("-G"); //$NON-NLS-1$ argv.add("-p"); //$NON-NLS-1$ argv.add(m_address); String tmp = this.expand(m_clientPrefs.getName()); if(tmp != null) { argv.add("-c"); //$NON-NLS-1$ argv.add(tmp); } tmp = this.expand(m_clientPrefs.getServer().getUser()); if(tmp != null) { argv.add("-u"); //$NON-NLS-1$ argv.add(tmp); } tmp = m_clientPrefs.getServer().getPassword(); if(tmp != null) { argv.add("-P"); //$NON-NLS-1$ argv.add(tmp); } if(m_charset != null) { argv.add("-C"); //$NON-NLS-1$ argv.add(m_charset); } if(args == null) { argv.add(cmd); return this.exec(argv, cmdInput); } if(args.length > ARG_FILE_TRESHOLD) { File argsFile = null; PrintWriter writer = null; try { argsFile = File.createTempFile("p4", ".tmp"); //$NON-NLS-1$ //$NON-NLS-2$ argsFile.deleteOnExit(); writer = new PrintWriter(new FileWriter(argsFile)); for(int i = 0; i < args.length; i++) writer.println(args[i]); writer.close(); writer = null; argv.add("-x"); //$NON-NLS-1$ argv.add(argsFile.getPath()); argv.add(cmd); return this.exec(argv, cmdInput); } catch(IOException e) { throw BuckminsterException.wrap(e); } finally { if(argsFile != null) argsFile.delete(); IOUtils.close(writer); } } argv.add(cmd); for(String arg : args) argv.add(arg); return this.exec(argv, cmdInput); } public String formatDate(Date date) throws CoreException { synchronized(s_dateFormat) { s_dateFormat.setTimeZone(this.getConnectionInfo().getTimeZone()); return s_dateFormat.format(date); } } public ClientSpec getClientSpec() throws CoreException { List<Map<String, String>> info = this.exec("info"); //$NON-NLS-1$ if(info.size() != 1) throw BuckminsterException.fromMessage(Messages.p4_info_failed); Map<String, String> clientInfo = info.get(0); ClientSpec clientSpec = new ClientSpec(this, this.exec("client", new String[] { "-o" }).get(0)); //$NON-NLS-1$ //$NON-NLS-2$ // Perforce will create a new client for us if no client existed. That client // will have a default entry that reflects the complete depot. We do not // want that. // IPath clientRoot = clientSpec.getRoot(); String localRoot = this.expand(m_clientPrefs.getLocalRoot()); IPath expectedRoot = (localRoot == null) ? null : new Path(localRoot); if("*unknown*".equals(clientInfo.get("clientName"))) //$NON-NLS-1$ //$NON-NLS-2$ { // This is a freshly created ClientSpec // clientSpec.setView(new ViewEntry[0]); // Compare as files since the Path.equals is case sensitive on all platforms // if(expectedRoot != null && !expectedRoot.toFile().equals(clientRoot.toFile())) clientSpec.setRoot(expectedRoot); clientSpec.commitChanges(); } else { if(clientRoot != null) { if(expectedRoot == null) { m_clientPrefs.setLocalRoot(clientRoot.toOSString()); try { m_clientPrefs.save(); } catch(BackingStoreException e) { throw BuckminsterException.wrap(e); } } // Compare as files since the Path.equals is case sensitive on all platforms // else if(!clientRoot.toFile().equals(expectedRoot.toFile())) { // We found a client in the perforce server that has a root // that doesn't match the root defined in our preferences. // This must be fixed and we cannot be the judge // throw new LocalRootMismatchException(this.expand(m_clientPrefs.getName()), m_address, expectedRoot, clientRoot); } } } return clientSpec; } public synchronized ConnectionInfo getConnectionInfo() throws CoreException { if(m_info != null) return m_info; List<Map<String, String>> info = this.exec("info", null, null); //$NON-NLS-1$ int numInfo = info.size(); if(numInfo == 0) return null; boolean secure = false; String user = null; String client = null; String root = null; String port = null; String date = null; Map<String, String> firstInfo = info.get(0); if("stat".equals(firstInfo.get("code"))) //$NON-NLS-1$ //$NON-NLS-2$ { for(Map.Entry<String, String> entry : firstInfo.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if(key.equals("userName")) //$NON-NLS-1$ user = value; else if(key.equals("clientName")) //$NON-NLS-1$ client = value; else if(key.equals("clientRoot")) //$NON-NLS-1$ root = value; else if(key.equals("serverAddress")) //$NON-NLS-1$ port = value; else if(key.equals("serverDate")) //$NON-NLS-1$ date = value; else if(key.equals("security") && value.equals("enabled")) //$NON-NLS-1$ //$NON-NLS-2$ secure = true; } } else { for(Map<String, String> map : info) { String data = map.get("data"); //$NON-NLS-1$ if(data != null) { int idx = data.indexOf(':'); if(idx > -1) { String key = data.substring(0, idx); String value = data.substring(idx + 1); if(key.equals("User name")) //$NON-NLS-1$ user = value; else if(key.equals("Client name")) //$NON-NLS-1$ client = value; else if(key.equals("Client root")) //$NON-NLS-1$ root = value; else if(key.equals("Server address")) //$NON-NLS-1$ port = value; else if(key.equals("Server date")) //$NON-NLS-1$ date = value; else if(key.equals("security") && value.equals("enabled")) //$NON-NLS-1$ //$NON-NLS-2$ secure = true; } } } } m_info = new ConnectionInfo(user, client, root, port, parseTimeZone(date), secure); return m_info; } public DepotFolder[] getDepots() throws CoreException { List<Map<String, String>> depotsData = this.exec("depots"); //$NON-NLS-1$ int numDepots = depotsData.size(); ArrayList<DepotFolder> depots = new ArrayList<DepotFolder>(numDepots); for(int idx = 0; idx < numDepots; ++idx) { Map<String, String> dataMap = depotsData.get(idx); String name = dataMap.get("name"); //$NON-NLS-1$ if(name == null) { String data = dataMap.get("data"); //$NON-NLS-1$ if(data != null) { Matcher matcher = s_depotPattern.matcher(data); if(matcher.matches()) name = matcher.group(1); } } if(name != null) { depots.add(new DepotFolder(this, Collections.<String, String> singletonMap("dir", "//" + name), FileSpec.HEAD)); //$NON-NLS-1$ //$NON-NLS-2$ } } return depots.toArray(new DepotFolder[depots.size()]); } public DepotFile getFile(FileSpec path) throws CoreException { List<DepotFile> files = this.getFiles(new FileSpec[] { path }, false); return (files.size() > 0) ? files.get(0) : null; } public List<DepotFile> getFiles(FileSpec paths[], boolean includeDeleted) throws CoreException { int idx = paths.length; if(idx == 0) return Collections.<DepotFile> emptyList(); String[] pathStrings = new String[idx]; while(--idx >= 0) pathStrings[idx] = paths[idx].toString(); return this.getFiles(this.exec("fstat", pathStrings), includeDeleted); //$NON-NLS-1$ } public DepotFolder[] getFolders(IPath path, FileSpec.Specifier revision) throws CoreException { List<Map<String, String>> data = this.exec("dirs", new String[] { path.toString() }); //$NON-NLS-1$ int numDepots = data.size(); DepotFolder depots[] = new DepotFolder[numDepots]; for(int idx = 0; idx < numDepots; ++idx) depots[idx] = new DepotFolder(this, data.get(idx), revision); return depots; } public Label getLabel(String labelName) throws CoreException { List<Map<String, String>> data = this.exec("label", new String[] { "-o", labelName }); //$NON-NLS-1$ //$NON-NLS-2$ if(data.size() == 1) { Label label = new Label(this, data.get(0)); if(label.getAccess() != null || label.getUpdate() != null) return label; } return null; } public Label[] getLabels(IPath path) throws CoreException { List<Map<String, String>> data = this.exec("labels", new String[] { path.toString() }); //$NON-NLS-1$ int numLabels = data.size(); Label labels[] = new Label[numLabels]; for(int idx = 0; idx < numLabels; ++idx) { Map<String, String> entry = data.get(idx); // P4 stupidity. The "labels" command returns a map with "label" // but the "label" command returns "Label". Sigh... // String label = entry.remove("label"); //$NON-NLS-1$ if(label != null) entry.put("Label", label); //$NON-NLS-1$ labels[idx] = new Label(this, entry); } return labels; } /** * Returns the last change number for the given <code>path</code>. The <code>path</code> must denote a folder. The * command will append "/..." to the path and, in case <code>qualifier</code> is not null or an empty * string, also append "@<qualifier>" * * @param path * The folder that limits the search for a change number. * @param qualifier * An optional qualifier (typically a label) that limits the search. * @return The latest change number or -1 if no change could be found. * @throws CoreException */ public long getLastChangeNumber(IPath path, String qualifier) throws CoreException { String stringPath = path.append("...").toString(); //$NON-NLS-1$ if(qualifier != null && qualifier.length() > 0) stringPath += '@' + qualifier; List<Map<String, String>> data = this .exec("changes", new String[] { "-m", "1", "-s", "submitted", stringPath }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ if(data.size() == 1) { String change = data.get(0).get("change"); //$NON-NLS-1$ if(change != null) return Long.parseLong(change); } return -1; } public Date parseDate(String perforceDate) throws CoreException { synchronized(s_dateFormat) { s_dateFormat.setTimeZone(this.getConnectionInfo().getTimeZone()); try { return s_dateFormat.parse(perforceDate); } catch(ParseException e) { throw BuckminsterException.wrap(e); } } } public void setClientSpec(Map<String, String> values) throws CoreException { this.exec("client", new String[] { "-i" }, values); //$NON-NLS-1$ //$NON-NLS-2$ } public String[] where(IPath path) throws CoreException { Map<String, String> result = this.exec("where", new String[] { path.toString() }).get(0); //$NON-NLS-1$ String data = result.get("data"); //$NON-NLS-1$ String paths[] = new String[3]; if(data != null) { paths = new String[3]; // Locate second path that starts with '//' // int second = data.indexOf("//", 2); //$NON-NLS-1$ if(second < 0) throw BuckminsterException.fromMessage(NLS.bind(Messages.weird_responds_from_p4_where_0, data)); int secondEnd = data.indexOf(' ', second); if(secondEnd < 0) throw BuckminsterException.fromMessage(NLS.bind(Messages.weird_responds_from_p4_where_0, data)); int third = secondEnd + 1; while(Character.isWhitespace(data.charAt(third))) ++third; int firstEnd = second - 1; while(firstEnd >= 0 && Character.isWhitespace(data.charAt(firstEnd))) --firstEnd; ++firstEnd; paths[0] = data.substring(0, firstEnd); paths[1] = data.substring(second, secondEnd); paths[2] = data.substring(third); } else { paths[0] = result.get("depotFile"); //$NON-NLS-1$ paths[1] = result.get("clientFile"); //$NON-NLS-1$ paths[2] = result.get("path"); //$NON-NLS-1$ } return paths; } List<DepotFile> getFiles(List<Map<String, String>> fileInfos, boolean includeDeleted) { ArrayList<DepotFile> files = new ArrayList<DepotFile>(fileInfos.size()); for(Map<String, String> fileInfo : fileInfos) { String depotPath = fileInfo.get("depotFile"); //$NON-NLS-1$ if(depotPath != null) { if(includeDeleted || !"delete".equals(fileInfo.get("headAction"))) //$NON-NLS-1$ //$NON-NLS-2$ files.add(new DepotFile(this, fileInfo)); } } return files; } private List<Map<String, String>> exec(List<String> cmdLine, Map<String, String> cmdInput) throws CoreException { final Process process; OutputStream procOut = null; try { process = Runtime.getRuntime().exec(cmdLine.toArray(new String[cmdLine.size()])); procOut = process.getOutputStream(); if(cmdInput != null) { PythonOutputStream output = new PythonOutputStream(procOut); output.writeObject(cmdInput); output.flush(); } } catch(IOException e) { throw BuckminsterException.wrap(e); } finally { IOUtils.close(procOut); } final String[] inThreadException = new String[1]; final ArrayList<Map<String, String>> results = new ArrayList<Map<String, String>>(); Thread inThread = new Thread() { @Override public void run() { InputStream procIn = null; try { procIn = process.getInputStream(); PythonInputStream input = new PythonInputStream(procIn); for(;;) { Map<String, String> result = input.readStringMap(); if(result == null) break; results.add(result); } } catch(IOException e) { inThreadException[0] = e.getMessage(); } finally { IOUtils.close(procIn); } } }; final StringBuilder errorBuilder = new StringBuilder(); Thread errThread = new Thread() { @Override public void run() { InputStream procErr = null; try { procErr = process.getErrorStream(); InputStreamReader rdr = new InputStreamReader(procErr, "US-ASCII"); //$NON-NLS-1$ char[] buf = new char[1024]; int errCnt = rdr.read(buf); if(errCnt > 0) { do { errorBuilder.append(buf, 0, errCnt); errCnt = rdr.read(buf); } while(errCnt > 0); } } catch(IOException e) { // We ignore this for now. Errors reading the error stream // is not of major interest anyway. } finally { IOUtils.close(procErr); } } }; inThread.start(); errThread.start(); int exitCode; try { exitCode = process.waitFor(); inThread.join(); errThread.join(); if(inThreadException[0] != null) throw BuckminsterException.fromMessage(inThreadException[0]); } catch(InterruptedException e) { throw BuckminsterException.wrap(e); } if(exitCode != 0) { for(Map<String, String> result : results) { if("error".equals(result.get("code"))) //$NON-NLS-1$ //$NON-NLS-2$ throw BuckminsterException.fromMessage(result.get("data").toString()); //$NON-NLS-1$ } String error = errorBuilder.toString(); if(error.length() == 0) throw BuckminsterException.fromMessage(Messages.process_died_with_exit_code_0, Integer .valueOf(exitCode)); throw BuckminsterException.fromMessage(error); } return results; } }