/* * (c) Copyright 2010-2011 AgileBirds * * This file is part of OpenFlexo. * * OpenFlexo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenFlexo 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>. * */ package org.netbeans.lib.cvsclient; import java.io.File; import java.io.IOException; import java.util.Properties; import java.util.StringTokenizer; import org.netbeans.lib.cvsclient.connection.Connection; import org.netbeans.lib.cvsclient.connection.ConnectionFactory; /** * <p> * CVSRoot represents the cvsroot that identifies the cvs repository's location and the means to get to it. We use following definition of * cvsroot: * </p> * * <code> [:method:][[user][:password]@][hostname[:[port]]]/path/to/repository </code> * * <p> * When the method is not defined, we treat it as local or ext method depending on whether the hostname is present or not. This gives us two * different formats: * </p> * * <h4>1. Local format</h4> <code>[:method:]/path/to/repository</code> or <code>(:local:|:fork:)anything<code> <h4>2. Server format</h4> <code> [:method:][[user][:password]@]hostname[:[port]]/path/to/repository </code> * * <p> * There are currently 6 different methods that are implemented by 3 different connection classes. * <ul> * <li>:local:, :fork: & no-method --> LocalConnection (LOCAL_FORMAT)</li> * <li>:server: & :ext: --> SSH2Connection (SERVER_FORMAT)</li> * <li>:pserver: --> PServerConnection (SERVER_FORMAT)</li> </li> gserver and kserver are not included. Environment variables are not used * (like CVS_RSH). * </p> * <p> * local and no-method work like fork. They start the cvs server program on the local machine thus using the remote protocol on the local * machine. According to Cederqvist fork's relation to local is: "In other words it does pretty much the same thing as :local:, but various * quirks, bugs and the like are those of the remote CVS rather than the local CVS." * </p> * <p> * server is using ssh. According to Cederqvist it would use an internal RSH client but since it is not known what this exactly means it * just uses ssh. Note ssh is able only to use ssh protocol version 2 which is recommended anyways. * </p> * <p> * Note that cvsroot is case sensitive so remember to write the method in lowercase. You can succesfully construct a cvsroot that has a * different method but ConnectionFactory will be unable to create a connection for such CVSRoot. * </p> * <p> * CVSRoot object keeps the cvsroot in components that are * <ul> * <li>method</li> * <li>user</li> * <li>password</li> * <li>host</li> * <li>port</li> * <li>repository</li> * </ul> * You can change these components through setters. When you ask fo the cvsroot string representation it is constructed based on the current * values of the components. The returned cvsroot never contains the password for security reasons. Also "no-method" is always represented * as local method. * </p> */ public class CVSRoot { /** A constant representing the "local" connection method. */ public static final String METHOD_LOCAL = "local"; // NOI18N /** A constant representing the "fork" connection method. */ public static final String METHOD_FORK = "fork"; // NOI18N /** A constant representing the "server" connection method. */ public static final String METHOD_SERVER = "server"; // NOI18N /** A constant representing the "pserver" connection method. */ public static final String METHOD_PSERVER = "pserver"; // NOI18N /** A constant representing the "ext" connection method. */ public static final String METHOD_EXT = "ext"; // NOI18N // the connection method. no-method is represented by null // the value is interned for fast comparisons private String method; // user (default = null) private String username; // password (default = null) private String password; // hostname (default = null) private String hostname; // port (default = 0) 0 means that port is not used and the protocol will use default protocol private int port; // repository as string representation private String repository; /** * Parse the CVSROOT string into CVSRoot object. The CVSROOT string must be of the form * [:method:][[user][:password]@][hostname:[port]]/path/to/repository */ public static CVSRoot parse(String cvsroot) throws IllegalArgumentException { return new CVSRoot(cvsroot); } /** * Construct CVSRoot from Properties object. The names are exactly the same as the attribute names in this class. */ public static CVSRoot parse(Properties props) throws IllegalArgumentException { return new CVSRoot(props); } /** * This constructor allows to construct CVSRoot from Properties object. The names are exactly the same as the attribute names in this * class. */ protected CVSRoot(Properties props) throws IllegalArgumentException { String mtd = props.getProperty("method"); if (mtd != null) { this.method = mtd.intern(); } // host & port this.hostname = props.getProperty("hostname"); if (this.hostname.length() == 0) { this.hostname = null; // this.localFormat = this.hostname == null || this.hostname.length() == 0; } if (this.hostname != null) { // user & password (they are always optional) this.username = props.getProperty("username"); this.password = props.getProperty("password"); // host & port // We already have hostname try { int p = Integer.parseInt(props.getProperty("port")); if (p > 0) { this.port = p; } else { throw new IllegalArgumentException("The port is not a positive number."); } } catch (NumberFormatException e) { throw new IllegalArgumentException("The port is not a number: '" + props.getProperty("port") + "'."); } } // and the most important which is repository String r = props.getProperty("repository"); if (r == null) { throw new IllegalArgumentException("Repository is obligatory."); } else { this.repository = r; } } /** * Breaks the string representation of cvsroot into it's components: * * The valid format (from the cederqvist) is: * * :method:[[user][:password]@]hostname[:[port]]/path/to/repository * * Also parse alternative format from WinCVS, which stores connection parameters such as username and hostname in method options: * * :method[;option=arg...]:other_connection_data * * e.g. :pserver;username=anonymous;hostname=localhost:/path/to/repository * * For CVSNT compatability it also supports following local repository path format * * driveletter:path\\path\\path * */ protected CVSRoot(String cvsroot) throws IllegalArgumentException { int colonPosition = 0; boolean localFormat; if (cvsroot.startsWith(":") == false) { // no method mentioned guess it using heuristics localFormat = cvsroot.startsWith("/"); if (localFormat == false) { if (cvsroot.indexOf(':') == 1 && cvsroot.indexOf('\\') == 2) { // #67504 it looks like windows drive => local method = METHOD_LOCAL; repository = cvsroot; return; } colonPosition = cvsroot.indexOf(':'); if (colonPosition < 0) { // No colon => server format, but there must be a '/' in the middle int slash = cvsroot.indexOf('/'); if (slash < 0) { throw new IllegalArgumentException("CVSROOT must be an absolute pathname."); } method = METHOD_SERVER; } else { method = METHOD_EXT; } colonPosition = 0; } else { method = METHOD_LOCAL; } } else { // connection method is given so parse it colonPosition = cvsroot.indexOf(':', 1); if (colonPosition < 0) { throw new IllegalArgumentException("The connection method does not end with ':'."); } int methodNameEnd = colonPosition; int semicolonPosition = cvsroot.indexOf(";", 1); if (semicolonPosition != -1 && semicolonPosition < colonPosition) { // method has options methodNameEnd = semicolonPosition; String options = cvsroot.substring(semicolonPosition + 1, colonPosition); StringTokenizer tokenizer = new StringTokenizer(options, "=;"); while (tokenizer.hasMoreTokens()) { String option = tokenizer.nextToken(); if (tokenizer.hasMoreTokens() == false) { throw new IllegalArgumentException("Undefined " + option + " option value."); } String value = tokenizer.nextToken(); if ("hostname".equals(option)) { // NOI18N hostname = value; } else if ("username".equals(option)) { // NOI18N username = value; } else if ("password".equals(option)) { // NOI18N password = value; } if ("port".equals(option)) { // NOI18N try { port = Integer.parseInt(value, 10); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Port option must be number."); } } } } this.method = cvsroot.substring(1, methodNameEnd).intern(); // #65742 read E!e>2.0 workdirs if ("extssh".equals(method)) { // NOI18N method = METHOD_EXT; } // CVSNT supports :ssh;ver=2:username@cvs.sf.net:/cvsroot/xoops if ("ssh".equals(method)) { // NOI18N method = METHOD_EXT; } colonPosition++; // Set local format in case of :local: or :fork: methods. localFormat = isLocalMethod(this.method); } if (localFormat) { // everything after method is repository in local format this.repository = cvsroot.substring(colonPosition); } else { /* So now we parse SERVER_FORMAT :method:[[user][:password]@]hostname[:[port]]/reposi/tory ALGORITHM: - find the first '@' character - not found: - find the first ':' - not found - find the first '/' - not found - exception - found - parse hostname/path/to/repository - found - parse rest - found - find the following ':' character - not found - exception - found parse rest */ int startSearch = cvsroot.indexOf('@', colonPosition); if (startSearch < 0) { startSearch = colonPosition; } String userPasswdHost; int pathBegin = -1; int hostColon = cvsroot.indexOf(':', startSearch); if (hostColon == -1) { pathBegin = cvsroot.indexOf('/', startSearch); if (pathBegin < 0) { throw new IllegalArgumentException("cvsroot " + cvsroot + " is malformed, host name is missing."); } else { userPasswdHost = cvsroot.substring(colonPosition, pathBegin); } } else { userPasswdHost = cvsroot.substring(colonPosition, hostColon); } int at = userPasswdHost.indexOf('@'); if (at == -1) { // there is no user or password, only hostname before port if (userPasswdHost.length() > 0) { this.hostname = userPasswdHost; } } else { // there is user, password or both before hostname // up = username, password or both String up = userPasswdHost.substring(0, at); if (up.length() > 0) { int upDivider = up.indexOf(':'); if (upDivider != -1) { this.username = up.substring(0, upDivider); this.password = up.substring(upDivider + 1); } else { this.username = up; } } // hostname this.hostname = userPasswdHost.substring(at + 1); } if (hostname == null || hostname.length() == 0) { throw new IllegalArgumentException("Didn't specify hostname in CVSROOT '" + cvsroot + "'."); } /* Now we are left with port (optional) and repository after hostColon pr = possible port and repository */ if (hostColon > 0) { String pr = cvsroot.substring(hostColon + 1); int index = 0; int port = 0; char c; while (pr.length() > index && Character.isDigit(c = pr.charAt(index))) { int d = Character.digit(c, 10); port = port * 10 + d; index++; } this.port = port; if (index > 0) { pr = pr.substring(index); } if (pr.startsWith(":")) { // NOI18N pr = pr.substring(1); } this.repository = pr; } else { this.port = 0; this.repository = cvsroot.substring(pathBegin); } } } /** * Test whether this cvsroot describes a local connection or remote connection. The connection is local if and only if the host name is * <code>null</code>. E.g. for local or fork methods. */ public boolean isLocal() { return hostname == null; } /** * <ul> * <li> * <code>LOCAL_FORMAT --> :method:/reposi/tory</code> <br/> * "no method" is always represented internally as null</li> * <li> * <code>SERVER_FORMAT --> :method:user@hostname:[port]/reposi/tory</code> <br/> * Password is never included in cvsroot string representation. Use getPassword to get it.</li> * </ul> */ @Override public String toString() { if (this.hostname == null) { if (this.method == null) { return this.repository; } return ":" + this.method + ":" + this.repository; } else { StringBuffer buf = new StringBuffer(); if (this.method != null) { buf.append(':'); buf.append(this.method); buf.append(':'); } // don't put password in cvsroot if (this.username != null) { buf.append(this.username); buf.append('@'); } // hostname buf.append(this.hostname); buf.append(':'); // port if (this.port > 0) { buf.append(this.port); } // repository buf.append(this.repository); return buf.toString(); } } /** * <p> * With this method it is possible to compare how close two CVSRoots are to each other. The possible values are: * </p> * * <ul> * <li>-1 = not compatible - if none of the below match</li> * <li>0 = when equals(..) returns true</li> * <li>1 = refers to same repository on the same machine using same method on same port and same user</li> * <li>2 = refers to same repository on the same machine using same method</li> * <li>3 = refers to same repository on the same machine</li> * </ul> */ public int getCompatibilityLevel(CVSRoot compared) { if (equals(compared)) { return 0; } boolean sameRepository = isSameRepository(compared); boolean sameHost = isSameHost(compared); boolean sameMethod = isSameMethod(compared); boolean samePort = isSamePort(compared); boolean sameUser = isSameUser(compared); if (sameRepository && sameHost && sameMethod && samePort && sameUser) { return 1; } else if (sameRepository && sameHost && sameMethod) { return 2; } else if (sameRepository && sameHost) { return 3; } else { return -1; } } private boolean isSameRepository(CVSRoot compared) { if (this.repository.equals(compared.repository)) { return true; } try { if (new File(this.repository).getCanonicalFile().equals(new File(compared.repository).getCanonicalFile())) { return true; } else { return false; } } catch (IOException ioe) { // something went wrong when invoking getCanonicalFile() so return false return false; } } private boolean isSameHost(CVSRoot compared) { String comparedHostName = compared.getHostName(); if (this.hostname == comparedHostName) { return true; } if (this.hostname != null) { return this.hostname.equalsIgnoreCase(comparedHostName); } else { return false; } } private boolean isSameMethod(CVSRoot compared) { if (this.method == null) { if (compared.getMethod() == null) { return true; } else { return false; } } else if (this.method.equals(compared.getMethod())) { return true; } else { return false; } } private boolean isSamePort(CVSRoot compared) { if (this.isLocal() == compared.isLocal()) { if (this.isLocal()) { return true; } else if (this.port == compared.getPort()) { return true; } else { try { Connection c1 = ConnectionFactory.getConnection(this); Connection c2 = ConnectionFactory.getConnection(compared); // Test actual ports used by the connections. // This is necessary in case that port in CVSRoot is zero and the conection is using some default port number. return c1.getPort() == c2.getPort(); } catch (IllegalArgumentException iaex) { return false; } } } else { return false; } } private boolean isSameUser(CVSRoot compared) { String user = compared.getUserName(); if (user == getUserName()) { return true; } if (user != null) { return user.equals(getUserName()); } return false; } /** * CVSRoots are equal if their toString representations are equal. This puts some extra pressure on the toString method that should be * defined very precisely. */ @Override public boolean equals(Object o) { // This should be null safe, right? if (!(o instanceof CVSRoot)) { return false; } CVSRoot compared = (CVSRoot) o; if (toString().equals(compared.toString())) { return true; } return false; } @Override public int hashCode() { return toString().hashCode(); } /** * Get the format of this cvsroot. * * @return Either {@link #LOCAL_FORMAT} or {@link #SERVER_FORMAT}. * * public int getUrlFormat() { return urlFormat; } */ /** * Get the connection method. * * @return The connection method or <code>null</code> when no method is defined. */ public String getMethod() { return method; } /** * setting the method has effects on other components. The method might change - urlFormat - username and password - hostname/port If * urlFormat becomes LOCAL_FORMAT then username, password and hostname are set to null and port to 0. If urlFormat becomes SERVER_FORMAT * then hostname must not be null. */ protected void setMethod(String method) { if (method != null) { this.method = method.intern(); } else { method = null; } if (isLocalMethod(method)) { this.username = null; this.password = null; this.hostname = null; this.port = 0; } else { if (this.hostname == null) { throw new IllegalArgumentException("Hostname must not be null when setting a remote method."); } } } // test whether the method is "local" or "fork" private boolean isLocalMethod(String method) { return METHOD_LOCAL == method || METHOD_FORK == method; } /** * Get the user name. * * @return The user name or code>null</code> when the user name is not defined. */ public String getUserName() { return username; } /** * Set the user name. * * @param username * The user name. */ protected void setUserName(String username) { this.username = username; } /** * Get the password. * * @return The password or <code>null</code> when the password is not defined. */ public String getPassword() { return this.password; } /** * Set the password. * * @param password * The password */ protected void setPassword(String password) { this.password = password; } /** * Get the host name. * * @return The host name or <code>null</code> when the host name is not defined */ public String getHostName() { return this.hostname; } /** * Set the host name. * * @param hostname * The host name or <code>null</code> when the host name is not defined. */ protected void setHostName(String hostname) { this.hostname = hostname; } /** * Get the port number. * * @return The port number or zero when the port is not defined. */ public int getPort() { return this.port; } /** * Set the port number. * * @param port * The port number or zero when the port is not defined. */ public void setPort(int port) { this.port = port; } /** * Get the repository. * * @return The repository. This is never <code>null</code>. */ public String getRepository() { return repository; } /** * Set the repository. * * @param repository * The repository. Must not be <code>null</code>. */ protected void setRepository(String repository) { if (repository == null) { throw new IllegalArgumentException("The repository must not be null."); } this.repository = repository; } }