Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: a6dd3a1cc98c03168b16025c5fabbd5b9000d908 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
/*******************************************************************************
 * Copyright (c) 2008, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.runtime;

import java.io.File;
import java.net.*;

/**
 * A utility class for manipulating URIs. This class works around some of the
 * undesirable behavior of the {@link java.net.URI} class, and provides additional
 * path manipulation methods that are not available on the URI class.
 * 
 * @noinstantiate This class is not intended to be instantiated by clients.
 * @since org.eclipse.equinox.common 3.5
 */
public final class URIUtil {

	private static final String JAR_SUFFIX = "!/"; //$NON-NLS-1$
	private static final String UNC_PREFIX = "//"; //$NON-NLS-1$
	private static final String SCHEME_FILE = "file"; //$NON-NLS-1$
	private static final String SCHEME_JAR = "jar"; //$NON-NLS-1$

	private static final boolean decodeResolved;
	static {
		decodeResolved = URI.create("foo:/a%20b/").resolve("c").getSchemeSpecificPart().indexOf('%') > 0; //$NON-NLS-1$ //$NON-NLS-2$
	}

	private URIUtil() {
		// prevent instantiation
	}

	/**
	 * Returns a new URI with all the same components as the given base URI,
	 * but with a path component created by appending the given extension to the
	 * base URI's path.
	 * <p>
	 * The important difference between this method
	 * and {@link java.net.URI#resolve(String)} is in the treatment of the final segment.
	 * The URI resolve method drops the last segment if there is no trailing slash as
	 * specified in section 5.2 of RFC 2396. This leads to unpredictable behaviour
	 * when working with file: URIs, because the existence of the trailing slash
	 * depends on the existence of a local file on disk. This method operates
	 * like a traditional path append and always preserves all segments of the base path.
	 * 
	 * @param base The base URI to append to
	 * @param extension The unencoded path extension to be added
	 * @return The appended URI
	 */
	public static URI append(URI base, String extension) {
		try {
			String path = base.getPath();
			if (path == null)
				return appendOpaque(base, extension);
			//if the base is already a directory then resolve will just do the right thing
			URI result;
			if (path.endsWith("/")) {//$NON-NLS-1$
				result = base.resolve(new URI(null, null, extension, null));
				if (decodeResolved) {
					//see bug 267219 - Java 1.4 implementation of URI#resolve incorrectly encoded the ssp
					result = new URI(toUnencodedString(result));
				}
			} else {
				path = path + '/' + extension;
				result = new URI(base.getScheme(), base.getUserInfo(), base.getHost(), base.getPort(), path, base.getQuery(), base.getFragment());
			}
			result = result.normalize();
			//Fix UNC paths that are incorrectly normalized by URI#resolve (see Java bug 4723726)
			String resultPath = result.getPath();
			if (isFileURI(base) && path != null && path.startsWith(UNC_PREFIX) && (resultPath == null || !resultPath.startsWith(UNC_PREFIX)))
				result = new URI(result.getScheme(), ensureUNCPath(result.getSchemeSpecificPart()), result.getFragment());
			return result;
		} catch (URISyntaxException e) {
			//shouldn't happen because we started from a valid URI
			throw new RuntimeException(e);
		}
	}

	/**
	 * Special case of appending to an opaque URI. Since opaque URIs
	 * have no path segment the best we can do is append to the scheme-specific part
	 */
	private static URI appendOpaque(URI base, String extension) throws URISyntaxException {
		String ssp = base.getSchemeSpecificPart();
		if (ssp.endsWith("/")) //$NON-NLS-1$
			ssp += extension;
		else
			ssp = ssp + "/" + extension; //$NON-NLS-1$
		return new URI(base.getScheme(), ssp, base.getFragment());
	}

	/**
	 * Ensures the given path string starts with exactly four leading slashes.
	 */
	private static String ensureUNCPath(String path) {
		int len = path.length();
		StringBuffer result = new StringBuffer(len);
		for (int i = 0; i < 4; i++) {
			//	if we have hit the first non-slash character, add another leading slash
			if (i >= len || result.length() > 0 || path.charAt(i) != '/')
				result.append('/');
		}
		result.append(path);
		return result.toString();
	}

	/**
	 * Returns a URI corresponding to the given unencoded string. This method
	 * will take care of encoding any characters that must be encoded according
	 * to the URI specification. This method must not be called with a string that
	 * already contains an encoded URI, since this will result in the URI escape character ('%')
	 * being escaped itself.
	 * 
	 * @param uriString An unencoded URI string
	 * @return A URI corresponding to the given string
	 * @throws URISyntaxException If the string cannot be formed into a valid URI
	 */
	public static URI fromString(String uriString) throws URISyntaxException {
		int colon = uriString.indexOf(':');
		int hash = uriString.lastIndexOf('#');
		boolean noHash = hash < 0;
		if (noHash)
			hash = uriString.length();
		String scheme = colon < 0 ? null : uriString.substring(0, colon);
		String ssp = uriString.substring(colon + 1, hash);
		String fragment = noHash ? null : uriString.substring(hash + 1);
		//use java.io.File for constructing file: URIs
		if (scheme != null && scheme.equals(SCHEME_FILE)) {
			//handle relative URI string with scheme (produced by java.net.URL)
			File file = new File(ssp);
			if (file.isAbsolute())
				return file.toURI();
			if (File.separatorChar != '/')
				ssp = ssp.replace(File.separatorChar, '/');
			//relative URIs have a null scheme.
			if (!ssp.startsWith("/"))//$NON-NLS-1$
				scheme = null;
		}
		return new URI(scheme, ssp, fragment);
	}

	/**
	 * Returns whether the given URI refers to a local file system URI.
	 * @param uri The URI to check
	 * @return <code>true</code> if the URI is a local file system location, and <code>false</code> otherwise
	 */
	public static boolean isFileURI(URI uri) {
		return SCHEME_FILE.equalsIgnoreCase(uri.getScheme());
	}

	/**
	 * Returns the last segment of the given URI. For a hierarchical URL this returns
	 * the last segment of the path. For opaque URIs this treats the scheme-specific
	 * part as a path and returns the last segment. Returns <code>null</code> for
	 * a hierarchical URI with an empty path, and for opaque URIs whose scheme-specific
	 * part cannot be interpreted as a path.
	 */
	public static String lastSegment(URI location) {
		String path = location.getPath();
		if (path == null)
			return new Path(location.getSchemeSpecificPart()).lastSegment();
		return new Path(path).lastSegment();
	}

	/**
	 * Returns a new URI which is the same as this URI but with the file extension removed 
	 * from the path part.  If this URI does not have an extension, this path is returned.
	 * <p>
	 * The file extension portion is defined as the string
	 * following the last period (".") character in the last segment.
	 * If there is no period in the last segment, the path has no
	 * file extension portion. If the last segment ends in a period,
	 * the file extension portion is the empty string.
	 * </p>
	 *
	 * @return the new URI
	 */
	public static URI removeFileExtension(URI uri) {
		String lastSegment = lastSegment(uri);
		if (lastSegment == null)
			return uri;
		int lastIndex = lastSegment.lastIndexOf('.');
		if (lastIndex == -1)
			return uri;
		String uriString = uri.toString();
		lastIndex = uriString.lastIndexOf('.');
		uriString = uriString.substring(0, lastIndex);
		return URI.create(uriString);
	}

	/**
	 * Returns true if the two URIs are equal. URIs are considered equal if
	 * {@link URI#equals(Object)} returns true, if the string representation
	 * of the URIs is equal, or if they URIs are represent the same local file.
	 * @param uri1 The first URI to compare
	 * @param uri2 The second URI to compare
	 * @return <code>true</code> if the URIs are the same, and <code>false</code> otherwise.
	 */
	public static boolean sameURI(URI uri1, URI uri2) {
		if (uri1 == uri2)
			return true;
		if (uri1 == null || uri2 == null)
			return false;

		if (uri1.equals(uri2))
			return true;

		if (sameString(uri1.getScheme(), uri2.getScheme()) && sameString(uri1.getSchemeSpecificPart(), uri2.getSchemeSpecificPart()) && sameString(uri1.getFragment(), uri2.getFragment()))
			return true;

		if (uri1.isAbsolute() != uri2.isAbsolute())
			return false;

		// check if we have two local file references that are case variants
		File file1 = toFile(uri1);
		return file1 == null ? false : file1.equals(toFile(uri2));
	}

	private static boolean sameString(String s1, String s2) {
		return (s1 == s2) || s1 != null && s1.equals(s2);
	}

	/**
	 * Returns the URI as a local file, or <code>null</code> if the given
	 * URI does not represent a local file.
	 * @param uri The URI to return the file for
	 * @return The local file corresponding to the given URI, or <code>null</code>
	 */
	public static File toFile(URI uri) {
		if (!isFileURI(uri))
			return null;
		//assume all illegal characters have been properly encoded, so use URI class to unencode
		return new File(uri.getSchemeSpecificPart());
	}

	/**
	 * Returns a Java ARchive (JAR) URI for an entry in a jar or zip file.  The given input URI 
	 * should represent a zip or jar file, but this method will not check for existence or 
	 * validity of a file at the given URI.
	 * <p>
	 * The entry path parameter can optionally be used to obtain the URI of an entry
	 * in a zip or jar file. If an entry path of <code>null</code> is provided, the resulting
	 * URI will represent the jar file itself.
	 * </p>
	 * 
	 * @param uri The URI of a zip or jar file
	 * @param entryPath The path of a file inside the jar, or <code>null</code> to
	 * obtain the URI for the jar file itself.
	 * @return A URI with the "jar" scheme for the given input URI and entry path
	 * @see JarURLConnection
	 */
	public static URI toJarURI(URI uri, IPath entryPath) {
		try {
			if (entryPath == null)
				entryPath = Path.EMPTY;
			//must deconstruct the input URI to obtain unencoded strings, and then pass to URI constructor that will encode the entry path
			return new URI(SCHEME_JAR, uri.getScheme() + ':' + uri.getSchemeSpecificPart() + JAR_SUFFIX + entryPath.toString(), null);
		} catch (URISyntaxException e) {
			//should never happen
			throw new RuntimeException(e);
		}
	}

	/**
	 * Returns the URL as a URI. This method will handle URLs that are
	 * not properly encoded (for example they contain unencoded space characters).
	 * 
	 * @param url The URL to convert into a URI
	 * @return A URI representing the given URL
	 */
	public static URI toURI(URL url) throws URISyntaxException {
		//URL behaves differently across platforms so for file: URLs we parse from string form
		if (SCHEME_FILE.equals(url.getProtocol())) {
			String pathString = url.toExternalForm().substring(5);
			//ensure there is a leading slash to handle common malformed URLs such as file:c:/tmp
			if (pathString.indexOf('/') != 0)
				pathString = '/' + pathString;
			else if (pathString.startsWith(UNC_PREFIX) && !pathString.startsWith(UNC_PREFIX, 2)) {
				//URL encodes UNC path with two slashes, but URI uses four (see bug 207103)
				pathString = ensureUNCPath(pathString);
			}
			return new URI(SCHEME_FILE, null, pathString, null);
		}
		try {
			return new URI(url.toExternalForm());
		} catch (URISyntaxException e) {
			//try multi-argument URI constructor to perform encoding
			return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
		}
	}

	/**
	 * Returns a URI as a URL.
	 * 
	 * <p>Better use {@link URI#toURL()} instead.</p>
	 */
	public static URL toURL(URI uri) throws MalformedURLException {
		return new URL(uri.toString());
	}

	/**
	 * Returns a string representation of the given URI that doesn't have illegal
	 * characters encoded. This string is suitable for later passing to {@link #fromString(String)}.
	 * @param uri The URI to convert to string format
	 * @return An unencoded string representation of the URI
	 */
	public static String toUnencodedString(URI uri) {
		StringBuffer result = new StringBuffer();
		String scheme = uri.getScheme();
		if (scheme != null)
			result.append(scheme).append(':');
		//there is always a ssp
		result.append(uri.getSchemeSpecificPart());
		String fragment = uri.getFragment();
		if (fragment != null)
			result.append('#').append(fragment);
		return result.toString();
	}

	/**
	 * Returns an absolute URI that is created by appending the given relative URI to 
	 * the given base.  If the <tt>relative</tt> URI is already absolute it is simply returned.
	 * <p>
	 * This method is guaranteed to be the inverse of {@link #makeRelative(URI, URI)}.
	 * That is, if R = makeRelative(O, B), then makeAbsolute(R, B), will return the original
	 * URI O.
	 * 
	 * @param relative the relative URI
	 * @param baseURI the base URI
	 * @return an absolute URI
	 */
	public static URI makeAbsolute(URI relative, URI baseURI) {
		if (relative.isAbsolute())
			return relative;
		return append(baseURI, toUnencodedString(relative));
	}

	/**
	 * Returns a URI equivalent to the given original URI, but relative to the given base 
	 * URI if possible.
	 * <p>
	 * This method is equivalent to {@link java.net.URI#relativize}, except for its
	 * handling of file URIs. For file URIs, this method handles file system path devices.
	 * If the URIs are not on the same device, then the original URI is returned.
	 * 
	 * @param original the original URI
	 * @param baseURI the base URI
	 * @return a relative URI
	 */
	public static URI makeRelative(URI original, URI baseURI) {
		// for non-local URIs just use the built in relativize method
		if (!SCHEME_FILE.equals(original.getScheme()) || !SCHEME_FILE.equals(baseURI.getScheme()))
			return baseURI.relativize(original);

		IPath originalPath = new Path(original.getSchemeSpecificPart());
		IPath basePath = new Path(baseURI.getSchemeSpecificPart());

		// make sure we have an absolute path to start
		if (!basePath.isAbsolute())
			return original;
		IPath relativePath = originalPath.makeRelativeTo(basePath);
		//if we could not make it relative, just return the original URI
		if (relativePath == originalPath)
			return original;
		try {
			return new URI(null, null, relativePath.toString(), original.getFragment());
		} catch (URISyntaxException e) {
			//cannot make a relative path, just return the original
			return original;
		}
	}
}

Back to the top