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 Elmar Klaus Bartz
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.util;
023
024import java.io.BufferedInputStream;
025import java.io.BufferedOutputStream;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.IOException;
030import java.util.Properties;
031import java.util.StringTokenizer;
032
033/**
034 * <p>
035 * Class with some IO related helper.
036 * </p>
037 * 
038 */
039public class IoHelper
040{
041
042    // This class uses the same values for family and flavor as
043    // TargetFactory. But this class should not depends on TargetFactory,
044    // because it is possible that TargetFactory is not bound. Therefore
045    // the definition here again.
046
047    // ------------------------------------------------------------------------
048    // Constant Definitions
049    // ------------------------------------------------------------------------
050
051    /** Placeholder during translatePath computing */
052    private static final String MASKED_SLASH_PLACEHOLDER = "~&_&~";
053
054    private static Properties envVars = null;
055
056    /**
057     * Default constructor
058     */
059    private IoHelper()
060    {
061    }
062
063    /**
064     * Copies the contents of inFile into outFile.
065     * 
066     * @param inFile path of file which should be copied
067     * @param outFile path of file to create and copy the contents of inFile into
068     */
069    public static void copyFile(String inFile, String outFile) throws IOException
070    {
071        copyFile(new File(inFile), new File(outFile));
072    }
073
074    /**
075     * Creates an in- and output stream for the given File objects and copies all the data from the
076     * specified input to the specified output.
077     * 
078     * @param inFile File object for input
079     * @param outFile File object for output
080     * @exception IOException if an I/O error occurs
081     */
082    public static void copyFile(File inFile, File outFile) throws IOException
083    {
084        copyFile(inFile, outFile, null, null);
085    }
086
087    /**
088     * Creates an in- and output stream for the given File objects and copies all the data from the
089     * specified input to the specified output. If permissions is not null, a chmod will be done on
090     * the output file.
091     * 
092     * @param inFile File object for input
093     * @param outFile File object for output
094     * @param permissions permissions for the output file
095     * @exception IOException if an I/O error occurs
096     */
097    public static void copyFile(File inFile, File outFile, String permissions) throws IOException
098    {
099        copyFile(inFile, outFile, permissions, null);
100    }
101
102    /**
103     * Creates an in- and output stream for the given File objects and copies all the data from the
104     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
105     * will be done during copy.
106     * 
107     * @param inFile File object for input
108     * @param outFile File object for output
109     * @param vss substitutor which is used during copying
110     * @exception IOException if an I/O error occurs
111     */
112    public static void copyFile(File inFile, File outFile, VariableSubstitutor vss)
113            throws IOException
114    {
115        copyFile(inFile, outFile, null, vss);
116    }
117
118    /**
119     * Creates an in- and output stream for the given File objects and copies all the data from the
120     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
121     * will be done during copy. If permissions is not null, a chmod will be done on the output
122     * file.
123     * 
124     * @param inFile File object for input
125     * @param outFile File object for output
126     * @param permissions permissions for the output file
127     * @param vs substitutor which is used during copying
128     * @exception IOException if an I/O error occurs
129     */
130    public static void copyFile(File inFile, File outFile, String permissions,
131            VariableSubstitutor vs) throws IOException
132    {
133        copyFile(inFile, outFile, permissions, vs, null);
134    }
135
136    /**
137     * Creates an in- and output stream for the given File objects and copies all the data from the
138     * specified input to the specified output. If the VariableSubstitutor is not null, a substition
139     * will be done during copy. If permissions is not null, a chmod will be done on the output
140     * file. If type is not null, that type is used as file type at substitution.
141     * 
142     * @param inFile File object for input
143     * @param outFile File object for output
144     * @param permissions permissions for the output file
145     * @param vs substitutor which is used during copying
146     * @param type file type for the substitutor
147     * @exception IOException if an I/O error occurs
148     */
149    public static void copyFile(File inFile, File outFile, String permissions,
150            VariableSubstitutor vs, String type) throws IOException
151    {
152        FileOutputStream out = new FileOutputStream(outFile);
153        FileInputStream in = new FileInputStream(inFile);
154        if (vs == null)
155        {
156            byte[] buffer = new byte[5120];
157            long bytesCopied = 0;
158            int bytesInBuffer;
159            while ((bytesInBuffer = in.read(buffer)) != -1)
160            {
161                out.write(buffer, 0, bytesInBuffer);
162                bytesCopied += bytesInBuffer;
163            }
164            in.close();
165            out.close();
166        }
167        else
168        {
169            BufferedInputStream bin = new BufferedInputStream(in, 5120);
170            BufferedOutputStream bout = new BufferedOutputStream(out, 5120);
171            vs.substitute(bin, bout, type, null);
172            bin.close();
173            bout.close();
174        }
175        if (permissions != null && IoHelper.supported("chmod"))
176        {
177            chmod(outFile.getAbsolutePath(), permissions);
178        }
179    }
180
181    /**
182     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
183     * possible, else the default extension is used. The contents of template will be copied into
184     * the temporary file.
185     * 
186     * @param template file to copy from and define file extension
187     * @param defaultExtension file extension if no is contained in template
188     * @return newly created and filled temporary file
189     * @throws IOException
190     */
191    public static File copyToTempFile(File template, String defaultExtension) throws IOException
192    {
193        return copyToTempFile(template, defaultExtension, null);
194    }
195
196    /**
197     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
198     * possible, else the default extension is used. The contents of template will be copied into
199     * the temporary file. If the variable substitutor is not null, variables will be replaced
200     * during copying.
201     * 
202     * @param template file to copy from and define file extension
203     * @param defaultExtension file extension if no is contained in template
204     * @param vss substitutor which is used during copying
205     * @return newly created and filled temporary file
206     * @throws IOException
207     */
208    public static File copyToTempFile(File template, String defaultExtension,
209            VariableSubstitutor vss) throws IOException
210    {
211        String path = template.getCanonicalPath();
212        int pos = path.lastIndexOf('.');
213        String ext = path.substring(pos);
214        if (ext == null) ext = defaultExtension;
215        File tmpFile = File.createTempFile("izpack_io", ext);
216        tmpFile.deleteOnExit();
217        IoHelper.copyFile(template, tmpFile, vss);
218        return tmpFile;
219    }
220
221    /**
222     * Creates a temp file with delete on exit rule. The extension is extracted from the template if
223     * possible, else the default extension is used. The contents of template will be copied into
224     * the temporary file.
225     * 
226     * @param template file to copy from and define file extension
227     * @param defaultExtension file extension if no is contained in template
228     * @return newly created and filled temporary file
229     * @throws IOException
230     */
231    public static File copyToTempFile(String template, String defaultExtension) throws IOException
232    {
233        return copyToTempFile(new File(template), defaultExtension);
234    }
235
236    /**
237     * Changes the permissions of the given file to the given POSIX permissions.
238     * 
239     * @param file the file for which the permissions should be changed
240     * @param permissions POSIX permissions to be set
241     * @throws IOException if an I/O error occurs
242     */
243    public static void chmod(File file, String permissions) throws IOException
244    {
245        chmod(file.getAbsolutePath(), permissions);
246    }
247
248    /**
249     * Changes the permissions of the given file to the given POSIX permissions. This method will be
250     * raised an exception, if the OS is not UNIX.
251     * 
252     * @param path the absolute path of the file for which the permissions should be changed
253     * @param permissions POSIX permissions to be set
254     * @throws IOException if an I/O error occurs
255     */
256    public static void chmod(String path, String permissions) throws IOException
257    {
258        // Perform UNIX
259        if (OsVersion.IS_UNIX)
260        {
261            String[] params = { "chmod", permissions, path};
262            String[] output = new String[2];
263            FileExecutor fe = new FileExecutor();
264            fe.executeCommand(params, output);
265        }
266        else
267        {
268            throw new IOException("Sorry, chmod not supported yet on " + OsVersion.OS_NAME + ".");
269        }
270    }
271
272    /**
273     * Returns the free (disk) space for the given path. If it is not ascertainable -1 returns.
274     * 
275     * @param path path for which the free space should be detected
276     * @return the free space for the given path
277     */
278    public static long getFreeSpace(String path)
279    {
280        long retval = -1;
281        if (OsVersion.IS_WINDOWS)
282        {
283            String command = "cmd.exe";
284            if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1) return (-1);
285            String[] params = { command, "/C", "\"dir /D /-C \"" + path + "\"\""};
286            String[] output = new String[2];
287            FileExecutor fe = new FileExecutor();
288            fe.executeCommand(params, output);
289            retval = extractLong(output[0], -3, 3, "%");
290        }
291        else if (OsVersion.IS_SUNOS)
292        {
293            String[] params = { "df", "-k", path};
294            String[] output = new String[2];
295            FileExecutor fe = new FileExecutor();
296            fe.executeCommand(params, output);
297            retval = extractLong(output[0], -3, 3, "%") * 1024;
298        }
299        else if (OsVersion.IS_UNIX)
300        {
301            String[] params = { "df", "-Pk", path};
302            String[] output = new String[2];
303            FileExecutor fe = new FileExecutor();
304            fe.executeCommand(params, output);
305            retval = extractLong(output[0], -3, 3, "%") * 1024;
306        }
307        return retval;
308    }
309
310    /**
311     * Returns whether the given method will be supported with the given environment. Some methods
312     * of this class are not supported on all operation systems.
313     * 
314     * @param method name of the method
315     * @return true if the method will be supported with the current enivronment else false
316     * @throws RuntimeException if the given method name does not exist
317     */
318    public static boolean supported(String method)
319    {
320        if (method.equals("getFreeSpace"))
321        {
322            if (OsVersion.IS_UNIX) return true;
323            if (OsVersion.IS_WINDOWS)
324            { // getFreeSpace do not work on Windows 98.
325                if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1)
326                    return (false);
327                return (true);
328            }
329        }
330        else if (method.equals("chmod"))
331        {
332            if (OsVersion.IS_UNIX) return true;
333        }
334        else if (method.equals("copyFile"))
335        {
336            return true;
337        }
338        else if (method.equals("getPrimaryGroup"))
339        {
340            if (OsVersion.IS_UNIX) return true;
341        }
342        else if (method.equals("getenv"))
343        {
344            return true;
345        }
346        else
347        {
348            throw new RuntimeException("method name " + method + "not supported by this method");
349        }
350        return false;
351
352    }
353
354    /**
355     * Returns the first existing parent directory in a path
356     * 
357     * @param path path which should be scanned
358     * @return the first existing parent directory in a path
359     */
360    public static File existingParent(File path)
361    {
362        File result = path;
363        while (!result.exists())
364        {
365            if (result.getParent() == null) return result;
366            result = result.getParentFile();
367        }
368        return result;
369    }
370
371    /**
372     * Extracts a long value from a string in a special manner. The string will be broken into
373     * tokens with a standard StringTokenizer. Arround the assumed place (with the given half range)
374     * the tokens are scaned reverse for a token which represents a long. if useNotIdentifier is not
375     * null, tokens which are contains this string will be ignored. The first founded long returns.
376     * 
377     * @param in the string which should be parsed
378     * @param assumedPlace token number which should contain the value
379     * @param halfRange half range for detection range
380     * @param useNotIdentifier string which determines tokens which should be ignored
381     * @return founded long
382     */
383    private static long extractLong(String in, int assumedPlace, int halfRange,
384            String useNotIdentifier)
385    {
386        long retval = -1;
387        StringTokenizer st = new StringTokenizer(in);
388        int length = st.countTokens();
389        int i;
390        int currentRange = 0;
391        String[] interestedEntries = new String[halfRange + halfRange];
392        for (i = 0; i < length - halfRange + assumedPlace; ++i)
393            st.nextToken(); // Forget this entries.
394
395        for (i = 0; i < halfRange + halfRange; ++i)
396        { // Put the interesting Strings into an intermediaer array.
397            if (st.hasMoreTokens())
398            {
399                interestedEntries[i] = st.nextToken();
400                currentRange++;
401            }
402        }
403
404        for (i = currentRange - 1; i >= 0; --i)
405        {
406            if (useNotIdentifier != null && interestedEntries[i].indexOf(useNotIdentifier) > -1)
407                continue;
408            try
409            {
410                retval = Long.parseLong(interestedEntries[i]);
411            }
412            catch (NumberFormatException nfe)
413            {
414                continue;
415            }
416            break;
417        }
418        return retval;
419    }
420
421    /**
422     * Returns the primary group of the current user. This feature will be supported only on Unix.
423     * On other systems null returns.
424     * 
425     * @return the primary group of the current user
426     */
427    public static String getPrimaryGroup()
428    {
429        if (supported("getPrimaryGroup"))
430        {
431            if (OsVersion.IS_SUNOS)
432            { // Standard id of SOLARIS do not support -gn.
433                String[] params = { "id"};
434                String[] output = new String[2];
435                FileExecutor fe = new FileExecutor();
436                fe.executeCommand(params, output);
437                // No we have "uid=%u(%s) gid=%u(%s)"
438                if (output[0] != null)
439                {
440                    StringTokenizer st = new StringTokenizer(output[0], "()");
441                    int length = st.countTokens();
442                    if (length >= 4)
443                    {
444                        for (int i = 0; i < 3; ++i)
445                            st.nextToken();
446                        return (st.nextToken());
447                    }
448                }
449                return (null);
450            }
451            else
452            {
453                String[] params = { "id", "-gn"};
454                String[] output = new String[2];
455                FileExecutor fe = new FileExecutor();
456                fe.executeCommand(params, output);
457                return output[0];
458            }
459        }
460        else
461            return null;
462    }
463
464    /**
465     * Returns a string resulting from replacing all occurrences of what in this string with with.
466     * In opposite to the String.replaceAll method this method do not use regular expression or
467     * other methods which are only available in JRE 1.4 and later. This method was special made to
468     * mask masked slashes to avert a conversion during path translation.
469     * 
470     * @param destination string for which the replacing should be performed
471     * @param what what string should be replaced
472     * @param with with what string what should be replaced
473     * @return a new String object if what was found in the given string, else the given string self
474     */
475    public static String replaceString(String destination, String what, String with)
476    {
477        if (destination.indexOf(what) >= 0)
478        { // what found, with (placeholder) not included in destination ->
479            // perform changing.
480            StringBuffer buf = new StringBuffer();
481            int last = 0;
482            int current = destination.indexOf(what);
483            int whatLength = what.length();
484            while (current >= 0)
485            { // Do not use Methods from JRE 1.4 and higher ...
486                if (current > 0) buf.append(destination.substring(last, current));
487                buf.append(with);
488                last = current + whatLength;
489                current = destination.indexOf(what, last);
490            }
491            if (destination.length() > last) buf.append(destination.substring(last));
492            return buf.toString();
493        }
494        return destination;
495    }
496
497    /**
498     * Translates a relative path to a local system path.
499     * 
500     * @param destination The path to translate.
501     * @return The translated path.
502     */
503    public static String translatePath(String destination, VariableSubstitutor vs)
504    {
505        // Parse for variables
506        destination = vs.substitute(destination, null);
507
508        // Convert the file separator characters
509
510        // destination = destination.replace('/', File.separatorChar);
511        // Undo the conversion if the slashes was masked with
512        // a backslash
513
514        // Not all occurencies of slashes are path separators. To differ
515        // between it we allow to mask a slash with a backslash infront.
516        // Unfortunately we cannot use String.replaceAll because it
517        // handles backslashes in the replacement string in a special way
518        // and the method exist only beginning with JRE 1.4.
519        // Therefore the little bit crude way following ...
520        if (destination.indexOf("\\/") >= 0 && destination.indexOf(MASKED_SLASH_PLACEHOLDER) < 0)
521        { // Masked slash found, placeholder not included in destination ->
522            // perform masking.
523            destination = replaceString(destination, "\\/", MASKED_SLASH_PLACEHOLDER);
524            // Masked slashes changed to MASKED_SLASH_PLACEHOLDER.
525            // Replace unmasked slashes.
526            destination = destination.replace('/', File.separatorChar);
527            // Replace the MASKED_SLASH_PLACEHOLDER to slashes; masking
528            // backslashes will
529            // be removed.
530            destination = replaceString(destination, MASKED_SLASH_PLACEHOLDER, "/");
531        }
532        else
533            destination = destination.replace('/', File.separatorChar);
534        return destination;
535    }
536
537    /**
538     * Returns the value of the environment variable given by key. This method is a work around for
539     * VM versions which do not support getenv in an other way. At the first call all environment
540     * variables will be loaded via an exec. On Windows keys are not case sensitive.
541     * 
542     * @param key variable name for which the value should be resolved
543     * @return the value of the environment variable given by key
544     */
545    public static String getenv(String key)
546    {
547        if (envVars == null) loadEnv();
548        if (envVars == null) return (null);
549        if (OsVersion.IS_WINDOWS) key = key.toUpperCase();
550        return (String) (envVars.get(key));
551    }
552
553    /**
554     * Loads all environment variables via an exec.
555     */
556    private static void loadEnv()
557    {
558        String[] output = new String[2];
559        String[] params;
560        if (OsVersion.IS_WINDOWS)
561        {
562            String command = "cmd.exe";
563            if (System.getProperty("os.name").toLowerCase().indexOf("windows 9") > -1)
564                command = "command.com";
565            String[] paramst = { command, "/C", "set"};
566            params = paramst;
567        }
568        else
569        {
570            String[] paramst = { "env"};
571            params = paramst;
572        }
573        FileExecutor fe = new FileExecutor();
574        fe.executeCommand(params, output);
575        if (output[0].length() <= 0) return;
576        String lineSep = System.getProperty("line.separator");
577        StringTokenizer st = new StringTokenizer(output[0], lineSep);
578        envVars = new Properties();
579        String var = null;
580        while (st.hasMoreTokens())
581        {
582            String line = st.nextToken();
583            if (line.indexOf('=') == -1)
584            { // May be a env var with a new line in it.
585                if (var == null)
586                {
587                    var = lineSep + line;
588                }
589                else
590                {
591                    var += lineSep + line;
592                }
593            }
594            else
595            { // New var, perform the previous one.
596                setEnvVar(var);
597                var = line;
598            }
599        }
600        setEnvVar(var);
601    }
602
603    /**
604     * Extracts key and value from the given string var. The key should be separated from the value
605     * by a sign. On Windows all chars of the key are translated to upper case.
606     * 
607     * @param var
608     */
609    private static void setEnvVar(String var)
610    {
611        if (var == null) return;
612        int index = var.indexOf('=');
613        if (index < 0) return;
614        String key = var.substring(0, index);
615        // On windows change all key chars to upper.
616        if (OsVersion.IS_WINDOWS) key = key.toUpperCase();
617        envVars.setProperty(key, var.substring(index + 1));
618
619    }
620}