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}