1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18 package org.apache.tools.ant.launch;
19
20 import java.net.MalformedURLException;
21
22 import java.net.URL;
23 import java.io.File;
24 import java.io.FilenameFilter;
25 import java.io.ByteArrayOutputStream;
26 import java.io.UnsupportedEncodingException;
27 import java.text.CharacterIterator;
28 import java.text.StringCharacterIterator;
29 import java.util.Locale;
30
31 // CheckStyle:LineLengthCheck OFF - urls are long!
32 /**
33 * The Locator is a utility class which is used to find certain items
34 * in the environment.
35 *
36 * It is used at boot time in the launcher, and cannot make use of any of Ant's other classes.
37 *
38 * This is a surprisingly brittle piece of code, and has had lots of bugs filed against it.
39 * {@link <a href="http://issues.apache.org/bugzilla/show_bug.cgi?id=42275">running ant off a network share can cause Ant to fail</a>}
40 * {@link <a href="http://issues.apache.org/bugzilla/show_bug.cgi?id=8031">use File.toURI().toURL().toExternalForm()</a>}
41 * {@link <a href="http://issues.apache.org/bugzilla/show_bug.cgi?id=42222">Locator implementation not encoding URI strings properly: spaces in paths</a>}
42 * It also breaks Eclipse 3.3 Betas
43 * {@link <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=183283">Exception if installation path has spaces</a>}
44 *
45 * Be very careful when making changes to this class, as a break will upset a lot of people.
46 * @since Ant 1.6
47 */
48 // CheckStyle:LineLengthCheck ON - urls are long!
49 public final class Locator {
50
51 private static final int NIBBLE = 4;
52 private static final int NIBBLE_MASK = 0xF;
53
54 private static final int ASCII_SIZE = 128;
55
56 private static final int BYTE_SIZE = 256;
57
58 private static final int WORD = 16;
59
60 private static final int SPACE = 0x20;
61 private static final int DEL = 0x7F;
62
63 /**
64 * encoding used to represent URIs
65 */
66 public static final String URI_ENCODING = "UTF-8";
67 // stolen from org.apache.xerces.impl.XMLEntityManager#getUserDir()
68 // of the Xerces-J team
69 // which ASCII characters need to be escaped
70 private static boolean[] gNeedEscaping = new boolean[ASCII_SIZE];
71 // the first hex character if a character needs to be escaped
72 private static char[] gAfterEscaping1 = new char[ASCII_SIZE];
73 // the second hex character if a character needs to be escaped
74 private static char[] gAfterEscaping2 = new char[ASCII_SIZE];
75 private static char[] gHexChs = {'0', '1', '2', '3', '4', '5', '6', '7',
76 '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
77 /** Error string used when an invalid uri is seen */
78 public static final String ERROR_NOT_FILE_URI
79 = "Can only handle valid file: URIs, not ";
80
81 // initialize the above 3 arrays
82 static {
83 for (int i = 0; i < SPACE; i++) {
84 gNeedEscaping[i] = true;
85 gAfterEscaping1[i] = gHexChs[i >> NIBBLE];
86 gAfterEscaping2[i] = gHexChs[i & NIBBLE_MASK];
87 }
88 gNeedEscaping[DEL] = true;
89 gAfterEscaping1[DEL] = '7';
90 gAfterEscaping2[DEL] = 'F';
91 char[] escChs = {' ', '<', '>', '#', '%', '"', '{', '}',
92 '|', '\\', '^', '~', '[', ']', '`'};
93 int len = escChs.length;
94 char ch;
95 for (int i = 0; i < len; i++) {
96 ch = escChs[i];
97 gNeedEscaping[ch] = true;
98 gAfterEscaping1[ch] = gHexChs[ch >> NIBBLE];
99 gAfterEscaping2[ch] = gHexChs[ch & NIBBLE_MASK];
100 }
101 }
102 /**
103 * Not instantiable
104 */
105 private Locator() {
106 }
107
108 /**
109 * Find the directory or jar file the class has been loaded from.
110 *
111 * @param c the class whose location is required.
112 * @return the file or jar with the class or null if we cannot
113 * determine the location.
114 *
115 * @since Ant 1.6
116 */
117 public static File getClassSource(Class c) {
118 String classResource = c.getName().replace('.', '/') + ".class";
119 return getResourceSource(c.getClassLoader(), classResource);
120 }
121
122 /**
123 * Find the directory or jar a given resource has been loaded from.
124 *
125 * @param c the classloader to be consulted for the source.
126 * @param resource the resource whose location is required.
127 *
128 * @return the file with the resource source or null if
129 * we cannot determine the location.
130 *
131 * @since Ant 1.6
132 */
133 public static File getResourceSource(ClassLoader c, String resource) {
134 if (c == null) {
135 c = Locator.class.getClassLoader();
136 }
137 URL url = null;
138 if (c == null) {
139 url = ClassLoader.getSystemResource(resource);
140 } else {
141 url = c.getResource(resource);
142 }
143 if (url != null) {
144 String u = url.toString();
145 try {
146 if (u.startsWith("jar:file:")) {
147 int pling = u.indexOf("!");
148 String jarName = u.substring("jar:".length(), pling);
149 return new File(fromURI(jarName));
150 } else if (u.startsWith("file:")) {
151 int tail = u.indexOf(resource);
152 String dirName = u.substring(0, tail);
153 return new File(fromURI(dirName));
154 }
155 } catch (IllegalArgumentException e) {
156 //unable to determine the URI for reasons unknown.
157 return null;
158 }
159 }
160 return null;
161 }
162
163 /**
164 * Constructs a file path from a <code>file:</code> URI.
165 *
166 * <p>Will be an absolute path if the given URI is absolute.</p>
167 *
168 * <p>Prior to Java 1.4,
169 * swallows '%' that are not followed by two characters.</p>
170 *
171 * See <a href="http://www.w3.org/TR/xml11/#dt-sysid">dt-sysid</a>
172 * which makes some mention of how
173 * characters not supported by URI Reference syntax should be escaped.
174 *
175 * @param uri the URI designating a file in the local filesystem.
176 * @return the local file system path for the file.
177 * @throws IllegalArgumentException if the URI is malformed or not a legal file: URL
178 * @since Ant 1.6
179 */
180 public static String fromURI(String uri) {
181 // #buzilla8031: first try Java 1.4.
182 String result = null;
183 //result = fromUriJava14(uri);
184 if (result == null) {
185 result = fromURIJava13(uri);
186 }
187 return result;
188 }
189
190
191 /**
192 * Java1.4+ code to extract the path from the URI.
193 * @param uri
194 * @return null if a conversion was not possible
195 */
196 private static String fromUriJava14(String uri) {
197 Class uriClazz = null;
198 try {
199 uriClazz = Class.forName("java.net.URI");
200 } catch (ClassNotFoundException cnfe) {
201 // Fine, Java 1.3 or earlier, do it by hand.
202 return null;
203 }
204 // Also check for properly formed URIs. Ant formerly recommended using
205 // nonsense URIs such as "file:./foo.xml" in XML includes. You shouldn't
206 // do that (just "foo.xml" is correct) but for compatibility we special-case
207 // things when the path is not absolute, and fall back to the old parsing behavior.
208 if (uriClazz != null && uri.startsWith("file:/")) {
209 try {
210 java.lang.reflect.Method createMethod
211 = uriClazz.getMethod("create", new Class[]{String.class});
212 Object uriObj = createMethod.invoke(null, new Object[]{encodeURI(uri)});
213 java.lang.reflect.Constructor fileConst
214 = File.class.getConstructor(new Class[]{uriClazz});
215 File f = (File) fileConst.newInstance(new Object[]{uriObj});
216 //bug #42227 forgot to decode before returning
217 return decodeUri(f.getAbsolutePath());
218 } catch (java.lang.reflect.InvocationTargetException e) {
219 Throwable e2 = e.getTargetException();
220 if (e2 instanceof IllegalArgumentException) {
221 // Bad URI, pass this on.
222 // no, this is downgraded to a warning after various
223 // JRE bugs surfaced. Hand off
224 // to our built in code on a failure
225 //throw new IllegalArgumentException(
226 // "Bad URI " + uri + ":" + e2.getMessage(), e2);
227 e2.printStackTrace();
228
229 } else {
230 // Unexpected target exception? Should not happen.
231 e2.printStackTrace();
232 }
233 } catch (Exception e) {
234 // Reflection problems? Should not happen, debug.
235 e.printStackTrace();
236 }
237 }
238 return null;
239 }
240
241 /**
242 * package-private for testing in same classloader
243 * @param uri uri to expand
244 * @return the decoded URI
245 * @since Ant1.7.1
246 */
247 static String fromURIJava13(String uri) {
248 // Fallback method for Java 1.3 or earlier.
249
250 URL url = null;
251 try {
252 url = new URL(uri);
253 } catch (MalformedURLException emYouEarlEx) {
254 // Ignore malformed exception
255 }
256 if (url == null || !("file".equals(url.getProtocol()))) {
257 throw new IllegalArgumentException(ERROR_NOT_FILE_URI + uri);
258 }
259 StringBuffer buf = new StringBuffer(url.getHost());
260 if (buf.length() > 0) {
261 buf.insert(0, File.separatorChar).insert(0, File.separatorChar);
262 }
263 String file = url.getFile();
264 int queryPos = file.indexOf('?');
265 buf.append((queryPos < 0) ? file : file.substring(0, queryPos));
266
267 uri = buf.toString().replace('/', File.separatorChar);
268
269 if (File.pathSeparatorChar == ';' && uri.startsWith("\\") && uri.length() > 2
270 && Character.isLetter(uri.charAt(1)) && uri.lastIndexOf(':') > -1) {
271 uri = uri.substring(1);
272 }
273 String path = null;
274 try {
275 path = decodeUri(uri);
276 String cwd = System.getProperty("user.dir");
277 int posi = cwd.indexOf(":");
278 if ((posi > 0) && path.startsWith(File.separator)) {
279 path = cwd.substring(0, posi + 1) + path;
280 }
281 } catch (UnsupportedEncodingException exc) {
282 // not sure whether this is clean, but this method is
283 // declared not to throw exceptions.
284 throw new IllegalStateException(
285 "Could not convert URI " + uri + " to path: "
286 + exc.getMessage());
287 }
288 return path;
289 }
290
291 /**
292 * Decodes an Uri with % characters.
293 * The URI is escaped
294 * @param uri String with the uri possibly containing % characters.
295 * @return The decoded Uri
296 * @throws UnsupportedEncodingException if UTF-8 is not available
297 * @since Ant 1.7
298 */
299 public static String decodeUri(String uri) throws UnsupportedEncodingException {
300 if (uri.indexOf('%') == -1) {
301 return uri;
302 }
303 ByteArrayOutputStream sb = new ByteArrayOutputStream(uri.length());
304 CharacterIterator iter = new StringCharacterIterator(uri);
305 for (char c = iter.first(); c != CharacterIterator.DONE;
306 c = iter.next()) {
307 if (c == '%') {
308 char c1 = iter.next();
309 if (c1 != CharacterIterator.DONE) {
310 int i1 = Character.digit(c1, WORD);
311 char c2 = iter.next();
312 if (c2 != CharacterIterator.DONE) {
313 int i2 = Character.digit(c2, WORD);
314 sb.write((char) ((i1 << NIBBLE) + i2));
315 }
316 }
317 } else {
318 sb.write(c);
319 }
320 }
321 return sb.toString(URI_ENCODING);
322 }
323
324 /**
325 * Encodes an Uri with % characters.
326 * The URI is escaped
327 * @param path String to encode.
328 * @return The encoded string, according to URI norms
329 * @throws UnsupportedEncodingException if UTF-8 is not available
330 * @since Ant 1.7
331 */
332 public static String encodeURI(String path) throws UnsupportedEncodingException {
333 int i = 0;
334 int len = path.length();
335 int ch = 0;
336 StringBuffer sb = null;
337 for (; i < len; i++) {
338 ch = path.charAt(i);
339 // if it's not an ASCII character, break here, and use UTF-8 encoding
340 if (ch >= ASCII_SIZE) {
341 break;
342 }
343 if (gNeedEscaping[ch]) {
344 if (sb == null) {
345 sb = new StringBuffer(path.substring(0, i));
346 }
347 sb.append('%');
348 sb.append(gAfterEscaping1[ch]);
349 sb.append(gAfterEscaping2[ch]);
350 // record the fact that it's escaped
351 } else if (sb != null) {
352 sb.append((char) ch);
353 }
354 }
355
356 // we saw some non-ascii character
357 if (i < len) {
358 if (sb == null) {
359 sb = new StringBuffer(path.substring(0, i));
360 }
361 // get UTF-8 bytes for the remaining sub-string
362 byte[] bytes = null;
363 byte b;
364 bytes = path.substring(i).getBytes(URI_ENCODING);
365 len = bytes.length;
366
367 // for each byte
368 for (i = 0; i < len; i++) {
369 b = bytes[i];
370 // for non-ascii character: make it positive, then escape
371 if (b < 0) {
372 ch = b + BYTE_SIZE;
373 sb.append('%');
374 sb.append(gHexChs[ch >> NIBBLE]);
375 sb.append(gHexChs[ch & NIBBLE_MASK]);
376 } else if (gNeedEscaping[b]) {
377 sb.append('%');
378 sb.append(gAfterEscaping1[b]);
379 sb.append(gAfterEscaping2[b]);
380 } else {
381 sb.append((char) b);
382 }
383 }
384 }
385 return sb == null ? path : sb.toString();
386 }
387
388 /**
389 * Convert a File to a URL.
390 * File.toURL() does not encode characters like #.
391 * File.toURI() has been introduced in java 1.4, so
392 * ANT cannot use it (except by reflection)
393 * FileUtils.toURI() cannot be used by Locator.java
394 * Implemented this way.
395 * File.toURL() adds file: and changes '\' to '/' for dos OSes
396 * encodeURI converts characters like ' ' and '#' to %DD
397 * @param file the file to convert
398 * @return URL the converted File
399 * @throws MalformedURLException on error
400 */
401 public static URL fileToURL(File file)
402 throws MalformedURLException {
403 try {
404 return new URL(encodeURI(file.toURL().toString()));
405 } catch (UnsupportedEncodingException ex) {
406 throw new MalformedURLException(ex.toString());
407 }
408 }
409
410 /**
411 * Get the File necessary to load the Sun compiler tools. If the classes
412 * are available to this class, then no additional URL is required and
413 * null is returned. This may be because the classes are explicitly in the
414 * class path or provided by the JVM directly.
415 *
416 * @return the tools jar as a File if required, null otherwise.
417 */
418 public static File getToolsJar() {
419 // firstly check if the tools jar is already in the classpath
420 boolean toolsJarAvailable = false;
421 try {
422 // just check whether this throws an exception
423 Class.forName("com.sun.tools.javac.Main");
424 toolsJarAvailable = true;
425 } catch (Exception e) {
426 try {
427 Class.forName("sun.tools.javac.Main");
428 toolsJarAvailable = true;
429 } catch (Exception e2) {
430 // ignore
431 }
432 }
433 if (toolsJarAvailable) {
434 return null;
435 }
436 // couldn't find compiler - try to find tools.jar
437 // based on java.home setting
438 String libToolsJar
439 = File.separator + "lib" + File.separator + "tools.jar";
440 String javaHome = System.getProperty("java.home");
441 File toolsJar = new File(javaHome + libToolsJar);
442 if (toolsJar.exists()) {
443 // Found in java.home as given
444 return toolsJar;
445 }
446 if (javaHome.toLowerCase(Locale.US).endsWith(File.separator + "jre")) {
447 javaHome = javaHome.substring(
448 0, javaHome.length() - "/jre".length());
449 toolsJar = new File(javaHome + libToolsJar);
450 }
451 if (!toolsJar.exists()) {
452 System.out.println("Unable to locate tools.jar. "
453 + "Expected to find it in " + toolsJar.getPath());
454 return null;
455 }
456 return toolsJar;
457 }
458
459 /**
460 * Get an array of URLs representing all of the jar files in the
461 * given location. If the location is a file, it is returned as the only
462 * element of the array. If the location is a directory, it is scanned for
463 * jar files.
464 *
465 * @param location the location to scan for Jars.
466 *
467 * @return an array of URLs for all jars in the given location.
468 *
469 * @exception MalformedURLException if the URLs for the jars cannot be
470 * formed.
471 */
472 public static URL[] getLocationURLs(File location)
473 throws MalformedURLException {
474 return getLocationURLs(location, new String[]{".jar"});
475 }
476
477 /**
478 * Get an array of URLs representing all of the files of a given set of
479 * extensions in the given location. If the location is a file, it is
480 * returned as the only element of the array. If the location is a
481 * directory, it is scanned for matching files.
482 *
483 * @param location the location to scan for files.
484 * @param extensions an array of extension that are to match in the
485 * directory search.
486 *
487 * @return an array of URLs of matching files.
488 * @exception MalformedURLException if the URLs for the files cannot be
489 * formed.
490 */
491 public static URL[] getLocationURLs(File location,
492 final String[] extensions)
493 throws MalformedURLException {
494 URL[] urls = new URL[0];
495
496 if (!location.exists()) {
497 return urls;
498 }
499 if (!location.isDirectory()) {
500 urls = new URL[1];
501 String path = location.getPath();
502 String littlePath = path.toLowerCase(Locale.US);
503 for (int i = 0; i < extensions.length; ++i) {
504 if (littlePath.endsWith(extensions[i])) {
505 urls[0] = fileToURL(location);
506 break;
507 }
508 }
509 return urls;
510 }
511 File[] matches = location.listFiles(
512 new FilenameFilter() {
513 public boolean accept(File dir, String name) {
514 String littleName = name.toLowerCase(Locale.US);
515 for (int i = 0; i < extensions.length; ++i) {
516 if (littleName.endsWith(extensions[i])) {
517 return true;
518 }
519 }
520 return false;
521 }
522 });
523 urls = new URL[matches.length];
524 for (int i = 0; i < matches.length; ++i) {
525 urls[i] = fileToURL(matches[i]);
526 }
527 return urls;
528 }
529 }