/*
* ====================================================================
* 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.internal.wc;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.util.SVNDebugLog;
import org.tmatesoft.svn.util.SVNLogType;
/**
* @version 1.3
* @author TMate Software Ltd.
*/
public class SVNExternal {
private SVNRevision myRevision;
private SVNRevision myPegRevision;
private String myURL;
private String myPath;
private SVNURL myResolvedURL;
private boolean myIsRevisionExplicit;
private boolean myIsPegRevisionExplicit;
private boolean myIsNewFormat;
private String myRawValue;
private SVNExternal() {
myRevision = SVNRevision.UNDEFINED;
myPegRevision = SVNRevision.UNDEFINED;
}
public SVNExternal(String target, String url, SVNRevision pegRevision, SVNRevision revision,
boolean isRevisionExplicit, boolean isPegRevisionExplicit, boolean isNewFormat) {
myPath = target;
myURL = url;
myRevision = revision;
myPegRevision = pegRevision;
myIsRevisionExplicit = isRevisionExplicit;
myIsPegRevisionExplicit = isPegRevisionExplicit;
myIsNewFormat = isNewFormat;
}
public SVNRevision getRevision() {
return myRevision;
}
public SVNRevision getPegRevision() {
return myPegRevision;
}
public String getPath() {
return myPath;
}
public String getUnresolvedUrl() {
return myURL;
}
public String getRawValue() {
return myRawValue;
}
public boolean isRevisionExplicit() {
return myIsRevisionExplicit;
}
public boolean isPegRevisionExplicit() {
return myIsPegRevisionExplicit;
}
public boolean isNewFormat() {
return myIsNewFormat;
}
public SVNURL getResolvedURL() {
return myResolvedURL;
}
public SVNURL resolveURL(SVNURL rootURL, SVNURL ownerURL) throws SVNException {
String canonicalURL;
if (myURL != null && myURL.startsWith("^/")) {
canonicalURL = "^/" + SVNPathUtil.canonicalizePath(myURL.substring("^/".length()));
} else {
canonicalURL = SVNPathUtil.canonicalizePath(myURL);
}
if (SVNPathUtil.isURL(canonicalURL)) {
myResolvedURL = SVNURL.parseURIEncoded(canonicalURL);
return getResolvedURL();
}
if (myURL.startsWith("../") || myURL.startsWith("^/")) {
// ../ relative to the parent directory of the external
// ^/ relative to the repository root
String[] base = myURL.startsWith("../") ? ownerURL.getPath().split("/") : rootURL.getPath().split("/");
LinkedList baseList = new LinkedList(Arrays.asList(base));
if (canonicalURL.startsWith("^/")) {
canonicalURL = canonicalURL.substring("^/".length());
}
String[] relative = canonicalURL.split("/");
for (int i = 0; i < relative.length; i++) {
if ("..".equals(relative[i])) {
// remove last from base.
if (!baseList.isEmpty()) {
baseList.removeLast();
}
} else {
baseList.add(relative[i]);
}
}
String finalPath = "/";
for (Iterator segments = baseList.iterator(); segments.hasNext();) {
String segment = (String) segments.next();
finalPath = SVNPathUtil.append(finalPath, segment);
}
myResolvedURL = ownerURL.setPath(finalPath, true);
return getResolvedURL();
}
if (myURL.indexOf("/../") >= 0 || myURL.startsWith("../") || myURL.endsWith("/..")) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "The external relative URL ''{0}'' cannot have backpaths, i.e. ''..''.", myURL);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
if (myURL.startsWith("//")) {
// // relative to the scheme
myResolvedURL = SVNURL.parseURIEncoded(SVNPathUtil.canonicalizePath(rootURL.getProtocol() + ":" + myURL));
return getResolvedURL();
} else if (myURL.startsWith("/")) {
// / relative to the server's host
myResolvedURL = ownerURL.setPath(myURL, true);
return getResolvedURL();
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "Unrecognized format for the relative external URL ''{0}''.", myURL);
SVNErrorManager.error(err, SVNLogType.DEFAULT);
return null;
}
public String toString() {
String value = "";
String path = quotePath(myPath);
String url = quotePath(myURL);
if (myIsPegRevisionExplicit && SVNRevision.isValidRevisionNumber(myPegRevision.getNumber())) {
if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
value += "-r" + myRevision + " ";
}
value += url + "@" + myPegRevision + " " + path;
} else {
if (myIsNewFormat) {
if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
value += "-r" + myRevision + " ";
}
value += url + " " + path;
} else {
value += path;
if (myIsRevisionExplicit && SVNRevision.isValidRevisionNumber(myRevision.getNumber())) {
value += " -r" + myRevision;
}
value += " " + url;
}
}
return value;
}
public static SVNExternal[] parseExternals(Object owner, String description) throws SVNException {
List lines = new ArrayList();
for(StringTokenizer tokenizer = new StringTokenizer(description, "\r\n"); tokenizer.hasMoreTokens();) {
lines.add(tokenizer.nextToken());
}
Collection externals = new ArrayList();
for (int i = 0; i < lines.size(); i++) {
String line = ((String) lines.get(i)).trim();
if ("".equals(line) || line.startsWith("#")) {
continue;
}
List tokens = new ArrayList();
for(Iterator tokenizer = new ExternalTokenizer(line); tokenizer.hasNext();) {
tokens.add(tokenizer.next());
}
if (tokens.size() < 2 || tokens.size() > 4) {
reportParsingError(owner, line);
}
SVNExternal external = new SVNExternal();
int revisionToken = fetchRevision(external, owner, line, tokens);
String token0 = (String) tokens.get(0);
String token1 = (String) tokens.get(1);
boolean token0isURL = SVNPathUtil.isURL(token0);
boolean token1isURL = SVNPathUtil.isURL(token1);
if (token0isURL && token1isURL) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
"Invalid svn:external property on ''{0}'': cannot use two absolute URLs (''{1}'' and ''{2}'') in an external; " +
"one must be a path where an absolute or relative URL is checked out to", new Object[] {owner, token0, token1});
SVNErrorManager.error(err, SVNLogType.WC);
}
if (revisionToken == 0 && token1isURL) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
"Invalid svn:external property on ''{0}'': cannot use a URL ''{1}'' as the target directory for an external definition",
new Object[] {owner, token1});
SVNErrorManager.error(err, SVNLogType.WC);
}
if (revisionToken == 1 && token0isURL) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
"Invalid svn:external property on ''{0}'': cannot use a URL ''{1}'' as the target directory for an external definition",
new Object[] {owner, token0});
SVNErrorManager.error(err, SVNLogType.WC);
}
if (revisionToken == 0 || (revisionToken == -1 && (token0isURL || !token1isURL))) {
external.myPath = token1;
boolean schemeRelative = token0.startsWith("//");
if (schemeRelative) {
token0 = token0.substring(2);
}
SVNPath path = new SVNPath(token0, true);
external.myURL = schemeRelative ? "//" + path.getTarget() : path.getTarget();
external.myPegRevision = path.getPegRevision();
if (external.myPegRevision == SVNRevision.BASE) {
external.myPegRevision = SVNRevision.HEAD;
}
if (external.myPegRevision != SVNRevision.UNDEFINED) {
external.myIsPegRevisionExplicit = true;
}
external.myIsNewFormat = true;
} else {
external.myPath = token0;
external.myURL = token1;
external.myPegRevision = external.myRevision;
}
if (external.myPegRevision == SVNRevision.UNDEFINED) {
external.myPegRevision = SVNRevision.HEAD;
}
if (external.myRevision == SVNRevision.UNDEFINED) {
external.myRevision = external.myPegRevision;
}
external.myPath = SVNPathUtil.canonicalizePath(external.myPath.replace(File.separatorChar, '/'));
if (external.myPath.length() == 0 ||
external.myPath.equals(".") ||
external.myPath.equals("..") ||
external.myPath.startsWith("../") ||
external.myPath.startsWith("/") ||
external.myPath.indexOf("/../") > 0 ||
external.myPath.endsWith("/..")) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
"Invalid {0} property on ''{1}'': target ''{2}'' is an absolute path or involves ''..''", new Object[] {SVNProperty.EXTERNALS, owner, external.myPath});
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
external.myRawValue = line;
if (external.myURL != null && SVNPathUtil.isURL(external.myURL)) {
SVNURL.parseURIEncoded(external.myURL);
}
externals.add(external);
}
return (SVNExternal[]) externals.toArray(new SVNExternal[externals.size()]);
}
private static String quotePath(String path) {
for(int i = 0; i < path.length(); i++) {
if (Character.isWhitespace(path.charAt(i))) {
return "\"" + path + "\"";
}
}
return path;
}
private static int fetchRevision(SVNExternal external, Object owner, String line, List tokens) throws SVNException {
for (int i = 0; i < tokens.size() && i < 2; i++) {
String token = (String) tokens.get(i);
String revisionStr = null;
if (token.length() >= 2 &&
token.charAt(0) == '-' && token.charAt(1) == 'r') {
if (token.length() == 2 && tokens.size() == 4) {
revisionStr = (String) tokens.get(i + 1);
// remove separate '-r' token.
tokens.remove(i);
} else if (tokens.size() == 3) {
revisionStr = token.substring(2);
}
if (revisionStr == null || "".equals(revisionStr)) {
reportParsingError(owner, line);
}
long revNumber = -1;
try {
revNumber = Long.parseLong(revisionStr);
if (revNumber < 0) {
SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, "Negative revision number found parsing '" + revisionStr + "'");
reportParsingError(owner, line);
}
} catch (NumberFormatException nfe) {
SVNDebugLog.getDefaultLog().logFine(SVNLogType.DEFAULT, "Invalid revision number found parsing '" + revisionStr + "'");
reportParsingError(owner, line);
}
external.myRevision = SVNRevision.create(revNumber);
external.myIsRevisionExplicit = true;
tokens.remove(i);
return i;
}
}
if (tokens.size() == 2) {
return -1;
}
reportParsingError(owner, line);
return -1;
}
private static void reportParsingError(Object owner, String line) throws SVNException {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.CLIENT_INVALID_EXTERNALS_DESCRIPTION,
"Error parsing {0} property on ''{1}'': ''{2}''", new Object[] {SVNProperty.EXTERNALS, owner, line});
SVNErrorManager.error(err, SVNLogType.DEFAULT);
}
public static List<String> findTargetDuplications(SVNExternal[] externals) {
List<String> paths = new ArrayList<String>();
for (int i = 0; i < externals.length; i++) {
final SVNExternal external = externals[i];
String path = external.getPath();
for (int j = i + 1; j < externals.length; j++) {
SVNExternal anotherExternal = externals[j];
String anotherPath = anotherExternal.getPath();
if (path.equals(anotherPath)) {
paths.add(path);
}
}
}
return paths;
}
private static class ExternalTokenizer implements Iterator {
private String myNextToken;
private String myLine;
public ExternalTokenizer(String line) {
myLine = line;
myNextToken = advance();
}
public boolean hasNext() {
return myNextToken != null;
}
public Object next() {
String next = myNextToken;
myNextToken = advance();
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
private String advance() {
while(myLine.length() > 0 && Character.isWhitespace(myLine.charAt(0))) {
myLine = myLine.substring(1);
}
if (myLine.length() == 0) {
return null;
}
char ch = myLine.charAt(0);
int quouteType = ch == '\'' ? 1 : (ch == '\"' ? 2 : 0);
if (quouteType != 0) {
myLine = myLine.substring(1);
}
int index = 0;
StringBuffer result = new StringBuffer();
while(index < myLine.length()) {
ch = myLine.charAt(index);
if (quouteType == 0) {
if (Character.isWhitespace(ch)) {
break;
}
} else if (quouteType == 1) {
if (ch == '\'') {
break;
}
} else if (quouteType == 2) {
if (ch == '\"') {
break;
}
}
if (ch == '\\') {
// append qouted character, so far whitespace only
if (index + 1 < myLine.length()) {
char escaped = myLine.charAt(index + 1);
if (escaped == ' ' || escaped == '\'' || escaped == '\"') {
// append escaped char instead of backslash
result.append(escaped);
index++;
index++;
continue;
}
}
}
result.append(ch);
index++;
}
if (index + 1 < myLine.length()) {
myLine = myLine.substring(index + 1);
} else {
myLine = "";
}
return result.toString();
}
}
}