001/* 002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved. 003 * 004 * http://www.izforge.com/izpack/ 005 * http://developer.berlios.de/projects/izpack/ 006 * 007 * Copyright 2004 Chadwick McHenry 008 * 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 */ 021 022package com.izforge.izpack.uninstaller; 023 024import java.io.BufferedOutputStream; 025import java.io.BufferedReader; 026import java.io.File; 027import java.io.FileOutputStream; 028import java.io.IOException; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.OutputStream; 032import java.io.PrintStream; 033import java.io.PrintWriter; 034import java.io.RandomAccessFile; 035import java.lang.reflect.Method; 036import java.lang.reflect.Modifier; 037import java.net.URI; 038import java.net.URL; 039import java.text.CharacterIterator; 040import java.text.SimpleDateFormat; 041import java.text.StringCharacterIterator; 042import java.util.Date; 043import java.util.Enumeration; 044import java.util.jar.JarFile; 045import java.util.zip.ZipEntry; 046 047import com.izforge.izpack.util.OsVersion; 048 049/** 050 * Allows an application to modify the jar file from which it came, including outright deletion. The 051 * jar file of an app is usually locked when java is run so this is normally not possible. 052 * <p> 053 * 054 * Create a SelfModifier with a target method, then invoke the SelfModifier with arguments to be 055 * passed to the target method. The jar file containing the target method's class (obtained by 056 * reflection) will be extracted to a temporary directory, and a new java process will be spawned to 057 * invoke the target method. The original jar file may now be modified. 058 * <p> 059 * 060 * If the constructor or invoke() methods fail, it is generally because secondary java processes 061 * could not be started. 062 * 063 * <b>Requirements</b> 064 * <ul> 065 * <li>The target method, and all it's required classes must be in a jar file. 066 * <li>The Self Modifier, and its inner classes must also be in the jar file. 067 * </ul> 068 * 069 * There are three system processes (or "phases") involved, the first invoked by the user, the 070 * second and third by the SelfModifier. 071 * <p> 072 * 073 * <b>Phase 1:</b> 074 * <ol> 075 * <li>Program is launched, SelfModifier is created, invoke(String[]) is called 076 * <li>A temporary directory (or "sandbox") is created in the default temp directory, and the jar 077 * file contents ar extracted into it 078 * <li>Phase 2 is spawned using the sandbox as it's classpath, SelfModifier as the main class, the 079 * arguments to "invoke(String[])" as the main arguments, and the <a 080 * href="#selfmodsysprops">SelfModifier system properties</a> set. 081 * <li>Immidiately exit so the system unlocks the jar file 082 * </ol> 083 * 084 * <b>Phase 2:</b> 085 * <ol> 086 * <li>Initializes from system properties. 087 * <li>Spawn phase 3 exactly as phase 2 except the self.modifier.phase system properties set to 3. 088 * <li>Wait for phase 3 to die 089 * <li>Delete the temporary sandbox 090 * </ol> 091 * 092 * <b>Phase 3:</b> 093 * <ol> 094 * <li>Initializes from system properties. 095 * <li>Redirect std err stream to the log 096 * <li>Invoke the target method with arguments we were given 097 * <li>The target method is expected to call exit(), or to not start any looping threads (e.g. AWT 098 * thread). In other words, the target is the new "main" method. 099 * </ol> 100 * 101 * <a name="selfmodsysprops"><b>SelfModifier system properties</b></a> used to pass information 102 * between processes. <table border="1"> 103 * <tr> 104 * <th>Constant 105 * <th>System property 106 * <th>description</tr> 107 * <tr> 108 * <td><a href="#BASE_KEY">BASE_KEY</a> 109 * <td>self.mod.jar 110 * <td>base path to log file and sandbox dir</tr> 111 * <tr> 112 * <td><a href="#JAR_KEY">JAR_KEY</a> 113 * <td>self.mod.class 114 * <td>path to original jar file</tr> 115 * <tr> 116 * <td><a href="#CLASS_KEY">CLASS_KEY</a> 117 * <td>self.mod.method 118 * <td>class of target method</tr> 119 * <tr> 120 * <td><a href="#METHOD_KEY">METHOD_KEY</a> 121 * <td>self.mod.phase 122 * <td>name of method to be invoked in sandbox</tr> 123 * <tr> 124 * <td><a href="#PHASE_KEY">PHASE_KEY</a> 125 * <td>self.mod.base 126 * <td>phase of operation to run</tr> 127 * </table> 128 * 129 * @author Chadwick McHenry 130 * @version 1.0 131 */ 132public class SelfModifier 133{ 134 135 /** System property name of base for log and sandbox of secondary processes. */ 136 public static final String BASE_KEY = "self.mod.base"; 137 138 /** System property name of original jar file containing application. */ 139 public static final String JAR_KEY = "self.mod.jar"; 140 141 /** System property name of class declaring target method. */ 142 public static final String CLASS_KEY = "self.mod.class"; 143 144 /** System property name of target method to invoke in secondary process. */ 145 public static final String METHOD_KEY = "self.mod.method"; 146 147 /** System property name of phase (1, 2, or 3) indicator. */ 148 public static final String PHASE_KEY = "self.mod.phase"; 149 150 /** Base prefix name for sandbox and log, used only in phase 1. */ 151 private String prefix = "izpack"; 152 153 /** Target method to be invoked in sandbox. */ 154 private Method method = null; 155 156 /** Log for phase 2 and 3, because we can't capture the stdio from them. */ 157 private File logFile = null; 158 159 /** Directory which we extract too, invoke from, and finally delete. */ 160 private File sandbox = null; 161 162 /** Original jar file program was launched from. */ 163 private File jarFile = null; 164 165 /** Current phase of execution: 1, 2, or 3. */ 166 private int phase = 0; 167 168 /** For logging time. */ 169 private SimpleDateFormat isoPoint = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); 170 171 private Date date = new Date(); 172 173 public static void test(String[] args) 174 { 175 // open a File for random access in the sandbox, which will cause 176 // deletion 177 // of the file and its parent directories to fail until it is closed (by 178 // virtue of this java process halting) 179 try 180 { 181 File sandbox = new File(System.getProperty(BASE_KEY) + ".d"); 182 File randFile = new File(sandbox, "RandomAccess.tmp"); 183 RandomAccessFile rand = new RandomAccessFile(randFile, "rw"); 184 rand.writeChars("Just a test: The jvm has to close 'cuz I won't!\n"); 185 186 System.err.print("Deleting sandbox: "); 187 deleteTree(sandbox); 188 System.err.println(sandbox.exists() ? "FAILED" : "SUCCEEDED"); 189 } 190 catch (Exception x) 191 { 192 System.err.println(x.getMessage()); 193 x.printStackTrace(); 194 } 195 } 196 197 public static void main(String[] args) 198 { 199 // phase 1 already set up the sandbox and spawned phase 2. 200 // phase 2 creates the log, spawns phase 3 and waits 201 // phase 3 invokes method and returns. method must kill all it's threads 202 203 try 204 { 205 // all it's attributes are retrieved from system properties 206 SelfModifier selfModifier = new SelfModifier(); 207 208 // phase 2: invoke a process for phase 3, wait, and clean up 209 if (selfModifier.phase == 2) 210 selfModifier.invoke2(args); 211 212 // phase 3: invoke method and die 213 else if (selfModifier.phase == 3) selfModifier.invoke3(args); 214 } 215 catch (IOException ioe) 216 { 217 System.err.println("Error invoking a secondary phase"); 218 System.err.println("Note that this program is only intended as a secondary process"); 219 ioe.printStackTrace(); 220 } 221 } 222 223 /** 224 * Internal constructor where target class and method are obtained from system properties. 225 * 226 * @throws IOException for errors getting to the sandbox. 227 * @throws SecurityException if access to the target method is denied 228 */ 229 private SelfModifier() throws IOException 230 { 231 phase = Integer.parseInt(System.getProperty(PHASE_KEY)); 232 233 String cName = System.getProperty(CLASS_KEY); 234 String tName = System.getProperty(METHOD_KEY); 235 236 jarFile = new File(System.getProperty(JAR_KEY)); 237 logFile = new File(System.getProperty(BASE_KEY) + ".log"); 238 sandbox = new File(System.getProperty(BASE_KEY) + ".d"); 239 240 // retrieve refrence to target method 241 try 242 { 243 Class clazz = Class.forName(cName); 244 Method method = clazz.getMethod(tName, new Class[] { String[].class}); 245 246 initMethod(method); 247 } 248 catch (ClassNotFoundException x1) 249 { 250 log("No class found for " + cName); 251 } 252 catch (NoSuchMethodException x2) 253 { 254 log("No method " + tName + " found in " + cName); 255 } 256 } 257 258 /** 259 * Creates a SelfModifier which will invoke the target method in a separate process from which 260 * it may modify it's own jar file. 261 * 262 * The target method must be public, static, and take a single array of strings as its only 263 * parameter. The class which declares the method must also be public. Reflection is used to 264 * ensure this. 265 * 266 * @param method a public, static method that accepts a String array as it's only parameter. Any 267 * return value is ignored. 268 * 269 * @throws NullPointerException if <code>method</code> is null 270 * @throws IllegalArgumentException if <code>method</code> is not public, static, and take a 271 * String array as it's only argument, or of it's declaring class is not public. 272 * @throws IllegalStateException if process was not invoked from a jar file, or an IOExceptioin 273 * occured while accessing it 274 * @throws IOException if java is unable to be executed as a separte process 275 * @throws SecurityException if access to the method, or creation of a subprocess is denied 276 */ 277 public SelfModifier(Method method) throws IOException 278 { 279 phase = 1; 280 initJavaExec(); 281 initMethod(method); 282 } 283 284 /** 285 * Check the method for the required properties (public, static, params:(String[])). 286 * 287 * @throws NullPointerException if <code>method</code> is null 288 * @throws IllegalArgumentException if <code>method</code> is not public, static, and take a 289 * String array as it's only argument, or of it's declaring class is not public. 290 * @throws SecurityException if access to the method is denied 291 */ 292 private void initMethod(Method method) 293 { 294 int mod = method.getModifiers(); 295 if ((mod & Modifier.PUBLIC) == 0 || (mod & Modifier.STATIC) == 0) 296 throw new IllegalArgumentException("Method not public and static"); 297 298 Class[] params = method.getParameterTypes(); 299 if (params.length != 1 || !params[0].isArray() 300 || !params[0].getComponentType().getName().equals("java.lang.String")) 301 throw new IllegalArgumentException("Method must accept String array"); 302 303 Class clazz = method.getDeclaringClass(); 304 mod = clazz.getModifiers(); 305 if ((mod & Modifier.PUBLIC) == 0 || (mod & Modifier.INTERFACE) != 0) 306 throw new IllegalArgumentException("Method must be in a public class"); 307 308 this.method = method; 309 } 310 311 /** 312 * This call ensures that java can be exec'd in a separate process. 313 * 314 * @throws IOException if an I/O error occurs, indicating java is unable to be exec'd 315 * @throws SecurityException if a security manager exists and doesn't allow creation of a 316 * subprocess 317 */ 318 private void initJavaExec() throws IOException 319 { 320 try 321 { 322 Process p = Runtime.getRuntime().exec(javaCommand()); 323 324 new StreamProxy(p.getErrorStream(), "err").start(); 325 new StreamProxy(p.getInputStream(), "out").start(); 326 p.getOutputStream().close(); 327 328 // even if it returns an error code, it was at least found 329 p.waitFor(); 330 } 331 catch (InterruptedException ie) 332 { 333 throw new IOException("Unable to create a java subprocess"); 334 } 335 } 336 337 /*********************************************************************************************** 338 * --------------------------------------------------------------------- Phase 1 (call from 339 * external spawn phase 2) --------------------------------------------------------------------- 340 */ 341 342 /** 343 * Invoke the target method in a separate process from which it may modify it's own jar file. 344 * This method does not normally return. After spawning the secondary process, the current 345 * process must die before the jar file is unlocked, therefore calling this method is akin to 346 * calling {@link System#exit(int)}. 347 * <p> 348 * 349 * The contents of the current jar file are extracted copied to a 'sandbox' directory from which 350 * the method is invoked. The path to the original jar file is placed in the system property 351 * {@link #JAR_KEY}. 352 * <p> 353 * 354 * @param args arguments to pass to the target method. May be empty or null to indicate no 355 * arguments. 356 * 357 * @throws IOException for lots of things 358 * @throws IllegalStateException if method's class was not loaded from a jar 359 */ 360 public void invoke(String[] args) throws IOException 361 { 362 // Initialize sandbox and log file to be unique, but similarly named 363 while (true) 364 { 365 logFile = File.createTempFile(prefix, ".log"); 366 String f = logFile.toString(); 367 sandbox = new File(f.substring(0, f.length() - 4) + ".d"); 368 369 // check if the similarly named directory is free 370 if (!sandbox.exists()) break; 371 372 logFile.delete(); 373 } 374 if (!sandbox.mkdir()) throw new RuntimeException("Failed to create temp dir: " + sandbox); 375 376 sandbox = sandbox.getCanonicalFile(); 377 logFile = logFile.getCanonicalFile(); 378 379 jarFile = findJarFile(method.getDeclaringClass()).getCanonicalFile(); 380 if (jarFile == null) throw new IllegalStateException("SelfModifier must be in a jar file"); 381 log("JarFile: " + jarFile); 382 383 extractJarFile(); 384 385 if (args == null) args = new String[0]; 386 spawn(args, 2); 387 388 // finally, if all went well, the invoking process must exit 389 log("Exit"); 390 System.exit(0); 391 } 392 393 /** 394 * Run a new jvm with all the system parameters needed for phases 2 and 3. 395 * 396 * @throws IOException if there is an error getting the cononical name of a path 397 */ 398 private Process spawn(String[] args, int nextPhase) throws IOException 399 { 400 String base = logFile.getAbsolutePath(); 401 base = base.substring(0, base.length() - 4); 402 403 // invoke from tmpdir, passing target method arguments as args, and 404 // SelfModifier parameters as sustem properties 405 String[] javaCmd = new String[] { javaCommand(), "-classpath", sandbox.getAbsolutePath(), 406 "-D" + BASE_KEY + "=" + base, "-D" + JAR_KEY + "=" + jarFile.getPath(), 407 "-D" + CLASS_KEY + "=" + method.getDeclaringClass().getName(), 408 "-D" + METHOD_KEY + "=" + method.getName(), "-D" + PHASE_KEY + "=" + nextPhase, 409 getClass().getName()}; 410 411 String[] entireCmd = new String[javaCmd.length + args.length]; 412 System.arraycopy(javaCmd, 0, entireCmd, 0, javaCmd.length); 413 System.arraycopy(args, 0, entireCmd, javaCmd.length, args.length); 414 415 StringBuffer sb = new StringBuffer("Spawning phase "); 416 sb.append(nextPhase).append(": "); 417 for (int i = 0; i < entireCmd.length; i++) 418 sb.append("\n\t").append(entireCmd[i]); 419 log(sb.toString()); 420 421 // Just invoke it and let it go, the exception will be caught above 422 // Won't compile on < jdk1.3, but will run on jre1.2 423 if (JAVA_SPECIFICATION_VERSION < 1.3) 424 return Runtime.getRuntime().exec(entireCmd, null); 425 else 426 return Runtime.getRuntime().exec(entireCmd, null, null); // workDir); 427 } 428 429 /** 430 * Retrieve the jar file the specified class was loaded from. 431 * 432 * @return null if file was not loaded from a jar file 433 * @throws SecurityException if access to is denied by SecurityManager 434 */ 435 public static File findJarFile(Class clazz) 436 { 437 String resource = clazz.getName().replace('.', '/') + ".class"; 438 439 URL url = ClassLoader.getSystemResource(resource); 440 if (!url.getProtocol().equals("jar")) return null; 441 442 String path = url.getFile(); 443 // starts at "file:..." (use getPath() as of 1.3) 444 path = path.substring(0, path.lastIndexOf('!')); 445 446 File file; 447 448 // getSystemResource() returns a valid URL (eg. spaces are %20), but a 449 // file 450 // Constructed w/ it will expect "%20" in path. URI and File(URI) 451 // properly 452 // deal with escaping back and forth, but didn't exist until 1.4 453 if (JAVA_SPECIFICATION_VERSION < 1.4) 454 file = new File(fromURI(path)); 455 else 456 file = new File(URI.create(path)); 457 458 return file; 459 } 460 461 /** 462 * @throws IOException 463 */ 464 private void extractJarFile() throws IOException 465 { 466 byte[] buf = new byte[5120]; 467 int extracted = 0; 468 InputStream in = null; 469 OutputStream out = null; 470 String MANIFEST = "META-INF/MANIFEST.MF"; 471 472 JarFile jar = new JarFile(jarFile, true); 473 474 try 475 { 476 Enumeration entries = jar.entries(); 477 while (entries.hasMoreElements()) 478 { 479 ZipEntry entry = (ZipEntry) entries.nextElement(); 480 if (entry.isDirectory()) continue; 481 482 String pathname = entry.getName(); 483 if (MANIFEST.equals(pathname.toUpperCase())) continue; 484 485 in = jar.getInputStream(entry); 486 487 File outFile = new File(sandbox, pathname); 488 File parent = outFile.getParentFile(); 489 if (parent != null && !parent.exists()) parent.mkdirs(); 490 491 out = new BufferedOutputStream(new FileOutputStream(outFile)); 492 493 int n; 494 while ((n = in.read(buf, 0, buf.length)) > 0) 495 out.write(buf, 0, n); 496 497 out.close(); 498 extracted++; 499 } 500 jar.close(); 501 502 log("Extracted " + extracted + " file" + (extracted > 1 ? "s" : "") + " into " 503 + sandbox.getPath()); 504 } 505 finally 506 { 507 try 508 { 509 jar.close(); 510 } 511 catch (IOException ioe) 512 {} 513 if (out != null) 514 { 515 try 516 { 517 out.close(); 518 } 519 catch (IOException ioe) 520 {} 521 } 522 if (in != null) 523 { 524 try 525 { 526 in.close(); 527 } 528 catch (IOException ioe) 529 {} 530 } 531 } 532 } 533 534 /*********************************************************************************************** 535 * --------------------------------------------------------------------- Phase 2 (spawn the 536 * phase 3 and clean up) --------------------------------------------------------------------- 537 */ 538 539 /** 540 * Invoke phase 2, which starts phase 3, then cleans up the sandbox. This is needed because 541 * GUI's often call the exit() method to kill the AWT thread, and early versions of java did not 542 * have exit hooks. In order to delete the sandbox on exit we invoke method in separate process 543 * and wait for that process to complete. Even worse, resources in the jar may be locked by the 544 * target process, which would prevent the sandbox from being deleted as well. 545 */ 546 private void invoke2(String[] args) 547 { 548 549 int retVal = -1; 550 try 551 { 552 // TODO: in jre 1.2, Phs1 consistently needs more time to unlock the 553 // original jar. Phs2 should wait to invoke Phs3 until it knows its 554 // parent (Phs1) has died, but Process.waitFor() only works on 555 // children. Can we see when a parent dies, or /this/ Process 556 // becomes 557 // orphaned? 558 try 559 { 560 Thread.sleep(1000); 561 } 562 catch (Exception x) 563 {} 564 565 // spawn phase 3, capture its stdio and wait for it to exit 566 Process p = spawn(args, 3); 567 568 new StreamProxy(p.getErrorStream(), "err", log).start(); 569 new StreamProxy(p.getInputStream(), "out", log).start(); 570 p.getOutputStream().close(); 571 572 try 573 { 574 retVal = p.waitFor(); 575 } 576 catch (InterruptedException e) 577 { 578 log(e); 579 } 580 581 // clean up and go 582 log("deleteing sandbox"); 583 deleteTree(sandbox); 584 } 585 catch (Exception e) 586 { 587 log(e); 588 } 589 log("Phase 3 return value = " + retVal); 590 } 591 592 /** Recursively delete a file structure. */ 593 public static boolean deleteTree(File file) 594 { 595 if (file.isDirectory()) 596 { 597 File[] files = file.listFiles(); 598 for (int i = 0; i < files.length; i++) 599 deleteTree(files[i]); 600 } 601 return file.delete(); 602 } 603 604 /*********************************************************************************************** 605 * --------------------------------------------------------------------- Phase 3 (invoke method, 606 * let it go as long as it likes) 607 * --------------------------------------------------------------------- 608 */ 609 610 /** 611 * Invoke the target method and let it run free! 612 */ 613 private void invoke3(String[] args) 614 { 615 // std io is being redirected to the log 616 try 617 { 618 errlog("Invoking method: " + method.getDeclaringClass().getName() + "." 619 + method.getName() + "(String[] args)"); 620 621 method.invoke(null, new Object[] { args}); 622 } 623 catch (Throwable t) 624 { 625 errlog(t.getMessage()); 626 t.printStackTrace(); 627 errlog("exiting"); 628 System.err.flush(); 629 System.exit(31); 630 } 631 632 errlog("Method returned, waiting for other threads"); 633 System.err.flush(); 634 // now let the method call exit... 635 } 636 637 /*********************************************************************************************** 638 * --------------------------------------------------------------------- Logging 639 * --------------------------------------------------------------------- 640 */ 641 642 PrintStream log = null; 643 644 private void errlog(String msg) 645 { 646 date.setTime(System.currentTimeMillis()); 647 System.err.println(isoPoint.format(date) + " Phase " + phase + ": " + msg); 648 } 649 650 private PrintStream checkLog() 651 { 652 try 653 { 654 if (log == null) log = new PrintStream(new FileOutputStream(logFile.toString(), true)); 655 } 656 catch (IOException x) 657 { 658 System.err.println("Phase " + phase + " log err: " + x.getMessage()); 659 x.printStackTrace(); 660 } 661 date.setTime(System.currentTimeMillis()); 662 return log; 663 } 664 665 private void log(Throwable t) 666 { 667 if (checkLog() != null) 668 { 669 log.println(isoPoint.format(date) + " Phase " + phase + ": " + t.getMessage()); 670 t.printStackTrace(log); 671 } 672 } 673 674 private void log(String msg) 675 { 676 if (checkLog() != null) 677 log.println(isoPoint.format(date) + " Phase " + phase + ": " + msg); 678 } 679 680 public static class StreamProxy extends Thread 681 { 682 683 InputStream in; 684 685 String name; 686 687 OutputStream out; 688 689 public StreamProxy(InputStream in, String name) 690 { 691 this(in, name, null); 692 } 693 694 public StreamProxy(InputStream in, String name, OutputStream out) 695 { 696 this.in = in; 697 this.name = name; 698 this.out = out; 699 } 700 701 public void run() 702 { 703 try 704 { 705 PrintWriter pw = null; 706 if (out != null) pw = new PrintWriter(out); 707 708 BufferedReader br = new BufferedReader(new InputStreamReader(in)); 709 String line; 710 while ((line = br.readLine()) != null) 711 { 712 if (pw != null) pw.println(line); 713 // System.out.println(name + ">" + line); 714 } 715 if (pw != null) pw.flush(); 716 } 717 catch (IOException ioe) 718 { 719 ioe.printStackTrace(); 720 } 721 } 722 } 723 724 /*********************************************************************************************** 725 * --------------------------------------------------------------------- Apache ant code 726 * --------------------------------------------------------------------- 727 */ 728 // This was stolen (and specialized from much more modular code) from the 729 // jakarta ant class org.apache.tools.ant.taskdefs.condition.Os 730 // See the javaCommand() method. 731 private static final float JAVA_SPECIFICATION_VERSION = Float.parseFloat(System 732 .getProperty("java.specification.version")); 733 734 private static final String JAVA_HOME = System.getProperty("java.home"); 735 736 /** 737 * Constructs a file path from a <code>file:</code> URI. 738 * 739 * <p> 740 * Will be an absolute path if the given URI is absolute. 741 * </p> 742 * 743 * <p> 744 * Swallows '%' that are not followed by two characters, doesn't deal with non-ASCII characters. 745 * </p> 746 * 747 * @param uri the URI designating a file in the local filesystem. 748 * @return the local file system path for the file. 749 */ 750 public static String fromURI(String uri) 751 { 752 if (!uri.startsWith("file:")) 753 throw new IllegalArgumentException("Can only handle file: URIs"); 754 755 if (uri.startsWith("file://")) 756 uri = uri.substring(7); 757 else 758 uri = uri.substring(5); 759 760 uri = uri.replace('/', File.separatorChar); 761 if (File.pathSeparatorChar == ';' && uri.startsWith("\\") && uri.length() > 2 762 && Character.isLetter(uri.charAt(1)) && uri.lastIndexOf(':') > -1) 763 { 764 uri = uri.substring(1); 765 } 766 767 StringBuffer sb = new StringBuffer(); 768 CharacterIterator iter = new StringCharacterIterator(uri); 769 for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) 770 { 771 if (c == '%') 772 { 773 char c1 = iter.next(); 774 if (c1 != CharacterIterator.DONE) 775 { 776 int i1 = Character.digit(c1, 16); 777 char c2 = iter.next(); 778 if (c2 != CharacterIterator.DONE) 779 { 780 int i2 = Character.digit(c2, 16); 781 sb.append((char) ((i1 << 4) + i2)); 782 } 783 } 784 } 785 else 786 { 787 sb.append(c); 788 } 789 } 790 791 String path = sb.toString(); 792 return path; 793 } 794 795 private static String addExtension(String command) 796 { 797 // This is the most common extension case - exe for windows and OS/2, 798 // nothing for *nix. 799 return command + (OsVersion.IS_WINDOWS || OsVersion.IS_OS2 ? ".exe" : ""); 800 } 801 802 private static String javaCommand() 803 { 804 // This was stolen (and specialized from much more modular code) from 805 // the 806 // jakarta ant classes Os & JavaEnvUtils. Also see the following 807 // org.apache.tools.ant.taskdefs.Java 808 // org.apache.tools.ant.taskdefs.Execute 809 // org.apache.tools.ant.taskdefs.condition.Os 810 // org.apache.tools.ant.util.CommandlineJava 811 // org.apache.tools.ant.util.JavaEnvUtils 812 // org.apache.tools.ant.util.FileUtils 813 // TODO: I didn't copy nearly all of their conditions 814 String executable = addExtension("java"); 815 String dir = new File(JAVA_HOME + "/bin").getAbsolutePath(); 816 File jExecutable = new File(dir, executable); 817 818 // Unfortunately on Windows java.home doesn't always refer 819 // to the correct location, so we need to fall back to 820 // assuming java is somewhere on the PATH. 821 if (!jExecutable.exists()) return executable; 822 return jExecutable.getAbsolutePath(); 823 } 824}