001: /*
002: * ====================================================================
003: * Copyright (c) 2004-2008 TMate Software Ltd. All rights reserved.
004: *
005: * This software is licensed as described in the file COPYING, which
006: * you should have received as part of this distribution. The terms
007: * are also available at http://svnkit.com/license.html
008: * If newer versions of this license are posted there, you may use a
009: * newer version instead, at your option.
010: * ====================================================================
011: */
012: package org.tmatesoft.svn.core;
013:
014: import java.io.File;
015: import java.net.MalformedURLException;
016: import java.net.URL;
017: import java.util.HashMap;
018: import java.util.Map;
019: import java.util.StringTokenizer;
020:
021: import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil;
022: import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
023: import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
024:
025: /**
026: * The <b>SVNURL</b> class is used for representing urls. Those SVNKit
027: * API methods, that need repository locations to carry out an operation,
028: * receive a repository location url represented by <b>SVNURL</b>. This
029: * class does all the basic work for a caller: parses an original url
030: * string (splitting it to components), encodes/decodes a path component
031: * to/from the UTF-8 charset, checks for validity (such as protocol support
032: * - if SVNKit does not support a particular protocol, <b>SVNURL</b>
033: * throws a corresponding exception).
034: *
035: * <p>
036: * To create a new <b>SVNURL</b> representation, pass an original url
037: * string (like <span class="javastring">"http://userInfo@host:port/path"</span>)
038: * to a corresponding <i>parse</i> method of this class.
039: *
040: * @version 1.1.1
041: * @author TMate Software Ltd.
042: * @see <a target="_top" href="http://svnkit.com/kb/examples/">Examples</a>
043: */
044: public class SVNURL {
045: /**
046: * Creates a new <b>SVNURL</b> representation from the given url
047: * components.
048: *
049: * @param protocol a protocol component
050: * @param userInfo a user info component
051: * @param host a host component
052: * @param port a port number
053: * @param path a path component
054: * @param uriEncoded <span class="javakeyword">true</span> if
055: * <code>path</code> is UTF-8 encoded,
056: * <span class="javakeyword">false</span>
057: * otherwise
058: * @return a new <b>SVNURL</b> representation
059: * @throws SVNException if the resultant url (composed of the given
060: * components) is malformed
061: */
062: public static SVNURL create(String protocol, String userInfo,
063: String host, int port, String path, boolean uriEncoded)
064: throws SVNException {
065: if (host == null || host.indexOf('@') >= 0) {
066: SVNErrorManager.error(SVNErrorMessage.create(
067: SVNErrorCode.BAD_URL, "Invalid host name ''{0}''",
068: host));
069: }
070: path = path == null ? "/" : path.trim();
071: if (!uriEncoded) {
072: path = SVNEncodingUtil.uriEncode(path);
073: } else {
074: path = SVNEncodingUtil.autoURIEncode(path);
075: }
076: if (path.length() > 0 && path.charAt(0) != '/') {
077: path = "/" + path;
078: }
079: if (path.length() > 0 && path.charAt(path.length() - 1) == '/') {
080: path = path.substring(0, path.length() - 1);
081: }
082: protocol = protocol == null ? "http" : protocol.toLowerCase();
083: String errorMessage = null;
084: if (userInfo != null && userInfo.indexOf('/') >= 0) {
085: errorMessage = "Malformed URL: user info part could not include '/' symbol";
086: } else if (host == null && !"file".equals(protocol)) {
087: errorMessage = "Malformed URL: host could not be NULL";
088: } else if (!"file".equals(protocol) && host.indexOf('/') >= 0) {
089: errorMessage = "Malformed URL: host could not include '/' symbol";
090: }
091: if (errorMessage != null) {
092: SVNErrorMessage err = SVNErrorMessage.create(
093: SVNErrorCode.BAD_URL, errorMessage);
094: SVNErrorManager.error(err);
095: }
096: String url = composeURL(protocol, userInfo, host, port, path);
097: return new SVNURL(url, true);
098: }
099:
100: /**
101: * Parses the given decoded (not UTF-8 encoded) url string and creates
102: * a new <b>SVNURL</b> representation for this url.
103: *
104: * @param url an input url string (like <span class="javastring">'http://myhost/mypath'</span>)
105: * @return a new <b>SVNURL</b> representation of <code>url</code>
106: * @throws SVNException if <code>url</code> is malformed
107: */
108: public static SVNURL parseURIDecoded(String url)
109: throws SVNException {
110: return new SVNURL(url, false);
111: }
112:
113: /**
114: * Parses the given UTF-8 encoded url string and creates a new
115: * <b>SVNURL</b> representation for this url.
116: *
117: * @param url an input url string (like <span class="javastring">'http://myhost/my%20path'</span>)
118: * @return a new <b>SVNURL</b> representation of <code>url</code>
119: * @throws SVNException if <code>url</code> is malformed
120: */
121: public static SVNURL parseURIEncoded(String url)
122: throws SVNException {
123: return new SVNURL(url, true);
124: }
125:
126: /**
127: * Creates a <span class="javastring">"file:///"</span> <b>SVNURL</b>
128: * representation given a filesystem style repository path.
129: *
130: * @param repositoryPath a repository path as a filesystem path
131: * @return an <b>SVNURL</b> representation
132: * @throws SVNException
133: */
134: public static SVNURL fromFile(File repositoryPath)
135: throws SVNException {
136: if (repositoryPath == null) {
137: return null;
138: }
139: String path = repositoryPath.getAbsoluteFile()
140: .getAbsolutePath();
141: path = path.replace(File.separatorChar, '/');
142: if (!path.startsWith("/")) {
143: path = "/" + path;
144: }
145: return SVNURL.parseURIDecoded("file://" + path);
146: }
147:
148: /**
149: * Returns the default port number for the specified protocol.
150: *
151: * @param protocol a particular access protocol
152: * @return default port number
153: */
154: public static int getDefaultPortNumber(String protocol) {
155: if (protocol == null) {
156: return -1;
157: }
158: protocol = protocol.toLowerCase();
159: if ("file".equals(protocol)) {
160: return -1;
161: }
162: Integer dPort;
163: synchronized (DEFAULT_PORTS) {
164: dPort = (Integer) DEFAULT_PORTS.get(protocol);
165: }
166: if (dPort != null) {
167: return dPort.intValue();
168: }
169: return -1;
170: }
171:
172: private static final Map DEFAULT_PORTS = new HashMap();
173:
174: static {
175: DEFAULT_PORTS.put("svn", new Integer(3690));
176: DEFAULT_PORTS.put("svn+ssh", new Integer(22));
177: DEFAULT_PORTS.put("http", new Integer(80));
178: DEFAULT_PORTS.put("https", new Integer(443));
179: DEFAULT_PORTS.put("file", new Integer(0));
180: }
181:
182: public static void registerProtocol(String protocolName,
183: int defaultPort) {
184: if (protocolName != null) {
185: synchronized (DEFAULT_PORTS) {
186: if (defaultPort >= 0) {
187: DEFAULT_PORTS.put(protocolName, new Integer(
188: defaultPort));
189: } else {
190: DEFAULT_PORTS.remove(protocolName);
191: }
192: }
193: }
194: }
195:
196: private String myURL;
197: private String myProtocol;
198: private String myHost;
199: private String myPath;
200: private String myUserName;
201: private int myPort;
202: private String myEncodedPath;
203: private boolean myIsDefaultPort;
204:
205: private SVNURL(String url, boolean uriEncoded) throws SVNException {
206: if (url == null) {
207: SVNErrorMessage err = SVNErrorMessage.create(
208: SVNErrorCode.BAD_URL, "URL cannot be NULL");
209: SVNErrorManager.error(err);
210: }
211: if (url.endsWith("/")) {
212: url = url.substring(0, url.length() - 1);
213: }
214: int index = url.indexOf("://");
215: if (index <= 0) {
216: SVNErrorMessage err = SVNErrorMessage.create(
217: SVNErrorCode.BAD_URL, "Malformed URL ''{0}''", url);
218: SVNErrorManager.error(err);
219: }
220: myProtocol = url.substring(0, index);
221: myProtocol = myProtocol.toLowerCase();
222: synchronized (DEFAULT_PORTS) {
223: if (!DEFAULT_PORTS.containsKey(myProtocol)
224: && !myProtocol.startsWith("svn+")) {
225: SVNErrorMessage err = SVNErrorMessage.create(
226: SVNErrorCode.BAD_URL,
227: "URL protocol is not supported ''{0}''", url);
228: SVNErrorManager.error(err);
229: }
230: }
231: if ("file".equals(myProtocol)) {
232: String normalizedPath = norlmalizeURLPath(url, url
233: .substring("file://".length()));
234: int slashInd = normalizedPath.indexOf('/');
235: if (slashInd == -1) {
236: //no path, only host - follow subversion behaviour
237: SVNErrorMessage err = SVNErrorMessage
238: .create(
239: SVNErrorCode.RA_ILLEGAL_URL,
240: "Local URL ''{0}'' contains only a hostname, no path",
241: url);
242: SVNErrorManager.error(err);
243: }
244:
245: myPath = normalizedPath.substring(slashInd);
246: if (normalizedPath.equals(myPath)) {
247: myHost = "";
248: } else {
249: myHost = normalizedPath.substring(0, slashInd);
250: }
251:
252: URL testURL = null;
253: try {
254: testURL = new URL(myProtocol + "://" + normalizedPath);
255: } catch (MalformedURLException e) {
256: SVNErrorMessage err = SVNErrorMessage.create(
257: SVNErrorCode.BAD_URL,
258: "Malformed URL: ''{0}'': {1}", new Object[] {
259: url, e.getLocalizedMessage() });
260: SVNErrorManager.error(err, e);
261: return;
262: }
263:
264: if (uriEncoded) {
265: //do it before decoding - if a caller said url is encoded
266: //but however typed \ instead of / - this replace will recover
267: //his url
268: myPath = myPath.replace('\\', '/');
269: // autoencode it.
270: myEncodedPath = SVNEncodingUtil.autoURIEncode(myPath);
271: SVNEncodingUtil.assertURISafe(myEncodedPath);
272: myPath = SVNEncodingUtil.uriDecode(myEncodedPath);
273: if (!myPath.startsWith("/")) {
274: myPath = "/" + myPath;
275: }
276: } else {
277: myEncodedPath = SVNEncodingUtil.uriEncode(myPath);
278: myPath = myPath.replace('\\', '/');
279: if (!myPath.startsWith("/")) {
280: myPath = "/" + myPath;
281: }
282: }
283: myUserName = testURL.getUserInfo();
284: myPort = testURL.getPort();
285: } else {
286: String testURL = "http" + url.substring(index);
287: URL httpURL;
288: try {
289: httpURL = new URL(testURL);
290: } catch (MalformedURLException e) {
291: SVNErrorMessage err = SVNErrorMessage.create(
292: SVNErrorCode.BAD_URL,
293: "Malformed URL: ''{0}'': {1}", new Object[] {
294: url, e.getLocalizedMessage() });
295: SVNErrorManager.error(err, e);
296: return;
297: }
298: myHost = httpURL.getHost();
299: String httpPath = norlmalizeURLPath(url, getPath(httpURL));
300: if (uriEncoded) {
301: // autoencode it.
302: myEncodedPath = SVNEncodingUtil.autoURIEncode(httpPath);
303: SVNEncodingUtil.assertURISafe(myEncodedPath);
304: myPath = SVNEncodingUtil.uriDecode(myEncodedPath);
305: } else {
306: // do not use httpPath.
307: String originalPath = url.substring(index
308: + "://".length());
309: if (originalPath.indexOf("/") < 0) {
310: originalPath = "";
311: } else {
312: originalPath = originalPath.substring(originalPath
313: .indexOf("/") + 1);
314: }
315: myPath = originalPath;
316: if (!myPath.startsWith("/")) {
317: myPath = "/" + myPath;
318: }
319: myEncodedPath = SVNEncodingUtil.uriEncode(myPath);
320: }
321: myUserName = httpURL.getUserInfo();
322: myPort = httpURL.getPort();
323: myIsDefaultPort = myPort < 0;
324: if (myPort < 0) {
325: Integer defaultPort;
326: synchronized (DEFAULT_PORTS) {
327: defaultPort = (Integer) DEFAULT_PORTS
328: .get(myProtocol);
329: }
330: myPort = defaultPort != null ? defaultPort.intValue()
331: : 0;
332: }
333: }
334: if (myEncodedPath.equals("/")) {
335: myEncodedPath = "";
336: myPath = "";
337: }
338: }
339:
340: /**
341: * Returns the protocol component of the url represented by this
342: * object.
343: *
344: * @return a protocol name (like <code>http</code>)
345: */
346: public String getProtocol() {
347: return myProtocol;
348: }
349:
350: /**
351: * Returns the host component of the url represented by this object.
352: *
353: * @return a host name
354: */
355: public String getHost() {
356: return myHost;
357: }
358:
359: /**
360: * Returns the port number specified (or default) for the host.
361: *
362: * @return a port number
363: */
364: public int getPort() {
365: return myPort;
366: }
367:
368: /**
369: * Says if the url is provided with a non-default port number or not.
370: *
371: * @return <span class="javakeyword">true</span> if the url
372: * comes with a non-default port number,
373: * <span class="javakeyword">false</span> otherwise
374: * @see #getPort()
375: */
376: public boolean hasPort() {
377: return !myIsDefaultPort;
378: }
379:
380: /**
381: * Returns the path component of the url represented by this object
382: * as a uri-decoded string
383: *
384: * @return a uri-decoded path
385: */
386: public String getPath() {
387: return myPath;
388: }
389:
390: /**
391: * Returns the path component of the url represented by this object
392: * as a uri-encoded string
393: *
394: * @return a uri-encoded path
395: */
396: public String getURIEncodedPath() {
397: return myEncodedPath;
398: }
399:
400: /**
401: * Returns the user info component of the url represented by this
402: * object.
403: *
404: * @return a user info part of the url (if it was provided)
405: */
406: public String getUserInfo() {
407: return myUserName;
408: }
409:
410: /**
411: * Returns a string representing a UTF-8 encoded url.
412: *
413: * @return an encoded url string
414: */
415: public String toString() {
416: if (myURL == null) {
417: myURL = composeURL(getProtocol(), getUserInfo(), getHost(),
418: myIsDefaultPort ? -1 : getPort(),
419: getURIEncodedPath());
420: }
421: return myURL;
422: }
423:
424: /**
425: * Returns a string representing a decoded url.
426: *
427: * @return a decoded url string
428: */
429: public String toDecodedString() {
430: return composeURL(getProtocol(), getUserInfo(), getHost(),
431: myIsDefaultPort ? -1 : getPort(), getPath());
432: }
433:
434: /**
435: * Constructs a new <b>SVNURL</b> representation appending a new path
436: * segment to the path component of this representation.
437: *
438: * @param segment a new path segment
439: * @param uriEncoded <span class="javakeyword">true</span> if
440: * <code>segment</code> is UTF-8 encoded,
441: * <span class="javakeyword">false</span>
442: * otherwise
443: * @return a new <b>SVNURL</b> representation
444: * @throws SVNException if a parse error occurred
445: */
446: public SVNURL appendPath(String segment, boolean uriEncoded)
447: throws SVNException {
448: if (segment == null || "".equals(segment)) {
449: return this ;
450: }
451: if (!uriEncoded) {
452: segment = SVNEncodingUtil.uriEncode(segment);
453: } else {
454: segment = SVNEncodingUtil.autoURIEncode(segment);
455: }
456: String path = getURIEncodedPath();
457: if ("".equals(path)) {
458: path = "/" + segment;
459: } else {
460: path = SVNPathUtil.append(path, segment);
461: }
462: String url = composeURL(getProtocol(), getUserInfo(),
463: getHost(), myIsDefaultPort ? -1 : getPort(), path);
464: return parseURIEncoded(url);
465: }
466:
467: /**
468: * Creates a new <b>SVNURL</b> object replacing a path component of
469: * this object with a new provided one.
470: *
471: * @param path a path component
472: * @param uriEncoded <span class="javakeyword">true</span> if <code>path</code>
473: * is UTF-8 encoded
474: * @return a new <b>SVNURL</b> representation
475: * @throws SVNException if a parse error occurred
476: */
477: public SVNURL setPath(String path, boolean uriEncoded)
478: throws SVNException {
479: if (path == null || "".equals(path)) {
480: path = "/";
481: }
482: if (!uriEncoded) {
483: path = SVNEncodingUtil.uriEncode(path);
484: } else {
485: path = SVNEncodingUtil.autoURIEncode(path);
486: }
487: String url = composeURL(getProtocol(), getUserInfo(),
488: getHost(), myIsDefaultPort ? -1 : getPort(), path);
489: return parseURIEncoded(url);
490: }
491:
492: /**
493: * Constructs a new <b>SVNURL</b> representation removing a tail path
494: * segment from the path component of this representation.
495: *
496: * @return a new <b>SVNURL</b> representation
497: * @throws SVNException
498: */
499: public SVNURL removePathTail() throws SVNException {
500: String newPath = SVNPathUtil.removeTail(myPath);
501: String url = composeURL(getProtocol(), getUserInfo(),
502: getHost(), myIsDefaultPort ? -1 : getPort(), newPath);
503: return parseURIDecoded(url);
504: }
505:
506: /**
507: * Compares this object with another one.
508: *
509: * @param obj an object to compare with
510: * @return <span class="javakeyword">true</span> if <code>obj</code>
511: * is an instance of <b>SVNURL</b> and has got the same
512: * url components as this object has
513: */
514: public boolean equals(Object obj) {
515: if (obj == null || obj.getClass() != SVNURL.class) {
516: return false;
517: }
518: SVNURL url = (SVNURL) obj;
519: boolean eq = myProtocol.equals(url.myProtocol)
520: && myPort == url.myPort && myHost.equals(url.myHost)
521: && myPath.equals(url.myPath)
522: && hasPort() == url.hasPort();
523: if (myUserName == null) {
524: eq &= url.myUserName == null;
525: } else {
526: eq &= myUserName.equals(url.myUserName);
527: }
528: return eq;
529: }
530:
531: /**
532: * Calculates and returns a hash code for this object.
533: *
534: * @return a hash code value
535: */
536: public int hashCode() {
537: int code = myProtocol.hashCode() + myHost.hashCode() * 27
538: + myPath.hashCode() * 31 + myPort * 17;
539: if (myUserName != null) {
540: code += 37 * myUserName.hashCode();
541: }
542: return code;
543: }
544:
545: private static String composeURL(String protocol, String userInfo,
546: String host, int port, String path) {
547: StringBuffer url = new StringBuffer();
548: url.append(protocol);
549: url.append("://");
550: if (userInfo != null) {
551: url.append(userInfo);
552: url.append("@");
553: }
554: url.append(host);
555: if (port >= 0) {
556: url.append(":");
557: url.append(port);
558: }
559: if (path != null && !path.startsWith("/")) {
560: path = '/' + path;
561: }
562: if ("/".equals(path)) {
563: path = "";
564: }
565: url.append(path);
566: return url.toString();
567: }
568:
569: private static String norlmalizeURLPath(String url, String path)
570: throws SVNException {
571: StringBuffer result = new StringBuffer(path.length());
572: for (StringTokenizer tokens = new StringTokenizer(path, "/"); tokens
573: .hasMoreTokens();) {
574: String token = tokens.nextToken();
575: if ("".equals(token) || ".".equals(token)) {
576: continue;
577: } else if ("..".equals(token)) {
578: SVNErrorMessage err = SVNErrorMessage.create(
579: SVNErrorCode.BAD_URL,
580: "URL ''{0}'' contains '..' element", url);
581: SVNErrorManager.error(err);
582: } else {
583: result.append("/");
584: result.append(token);
585: }
586: }
587: if (!path.startsWith("/") && result.length() > 0) {
588: result = result.delete(0, 1);
589: }
590: return result.toString();
591: }
592:
593: private static String getPath(URL url) {
594: String path = url.getPath();
595: String ref = url.getRef();
596: if (ref != null) {
597: if (path == null) {
598: path = "";
599: }
600: path += '#' + ref;
601: }
602: return path;
603: }
604:
605: }
|