/*
* ====================================================================
* Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://svnkit.com/license.html
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
* ====================================================================
*/
package org.tmatesoft.svn.core;
import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.util.SVNLogType;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.StringTokenizer;
/**
* The <b>SVNURL</b> class is used for representing urls. Those SVNKit
* API methods, that need repository locations to carry out an operation,
* receive a repository location url represented by <b>SVNURL</b>. This
* class does all the basic work for a caller: parses an original url
* string (splitting it to components), encodes/decodes a path component
* to/from the UTF-8 charset, checks for validity (such as protocol support
* - if SVNKit does not support a particular protocol, <b>SVNURL</b>
* throws a corresponding exception).
*
* <p>
* To create a new <b>SVNURL</b> representation, pass an original url
* string (like <span class="javastring">"http://userInfo@host:port/path"</span>)
* to a corresponding <i>parse</i> method of this class.
*
* @version 1.3
* @author TMate Software Ltd.
* @since 1.2
* @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a>
*/
public class SVNURL {
/**
* Creates a new <b>SVNURL</b> representation from the given url
* components.
*
* @param protocol a protocol component
* @param userInfo a user info component
* @param host a host component
* @param port a port number
* @param path a path component
* @param uriEncoded <span class="javakeyword">true</span> if
* <code>path</code> is UTF-8 encoded,
* <span class="javakeyword">false</span>
* otherwise
* @return a new <b>SVNURL</b> representation
* @throws SVNException if the resultant url (composed of the given
* components) is malformed
*/
public static SVNURL create(String protocol, String userInfo, String host, int port, String path, boolean uriEncoded) throws SVNException {
if ((host == null && !"file".equalsIgnoreCase(protocol)) || (host != null && host.indexOf('@') >= 0)) {
SVNErrorManager.error(SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Invalid host name ''{0}''", host), SVNLogType.DEFAULT);
}
path = path == null ? "/" : path;
if (!uriEncoded) {
path = SVNEncodingUtil.uriEncode(path);
} else {
path = SVNEncodingUtil.autoURIEncode(path);
}
if (path.length() > 0 && path.charAt(0) != '/') {
path = "/" + path;
}
if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
path = path.substring(0, path.length() - 1);
}
protocol = protocol == null ? "http" : protocol.toLowerCase();
String errorMessage = null;
if (userInfo != null && userInfo.indexOf('/') >= 0) {
errorMessage = "Malformed URL: user info part could not include '/' symbol";
} else if (host == null && !"file".equals(protocol)) {
errorMessage = "Malformed URL: host could not be NULL";
} else if (!"file".equals(protocol) && host.indexOf('/') >= 0) {
errorMessage = "Malformed URL: host could not include '/' symbol";
}
if (errorMessage != null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, errorMessage);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
String url = composeURL(protocol, userInfo, host, port, path);
return new SVNURL(url, true);
}
/**
* Parses the given decoded (not UTF-8 encoded) url string and creates
* a new <b>SVNURL</b> representation for this url.
*
* @param url an input url string (like <span class="javastring">'http://myhost/mypath'</span>)
* @return a new <b>SVNURL</b> representation of <code>url</code>
* @throws SVNException if <code>url</code> is malformed
*/
@Deprecated
public static SVNURL parseURIDecoded(String url) throws SVNException {
return new SVNURL(url, false);
}
/**
* Parses the given UTF-8 encoded url string and creates a new
* <b>SVNURL</b> representation for this url.
*
* @param url an input url string (like <span class="javastring">'http://myhost/my%20path'</span>)
* @return a new <b>SVNURL</b> representation of <code>url</code>
* @throws SVNException if <code>url</code> is malformed
*/
public static SVNURL parseURIEncoded(String url) throws SVNException {
return new SVNURL(url, true);
}
/**
* Creates a <span class="javastring">"file:///"</span> <b>SVNURL</b>
* representation given a filesystem style repository path.
*
* @param repositoryPath a repository path as a filesystem path
* @return an <b>SVNURL</b> representation
* @throws SVNException
*/
public static SVNURL fromFile(File repositoryPath) throws SVNException {
if (repositoryPath == null) {
return null;
}
String path = repositoryPath.getAbsoluteFile().getAbsolutePath();
String host = null;
if (SVNFileUtil.isWindows && path.startsWith("//") || path.startsWith("\\\\")) {
// this is UNC path, remove host.
path = path.replace(File.separatorChar, '/');
path = path.substring("//".length());
final int lastIndex = path.indexOf("/") > 0 ? path.indexOf("/") : path.length();
host = path.substring(0, lastIndex);
path = path.substring(host.length());
} else {
path = path.replace(File.separatorChar, '/');
}
if (!path.startsWith("/")) {
path = "/" + path;
}
return SVNURL.create("file", null, host, -1, path, false);
}
/**
* Returns the default port number for the specified protocol.
*
* @param protocol a particular access protocol
* @return default port number
*/
public static int getDefaultPortNumber(String protocol) {
if (protocol == null) {
return -1;
}
protocol = protocol.toLowerCase();
if ("file".equals(protocol)) {
return -1;
}
Integer dPort;
synchronized (DEFAULT_PORTS) {
dPort = (Integer) DEFAULT_PORTS.get(protocol);
}
if (dPort != null) {
return dPort.intValue();
}
return -1;
}
private static final Map DEFAULT_PORTS = new SVNHashMap();
static {
DEFAULT_PORTS.put("svn", new Integer(3690));
DEFAULT_PORTS.put("svn+ssh", new Integer(22));
DEFAULT_PORTS.put("http", new Integer(80));
DEFAULT_PORTS.put("https", new Integer(443));
DEFAULT_PORTS.put("file", new Integer(0));
}
/**
* Sets the default protocol for a repository access protocol.
*
* @param protocolName protocol name
* @param defaultPort default port value
* @since 1.2.0
*/
public static void registerProtocol(String protocolName, int defaultPort) {
if (protocolName != null) {
synchronized (DEFAULT_PORTS) {
if (defaultPort >= 0) {
DEFAULT_PORTS.put(protocolName, new Integer(defaultPort));
} else {
DEFAULT_PORTS.remove(protocolName);
}
}
}
}
private String myURL;
private String myProtocol;
private String myHost;
private String myPath;
private String myUserName;
private int myPort;
private String myEncodedPath;
private boolean myIsDefaultPort;
private SVNURL(String url, boolean uriEncoded) throws SVNException {
if (url == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "URL cannot be NULL");
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
int index = url.indexOf("://");
if (index <= 0) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Malformed URL ''{0}''", url);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
myProtocol = url.substring(0, index);
myProtocol = myProtocol.toLowerCase();
synchronized (DEFAULT_PORTS) {
if (!DEFAULT_PORTS.containsKey(myProtocol) && !myProtocol.startsWith("svn+")) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "URL protocol is not supported ''{0}''", url);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
}
if ("file".equals(myProtocol)) {
String normalizedPath = norlmalizeURLPath(url, url.substring("file://".length()));
int slashInd = normalizedPath.indexOf('/');
if (slashInd == -1 && normalizedPath.length() > 0) {
//no path, only host - follow subversion behaviour
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.RA_ILLEGAL_URL, "Local URL ''{0}'' contains only a hostname, no path", url);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
myPath = slashInd == -1 ? "" : normalizedPath.substring(slashInd);
if (normalizedPath.equals(myPath)) {
myHost = "";
} else {
myHost = normalizedPath.substring(0, slashInd);
}
URL testURL = null;
try {
testURL = new URL(myProtocol + "://" + normalizedPath);
} catch (MalformedURLException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Malformed URL: ''{0}'': {1}", new Object[] {url, e.getLocalizedMessage()});
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
return;
}
if (uriEncoded) {
//do it before decoding - if a caller said url is encoded
//but however typed \ instead of / - this replace will recover
//his url
myPath = myPath.replace('\\', '/');
// autoencode it.
myEncodedPath = SVNEncodingUtil.autoURIEncode(myPath);
SVNEncodingUtil.assertURISafe(myEncodedPath);
myPath = SVNEncodingUtil.uriDecode(myEncodedPath);
if(!myPath.startsWith("/")){
myPath = "/" + myPath;
}
} else {
myEncodedPath = SVNEncodingUtil.uriEncode(myPath);
myPath = myPath.replace('\\', '/');
if(!myPath.startsWith("/")){
myPath = "/" + myPath;
}
}
myUserName = testURL.getUserInfo();
myPort = testURL.getPort();
} else {
String testURL = "http" + url.substring(index);
URL httpURL;
try {
httpURL = new URL(testURL);
} catch (MalformedURLException e) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Malformed URL: ''{0}'': {1}", new Object[] {url, e.getLocalizedMessage()});
SVNErrorManager.error(err, e, SVNLogType.DEFAULT);
return;
}
myHost = httpURL.getHost();
if ("".equals(myHost)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Malformed URL: ''{0}''", url);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
return;
}
String httpPath = norlmalizeURLPath(url, getPath(httpURL));
if (uriEncoded) {
// autoencode it.
myEncodedPath = SVNEncodingUtil.autoURIEncode(httpPath);
SVNEncodingUtil.assertURISafe(myEncodedPath);
myPath = SVNEncodingUtil.uriDecode(myEncodedPath);
} else {
// do not use httpPath.
String originalPath = url.substring(index + "://".length());
if (originalPath.indexOf("/") < 0) {
originalPath = "";
} else {
originalPath = originalPath.substring(originalPath.indexOf("/") + 1);
}
myPath = originalPath;
if(!myPath.startsWith("/")){
myPath = "/" + myPath;
}
myEncodedPath = SVNEncodingUtil.uriEncode(myPath);
}
myUserName = httpURL.getUserInfo();
myPort = httpURL.getPort();
myIsDefaultPort = myPort < 0;
if (myPort < 0) {
Integer defaultPort;
synchronized (DEFAULT_PORTS) {
defaultPort = (Integer) DEFAULT_PORTS.get(myProtocol);
}
myPort = defaultPort != null ? defaultPort.intValue() : 0;
}
}
if (myEncodedPath.equals("/")) {
myEncodedPath = "";
myPath = "";
}
if (myHost != null) {
myHost = myHost.toLowerCase();
}
}
/**
* Returns the protocol component of the url represented by this
* object.
*
* @return a protocol name (like <code>http</code>)
*/
public String getProtocol() {
return myProtocol;
}
/**
* Returns the host component of the url represented by this object.
*
* @return a host name
*/
public String getHost() {
return myHost;
}
/**
* Returns the port number specified (or default) for the host.
*
* @return a port number
*/
public int getPort() {
return myPort;
}
/**
* Says if the url is provided with a non-default port number or not.
*
* @return <span class="javakeyword">true</span> if the url
* comes with a non-default port number,
* <span class="javakeyword">false</span> otherwise
* @see #getPort()
*/
public boolean hasPort() {
return !myIsDefaultPort;
}
/**
* Returns the path component of the url represented by this object
* as a uri-decoded string
*
* @return a uri-decoded path
*/
public String getPath() {
return myPath;
}
/**
* Returns the path component of the url represented by this object
* as a uri-encoded string
*
* @return a uri-encoded path
*/
public String getURIEncodedPath() {
return myEncodedPath;
}
/**
* Returns the user info component of the url represented by this
* object.
*
* @return a user info part of the url (if it was provided)
*/
public String getUserInfo() {
return myUserName;
}
/**
* Returns a string representing a UTF-8 encoded url.
*
* @return an encoded url string
*/
public String toString() {
if (myURL == null) {
myURL = composeURL(getProtocol(), getUserInfo(), getHost(), myIsDefaultPort ? -1 : getPort(), getURIEncodedPath());
}
return myURL;
}
/**
* Returns a string representing a decoded url.
*
* @return a decoded url string
*/
public String toDecodedString() {
return composeURL(getProtocol(), getUserInfo(), getHost(), myIsDefaultPort ? -1 : getPort(), getPath());
}
/**
* Constructs a new <b>SVNURL</b> representation appending a new path
* segment to the path component of this representation.
*
* @param segment a new path segment
* @param uriEncoded <span class="javakeyword">true</span> if
* <code>segment</code> is UTF-8 encoded,
* <span class="javakeyword">false</span>
* otherwise
* @return a new <b>SVNURL</b> representation
* @throws SVNException if a parse error occurred
*/
public SVNURL appendPath(String segment, boolean uriEncoded) throws SVNException {
if (segment == null || "".equals(segment)) {
return this;
}
if (!uriEncoded) {
segment = SVNEncodingUtil.uriEncode(segment);
} else {
segment = SVNEncodingUtil.autoURIEncode(segment);
}
String path = getURIEncodedPath();
if ("".equals(path)) {
path = "/" + segment;
} else {
path = SVNPathUtil.append(path, segment);
}
String url = composeURL(getProtocol(), getUserInfo(), getHost(), myIsDefaultPort ? -1 : getPort(), path);
return parseURIEncoded(url);
}
/**
* Creates a new <b>SVNURL</b> object replacing a path component of
* this object with a new provided one.
*
* @param path a path component
* @param uriEncoded <span class="javakeyword">true</span> if <code>path</code>
* is UTF-8 encoded
* @return a new <b>SVNURL</b> representation
* @throws SVNException if a parse error occurred
*/
public SVNURL setPath(String path, boolean uriEncoded) throws SVNException {
if (path == null || "".equals(path)) {
path = "/";
}
if (!uriEncoded) {
path = SVNEncodingUtil.uriEncode(path);
} else {
path = SVNEncodingUtil.autoURIEncode(path);
}
String url = composeURL(getProtocol(), getUserInfo(), getHost(), myIsDefaultPort ? -1 : getPort(), path);
return parseURIEncoded(url);
}
/**
* Constructs a new <b>SVNURL</b> representation removing a tail path
* segment from the path component of this representation.
*
* @return a new <b>SVNURL</b> representation
* @throws SVNException
*/
public SVNURL removePathTail() throws SVNException {
final String newPath = SVNPathUtil.removeTail(myEncodedPath);
final String url = composeURL(getProtocol(), getUserInfo(), getHost(), myIsDefaultPort ? -1 : getPort(), newPath);
return parseURIEncoded(url);
}
/**
* Compares this object with another one.
*
* @param obj an object to compare with
* @return <span class="javakeyword">true</span> if <code>obj</code>
* is an instance of <b>SVNURL</b> and has got the same
* url components as this object has
*/
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != SVNURL.class) {
return false;
}
SVNURL url = (SVNURL) obj;
boolean eq = myProtocol.equals(url.myProtocol) &&
myPort == url.myPort &&
myHost.equals(url.myHost) &&
myPath.equals(url.myPath) &&
hasPort() == url.hasPort();
if (myUserName == null) {
eq &= url.myUserName == null;
} else {
eq &= myUserName.equals(url.myUserName);
}
return eq;
}
/**
* Calculates and returns a hash code for this object.
*
* @return a hash code value
*/
public int hashCode() {
int code = myProtocol.hashCode() + myHost.hashCode()*27 + myPath.hashCode()*31 + myPort*17;
if (myUserName != null) {
code += 37*myUserName.hashCode();
}
return code;
}
private static String composeURL(String protocol, String userInfo, String host, int port, String path) {
StringBuffer url = new StringBuffer();
url.append(protocol);
url.append("://");
if (userInfo != null) {
url.append(userInfo);
url.append("@");
}
if (host != null) {
url.append(host);
}
if (port >= 0) {
url.append(":");
url.append(port);
}
if (path != null && !path.startsWith("/")) {
path = '/' + path;
}
if ("/".equals(path) && !"file".equals(protocol)) {
path = "";
}
url.append(path);
return url.toString();
}
private static String norlmalizeURLPath(String url, String path) throws SVNException {
StringBuffer result = new StringBuffer(path.length());
for(StringTokenizer tokens = new StringTokenizer(path, "/"); tokens.hasMoreTokens();) {
String token = tokens.nextToken();
if ("".equals(token) || ".".equals(token)) {
continue;
} else if ("..".equals(token)) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "URL ''{0}'' contains '..' element", url);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
} else {
result.append("/");
result.append(token);
}
}
if (!path.startsWith("/") && result.length() > 0) {
result = result.delete(0, 1);
}
return result.toString();
}
private static String getPath(URL url) {
String path = url.getPath();
String ref = url.getRef();
if (ref != null) {
if (path == null) {
path = "";
}
path += '#' + ref;
}
return path;
}
}