/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.updater;

import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import javax.swing.SwingUtilities;

/** Class used by autoupdate module for the work with module files and
 * for installing / uninstalling modules
 *
 * @author  Petr Hrebejk, Ales Kemr, Jiri Rechtacek
 * @version
 */
public final class ModuleUpdater extends Thread {
    /** Relative name of update directory */
    private static final String DOWNLOAD_DIR_NAME = "download"; // NOI18N

    /** Relative name of directory where the .NBM files are downloaded */
    static final String DOWNLOAD_DIR = UpdaterDispatcher.UPDATE_DIR + UpdateTracking.FILE_SEPARATOR + DOWNLOAD_DIR_NAME; // NOI18N

    /** Relative name of backup directory */
    private static final String BACKUP_DIR = UpdaterDispatcher.UPDATE_DIR + UpdateTracking.FILE_SEPARATOR + "backup"; // NOI18N

    /** The name of zip entry containing netbeans files */
    public static final String UPDATE_NETBEANS_DIR = "netbeans"; // NOI18N

    /** The name of zip entry containing java_extension files */
    public static final String UPDATE_JAVA_EXT_DIR = "java_ext"; // NOI18N

    /** The name of zip entry containing files for external installer */
    public static final String UPDATE_MAIN_DIR = "main"; // NOI18N
        
    /** Name of external installer parameters file*/
    private static final String JVM_PARAMS_FILE = "main.properties"; // NOI18N

    /** Extension of the distribution files */
    public static final String NBM_EXTENSION = "nbm"; // NOI18N

    /** Extension of the OSGi distribution files */
    public static final String JAR_EXTENSION = "jar"; // NOI18N

    /** The name of the log file */
    public static final String LOG_FILE_NAME = "update.log"; // NOI18N

    /** The name of the install_later file */
    public static final String LATER_FILE_NAME = "install_later.xml"; // NOI18N
    
    public static final char SPACE = ' ';
    public static final char QUOTE = '\"';
    
    private static final String TEMP_FILE_NAME = "temporary";
    
    public static final String UPDATER_JAR = "updater.jar"; // NOI18N
    public static final String AUTOUPDATE_UPDATER_JAR_PATH = "netbeans/modules/ext/" + UPDATER_JAR; // NOI18N
    public static final String AUTOUPDATE_UPDATER_JAR_LOCALE_PATTERN = "netbeans/modules/ext/locale/updater(_[a-zA-Z0-9]+)+"; // NOI18N

    public static final String EXECUTABLE_FILES_ENTRY = "Info/executables.list";
    
    /** files that are supposed to be installed (when running inside the ide) */
    private Map<File, Collection<File>> files2clustersForInstall;

    /** Should the thread stop */
    private volatile boolean stop = false;

    /** Total length of unpacked files */
    private long totalLength;
    
    private final UpdatingContext context;

    ModuleUpdater(UpdatingContext context) {
        super("Module Updater");
        this.context = context;
    }
    
    

    /** Creates new ModuleUpdater */
    @Override
    public void run() {
        assert ! SwingUtilities.isEventDispatchThread () : "Cannot run in EQ";
        try {

            checkStop();

            if (getClustersForInstall ().isEmpty ()) {
                endRun();
            }

            checkStop();

            totalLength();

            checkStop();

            unpack();
            
            for (File cluster: UpdateTracking.clusters (true)) {
                deleteAdditionalInfo (cluster);
            }

        } catch (Exception x) {
            XMLUtil.LOG.log(Level.SEVERE, "Error while upgrading", x);
        } finally {
            context.runningFinished();
        }
    }
    
    private void deleteInstallLater (File cluster) {
        File later = new File (cluster, UpdateTracking.FILE_SEPARATOR + DOWNLOAD_DIR + UpdateTracking.FILE_SEPARATOR + LATER_FILE_NAME);
        if ( later.exists() ) {
            later.delete();
            XMLUtil.LOG.info("File " + later + " deleted.");
        }
        File f = later.getParentFile ();
        while (f != null && f.delete ()) { // remove empty dirs too
            f = f.getParentFile ();
        }
    }

    private void deleteAdditionalInfo (File cluster) {
        File additional = new File (cluster, UpdateTracking.FILE_SEPARATOR + DOWNLOAD_DIR + UpdateTracking.FILE_SEPARATOR + UpdateTracking.ADDITIONAL_INFO_FILE_NAME);
        if (additional != null && additional.exists ()) {
            additional.delete ();
            XMLUtil.LOG.info("File " + additional + " deleted.");
        }
        File f = additional == null ? null : additional.getParentFile ();
        while (f != null && f.delete ()) { // remove empty dirs too
            f = f.getParentFile ();
        }
    }

    /** ends the run of update */
    void endRun() {
        stop = true;
    }

    /** checks wheter ends the run of update */
    private void checkStop() {
        if ( stop ) {
            if (context.isFromIDE ()) {
                context.unpackingFinished();
            } else {
                System.exit( 0 );
            }
        }
    }

    private void processFilesForInstall () {
        // if installOnly are null then generate all NBMs around all clusters
        if (context.forInstall() == null) {
            files2clustersForInstall = new HashMap<File, Collection<File>> ();
            for (File cluster : UpdateTracking.clusters (true)) {
                Collection<File> tmp = getModulesToInstall (cluster);
                files2clustersForInstall.put (cluster, tmp);
                // if ModuleUpdater runs 'offline' then delete install_later files
                if (!context.isFromIDE()) {
                    deleteInstallLater (cluster);
                }
            }
        } else {
            files2clustersForInstall = new HashMap<File, Collection<File>> ();
            for (File nbm : context.forInstall()) {
                if (nbm.exists()) {
                    File cluster = getCluster (nbm);
                    if (files2clustersForInstall.get (cluster) == null) {
                        files2clustersForInstall.put (cluster, new HashSet<File> ());
                    }
                    files2clustersForInstall.get (cluster).add (nbm);
                }
            }
        }
    }
    
    private static File getCluster (File nbm) {
        File cluster = null;
        try {
            // nbms are in <cluster>/update/download dir
            // but try to check it
            assert nbm.exists () : nbm + " for install exists.";
            assert nbm.getParentFile () != null : nbm + " has parent.";
            assert DOWNLOAD_DIR_NAME.equalsIgnoreCase (nbm.getParentFile ().getName ()) : nbm + " is in directory " + DOWNLOAD_DIR_NAME;
            assert nbm.getParentFile ().getParentFile () != null : nbm.getParentFile () + " has parent.";
            assert UpdaterDispatcher.UPDATE_DIR.equalsIgnoreCase (nbm.getParentFile ().getParentFile ().getName ()) :
                nbm + " is in directory " + UpdaterDispatcher.UPDATE_DIR;
            assert nbm.getParentFile ().getParentFile ().getParentFile () != null : nbm.getParentFile ().getParentFile () + " has parent.";
            
            cluster = nbm.getParentFile ().getParentFile ().getParentFile ();
        } catch (NullPointerException npe) {
            XMLUtil.LOG.log(Level.SEVERE, "getCluster (" + nbm + ") throws an exception", npe);
        }
        return cluster;
    }
    
    private Collection<File> getFilesForInstallInCluster (File cluster) {
        if (files2clustersForInstall == null) {
            processFilesForInstall ();
        }
        return files2clustersForInstall == null ? null : files2clustersForInstall.get(cluster);
    }

    private Collection<File> getClustersForInstall () {
        if (files2clustersForInstall == null) {
            processFilesForInstall ();
        }
        return files2clustersForInstall == null ? null : files2clustersForInstall.keySet();
    }

    /** Determines size of unpacked modules */
    private void totalLength () {
        totalLength = 0L;

        context.setLabel (Localization.getBrandedString ("CTL_PreparingUnpack"));
        Collection<File> allFiles = new HashSet<File> ();
        for (File c : getClustersForInstall ()) {
            allFiles.addAll (getFilesForInstallInCluster (c));
        }
        context.setProgressRange (0, allFiles.size ());

        int i = 0;
        for (File f : allFiles) {

            JarFile jarFile = null;

            try {
                if(f.getName().endsWith(".jar")) {
                    //OSGi bundle
                    totalLength += f.length();
                } else {
                jarFile = new JarFile (f);
                Enumeration<JarEntry> entries = jarFile.entries ();
                while (entries.hasMoreElements ()) {
                    JarEntry entry = entries.nextElement ();

                    checkStop ();

                    if ((entry.getName ().startsWith (UPDATE_NETBEANS_DIR) || entry.getName ().startsWith (ModuleUpdater.UPDATE_JAVA_EXT_DIR) || entry.getName ().startsWith (UPDATE_MAIN_DIR)) && ! entry.isDirectory ()) {
                        totalLength += entry.getSize ();
                    }
                }
                }
                context.setProgressValue (i ++);
            } catch (java.io.IOException e) {
                XMLUtil.LOG.log(Level.WARNING, "Cannot count size of entries in " + f, e);
            } finally {
                try {
                    if (jarFile != null) {
                        jarFile.close ();
                    }
                } catch (java.io.IOException e) {
                    // We can't close the file do nothing
                    XMLUtil.LOG.log(Level.WARNING, "While closing " + jarFile + " input stream", e); // NOI18N
                }
            }
        }
    }



    /** Unpack the distribution files into update directory */

    private void unpack ()  {
        long bytesRead = 0L;
        boolean hasMainClass;

        context.setLabel( "" ); // NOI18N
        context.setProgressRange( 0, totalLength );
        
        ArrayList<UpdateTracking> allTrackings = new ArrayList<UpdateTracking> ();
        Map<ModuleUpdate, UpdateTracking.Version> l10ns = 
                new HashMap<ModuleUpdate, UpdateTracking.Version>();
        
        for (File cluster : getClustersForInstall ()) {
            UpdateTracking tracking = UpdateTracking.getTracking (cluster, true, context);
            if (tracking == null) {
                throw new RuntimeException ("No update_tracking file in cluster " + cluster);
            }
            allTrackings.add (tracking);

            int installedNBMs = 0;
            for (File nbm : getFilesForInstallInCluster (cluster)) {
                installedNBMs++;
                
                UpdateTracking.Version version;
                UpdateTracking.Module modtrack;
                
                context.setLabel( Localization.getBrandedString("CTL_UnpackingFile") + "  " + nbm.getName() ); //NOI18N
                XMLUtil.LOG.info("780: " + Localization.getBrandedString("CTL_UnpackingFile") + " " + nbm.getName()); //NOI18N
                context.unpackingIsRunning ();
                
                ModuleUpdate mu;
                try {
                    mu = new ModuleUpdate (nbm);
                } catch (RuntimeException re) {
                    if (nbm.exists ()) {
                        XMLUtil.LOG.info("Deleteing file: " + nbm);
                        if (! nbm.delete ()) {
                            XMLUtil.LOG.log(Level.WARNING, "File " + nbm + " cannot be deleted. Propably file lock on the file."); // NOI18N
                            assert false : "Error: File " + nbm + " cannot be deleted. Propably file lock on the file.";
                            nbm.deleteOnExit ();
                        } else {
                            XMLUtil.LOG.info("File " + nbm + " deleted.");
                        }
                    }
                    continue;
                }
                assert mu != null : "Module update is not null for file: " + nbm; // NOI18N
                if ( mu.isL10n() ) {
                    modtrack = null;
                    version = tracking.createVersion( "0" ); // NOI18N
                    l10ns.put( mu, version );
                } else {
                    modtrack = tracking.readModuleTracking (mu.getCodenamebase (), true);
                    // find origin for file
                    UpdateTracking.AdditionalInfo info = UpdateTracking.getAdditionalInformation (cluster, context);
                    String origin = info != null && info.getSource (nbm.getName ()) != null ?
                        info.getSource (nbm.getName ()) : UpdateTracking.UPDATER_ORIGIN;
                    version = modtrack.addNewVersion (mu.getSpecification_version (), origin);
                }
                // input streams should be released, but following is needed
                //System.gc();

                hasMainClass = false;
                context.setProgressValue( bytesRead );
                JarFile jarFile = null;

                try {
                    jarFile = new JarFile (nbm);
                    Enumeration<JarEntry> entries = jarFile.entries();
                    final Manifest manifest = jarFile.getManifest();
                    String symbolicName = manifest != null ? ModuleUpdate.extractCodeName(manifest.getMainAttributes()) : null;
                    if (symbolicName != null) {
                        //OSGi bundle
                        File osgiJar = nbm;
                        
                        File destFile = new File(cluster, "modules/" + osgiJar.getName());
                        if (destFile.exists()) {
                            File bckFile = new File(getBackupDirectory(cluster), osgiJar.getName());
                            bckFile.getParentFile().mkdirs();
                            copyStreams(new FileInputStream(destFile), context.createOS(bckFile), -1);
                            XMLUtil.LOG.info("Backup file " + destFile + " to " + bckFile);
                            if (!destFile.delete() && isWindows()) {
                                trickyDeleteOnWindows(destFile);
                            } else {
                                XMLUtil.LOG.info("File " + destFile + " deleted.");
                            }
                        } else {
                            destFile.getParentFile().mkdirs();
                        }

                        bytesRead = copyStreams(new FileInputStream(osgiJar), context.createOS(destFile), bytesRead);
                        XMLUtil.LOG.info("Copied file " + osgiJar + " to " + destFile);
                        long crc = UpdateTracking.getFileCRC(destFile);
                        version.addFileWithCrc("modules/" + osgiJar.getName(), Long.toString(crc));
                        //create config/Modules/cnb.xml
                        File configDir = new File (new File (cluster, ModuleDeactivator.CONFIG), ModuleDeactivator.MODULES); // NOI18N
                        String configFileName = symbolicName.replace ('.', '-') + ".xml";
                        File configFile = new File(configDir, configFileName);
                        if (configFile.exists()) {
                            long configFileCRC = UpdateTracking.getFileCRC(configFile);
                            version.addFileWithCrc("config/Modules/" + configFileName, Long.toString(configFileCRC));
                        }

                        context.setProgressValue(bytesRead);
                        modtrack.setOSGi(true);
                    } else {
                        //NBM
                        List <String> executableFiles = readExecutableFilesList(jarFile);
                        List <File> filesToChmod = new ArrayList <File> ();
                        while( entries.hasMoreElements() ) {
                            JarEntry entry = entries.nextElement();
                            checkStop();
                            if ( entry.getName().startsWith( UPDATE_NETBEANS_DIR ) ) {
                                if (! entry.isDirectory ()) {
                                    String pathTo = entry.getName().substring(UPDATE_NETBEANS_DIR.length() + 1);
                                    File destFile = new File (cluster, pathTo);
                                    if (AUTOUPDATE_UPDATER_JAR_PATH.equals (entry.getName ()) ||
                                            entry.toString().matches(AUTOUPDATE_UPDATER_JAR_LOCALE_PATTERN)) {
                                        
                                        // #220807 - NoClassDefFoundError: updater/XMLUtil
                                        version.addFileWithCrc(pathTo, Long.toString(destFile.exists() ? UpdateTracking.getFileCRC(destFile) : 0));
                                        
                                        if (destFile.exists()) {
                                            // skip updater.jar
                                            continue;
                                        }
                                    }
                                    // path without netbeans prefix
                                    if ( destFile.exists() ) {
                                        File bckFile = new File( getBackupDirectory (cluster), entry.getName() );
                                        bckFile.getParentFile ().mkdirs ();
                                        copyStreams( new FileInputStream( destFile ), context.createOS( bckFile ), -1 );
                                        XMLUtil.LOG.info("Backup file " + destFile + " to " + bckFile);
                                        if (!destFile.delete() && isWindows()) {
                                            trickyDeleteOnWindows(destFile);
                                        } else {
                                            XMLUtil.LOG.info("File " + destFile + " deleted.");
                                        }
                                    } else {
                                        destFile.getParentFile ().mkdirs ();
                                    }

                                    long crc;
                                    if (pathTo.endsWith(".external")) {
                                        File downloaded = new File(destFile.getParentFile(), destFile.getName().substring(0, destFile.getName().lastIndexOf(".external")));
                                        final InputStream spec = jarFile.getInputStream(entry);
                                        pathTo = pathTo.substring(0, pathTo.length() - ".external".length());
                                        long expectedCRC = externalDownload(spec, nbm);
                                        File external = new File(nbm + "." + Long.toHexString(expectedCRC));
                                        InputStream is = new FileInputStream(external);
                                        try {
                                            spec.close();
                                            OutputStream os = context.createOS(downloaded);
                                            try {
                                                XMLUtil.LOG.info("810: " + Localization.getBrandedString("CTL_DownloadingFile") + " " + downloaded); //NOI18N
                                                bytesRead = copyStreams(is, os, -1);
                                                XMLUtil.LOG.info("Copied external file " + external + " to " + downloaded);
                                            } finally {
                                                os.close();
                                            }
                                        } finally {
                                            external.delete();
                                            XMLUtil.LOG.info("File " + external + " deleted.");
                                            is.close();
                                        }
                                        crc = UpdateTracking.getFileCRC(downloaded);
                                        if (crc != expectedCRC) {
                                            downloaded.delete();
                                            XMLUtil.LOG.info("File " + downloaded + " deleted.");
                                            throw new IOException("Wrong CRC for " + downloaded);
                                        }
                                    } else {
                                        bytesRead = copyStreams( jarFile.getInputStream( entry ), context.createOS( destFile ), bytesRead );
                                        XMLUtil.LOG.info("Copied file " + jarFile.getName() + ":" + entry + " to " + destFile);
                                        crc = entry.getCrc();
                                    }
                                    if(executableFiles.contains(pathTo)) {
                                        filesToChmod.add(destFile);
                                    }
                                    if(pathTo.endsWith(".jar.pack.gz") &&
                                            jarFile.getEntry(entry.getName().substring(0, entry.getName().lastIndexOf(".pack.gz")))==null) {
                                         //check if file.jar.pack.gz does not exit for file.jar - then unpack current .pack.gz file
                                        File unpacked = new File(destFile.getParentFile(), destFile.getName().substring(0, destFile.getName().lastIndexOf(".pack.gz")));
                                        unpack200(destFile, unpacked);
                                        destFile.delete();
                                        XMLUtil.LOG.info("File " + destFile + " deleted.");
                                        pathTo = pathTo.substring(0, pathTo.length() - ".pack.gz".length());
                                        crc = UpdateTracking.getFileCRC(unpacked);
                                    }
                                    if ( mu.isL10n() ) {
                                        version.addL10NFileWithCrc( pathTo, Long.toString(crc), mu.getSpecification_version());
                                    } else {
                                        version.addFileWithCrc( pathTo, Long.toString(crc));
                                    }

                                    context.setProgressValue( bytesRead );
                                }
                            } else if ( entry.getName().startsWith( UPDATE_MAIN_DIR )&&
                                      !entry.isDirectory() ) {
                                // run main
                                String pathTo = entry.getName().substring(UPDATE_MAIN_DIR.length() + 1);
                                File destFile = new File (getMainDirectory (cluster), pathTo);
                                if(executableFiles.contains(pathTo)) {
                                    filesToChmod.add(destFile);
                                }
                                destFile.getParentFile ().mkdirs ();
                                hasMainClass = true;
                                bytesRead = copyStreams( jarFile.getInputStream( entry ), context.createOS( destFile ), bytesRead );
                                XMLUtil.LOG.info("Copied file " + jarFile.getName() + ":" + entry + " to " + destFile);
                                context.setProgressValue( bytesRead );
                            }
                        }
                        chmod(filesToChmod);
                        if ( hasMainClass ) {                    
                            MainConfig mconfig = new MainConfig (getMainDirString (cluster) + UpdateTracking.FILE_SEPARATOR + JVM_PARAMS_FILE, cluster);
                            if (mconfig.isValid()) {
                                String java_path = System.getProperty ("java.home") + UpdateTracking.FILE_SEPARATOR
                                    + "bin"  + UpdateTracking.FILE_SEPARATOR + "java";                              // NOI18N
                                java_path = quoteString( java_path );
                                String torun = java_path + " -cp " + quoteString (getMainDirString (cluster) + mconfig.getClasspath() ) + mconfig.getCommand();  // NOI18N
                                startCommand(torun);

                                deleteDir( getMainDirectory (cluster) );
                            }
                        }
                    }
                }
                catch ( java.io.IOException e ) {
                    // Ignore non readable files
                    XMLUtil.LOG.log(Level.INFO, "Ignore non-readable files ", e);
                }
                finally {
                    try {
                        if ( jarFile != null ) {
                            jarFile.close();
                        }
                    }
                    catch ( java.io.IOException e ) {
                        // We can't close the file do nothing
                        // XMLUtil.LOG.info("Can't close : " + e ); // NOI18N
                    }
                    //XMLUtil.LOG.info("Dleting :" + nbmFiles[i].getName() + ":" + nbmFiles[i].delete() ); // NOI18N

                    if (! nbm.delete ()) {
                        XMLUtil.LOG.log(Level.WARNING, "Error: Cannot delete {0}", nbm); // NOI18N
                        nbm.deleteOnExit ();
                    } else {
                        XMLUtil.LOG.info("File " + nbm + " deleted.");
                    }
                }
                if (! mu.isL10n ()) {
                    modtrack.write ();
                    modtrack.writeConfigModuleXMLIfMissing ();
                }
            }
            
            if (installedNBMs > 0) {
                UpdaterDispatcher.touchLastModified (cluster);
            }
        }
        
        for (UpdateTracking t: allTrackings) {
            // update_tracking of l10n's
            for (Map.Entry<ModuleUpdate, UpdateTracking.Version> entry: l10ns.entrySet()) {
                ModuleUpdate mod = entry.getKey();
                UpdateTracking.Version version = entry.getValue();
                UpdateTracking.Module modtrack = t.readModuleTracking( 
                    mod.getCodenamebase(), 
                    true 
                );
                modtrack.addL10NVersion( version );
                modtrack.write();
            }
            t.deleteUnusedFiles ();            
        }
    }
    
    private boolean unpack200(File src, File dest) {
        String unpack200Executable = new File(System.getProperty("java.home"),
                "bin/unpack200" + (isWindows() ? ".exe" : "")).getAbsolutePath();
        ProcessBuilder pb = new ProcessBuilder(unpack200Executable, src.getAbsolutePath(), dest.getAbsolutePath());
        pb.directory(src.getParentFile());
        int result = 1;
        try {
            //maybe reuse start() method here?
            Process process = pb.start();
            //TODO: Need to think of unpack200/lvprcsrv.exe issues
            //https://netbeans.org/bugzilla/show_bug.cgi?id=117334
            //https://netbeans.org/bugzilla/show_bug.cgi?id=119861
            result = process.waitFor();
            process.destroy();
            XMLUtil.LOG.info("Unpack " + src + " to " + dest);
        } catch (IOException e) {
            XMLUtil.LOG.log(Level.WARNING, null, e);
        } catch (InterruptedException e) {
            XMLUtil.LOG.log(Level.WARNING, null, e);
        }
        return result == 0;
    }

    private List<String> readExecutableFilesList(JarFile jarFile) {
        List<String> list = new ArrayList<String>();
        JarEntry executableFilesEntry = jarFile.getJarEntry(EXECUTABLE_FILES_ENTRY);
        if (executableFilesEntry != null) {
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(executableFilesEntry), "UTF-8"));
                String s;
                while ((s = reader.readLine()) != null) {
                    list.add(s);
                }
                reader.close();
            } catch (Exception e) {
                XMLUtil.LOG.log(Level.WARNING, null, e);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                    }
                }
            }
        }
        return list;
    }

    private void chmod(List<File> executableFiles) {
        if (isWindows() || executableFiles.isEmpty()) {
            return;
        }

        for (File executableFile : executableFiles) {
            executableFile.setExecutable(true, false);
         }
    }
        
    public static boolean trickyDeleteOnWindows(File destFile) {
        assert isWindows() : "Call it only on Windows but system is " + System.getProperty("os.name");
        File f = new File(destFile.getParentFile(), destFile.getName());
        assert f.exists() : "The file " + f + " must exists.";        
        try {
            File tmpFile = File.createTempFile(TEMP_FILE_NAME, null, f.getParentFile());
            if (tmpFile.delete()) {
                f.renameTo(tmpFile);
                XMLUtil.LOG.info("File " + f + " renamed to " + tmpFile);
                tmpFile.deleteOnExit ();
                XMLUtil.LOG.info("Locked file " + tmpFile + " will be deleted on exit.");
            } else {
                XMLUtil.LOG.info("File " + tmpFile + " was deleted.");
            }
        } catch (IOException ex) {
            //no special handling needed
        }
        return !f.exists();
    }
    
    public static boolean isWindows() {
        String os = System.getProperty("os.name"); // NOI18N
        return (os != null && os.toLowerCase().startsWith("windows"));//NOI18N
    }
    
    private void startCommand(String torun) {
        Runtime runtime=Runtime.getRuntime();
        Process proces;            
        try {
            proces=runtime.exec(parseParameters( torun ));
            final Process proc2 = proces;
            new Thread() {
                @Override
                public void run() {
                    try {
                        InputStreamReader stream= new InputStreamReader (proc2.getErrorStream());
                        BufferedReader reader= new BufferedReader(stream);
                        String vystup;
                        do {
                            vystup = reader.readLine();
                            if (vystup!=null) {
                                XMLUtil.LOG.info(vystup);
                            }
                        } while (vystup != null);
                    } catch (Exception e) {
                        XMLUtil.LOG.log(Level.INFO, null, e);
                  }
                }
            }.start();
            int x=proces.waitFor();
        }
        catch (Exception e){
            XMLUtil.LOG.log(Level.INFO, null, e);
        }
    }
    
    /** The directory where to backup old versions of modules */
    public File getBackupDirectory (File activeCluster) {
        // #72960: Backup file created in wrong cluster
        File backupDirectory = new File (activeCluster, BACKUP_DIR);
        if (! backupDirectory.isDirectory ()) {
            backupDirectory.mkdirs();
        }

        return backupDirectory;
    }

    /** Gets the netbeans directory */
    private static File getMainDirectory (File activeCluster) {
        // #72918: Post-install cannot write into platform cluster
        File mainDirectory = new File (activeCluster, UpdateTracking.FILE_SEPARATOR + UpdaterDispatcher.UPDATE_DIR + UpdateTracking.FILE_SEPARATOR + UPDATE_MAIN_DIR);
        if (! mainDirectory.isDirectory ()) {
            mainDirectory.mkdirs();
        }

        return mainDirectory;
    }
    
    private static String getMainDirString (File activeCluster) {
        return getMainDirectory (activeCluster).getPath ();
    }
    
     /** Quotes string correctly, eg. removes all quotes from the string and adds 
      * just one at the start and
      * second one at the end.
      * @param s string to be quoted
      * @return correctly quoted string
      */
     public static String quoteString(String s) {
         if ( s.indexOf( SPACE ) > -1 ) {
             StringBuilder sb = new StringBuilder(s);
             int i = 0;
             while ( i < sb.length() ) {
                 if ( sb.charAt(i) == QUOTE ) {
                     sb.deleteCharAt( i );
                 } else {
                     i++;
                 }
             }
             sb.insert( 0, QUOTE );
             sb.append( QUOTE );
             return sb.toString();
         }
         return s;
     }    

    /**
     * It takes the current progress value so it can update progress
     * properly, and also return the new progress value after the
     * copy is done.
     *
     * @param progressVal The current progress bar value.  If this is
     *          negative, we don't want to update the progress bar.
     */
    private long copyStreams( InputStream src, OutputStream dest,
                               long progressVal ) throws java.io.IOException {

        BufferedInputStream bsrc = new BufferedInputStream( src );
        BufferedOutputStream bdest = new BufferedOutputStream( dest );

        int count = 0;

        int c;
        byte [] bytes = new byte [8192];

        try {
            while( ( c = bsrc.read(bytes) ) != -1 ) {
                bdest.write(bytes, 0, c);
                count+=c;
                if ( count > 8500 ) {
                    if (progressVal >= 0) {
                        progressVal += count;
                        context.setProgressValue( progressVal );
                    }

                    count = 0;
                    checkStop();
                }
            }
            // Just update the value, no need to update the
            // GUI yet.   Caller can do that.
            if (progressVal >= 0) {
                progressVal += count;
            }
        }
        finally {
            bsrc.close();
            bdest.close();
            src.close();
            dest.close();
        }
        return progressVal;

    }

    private void deleteDir(File dir) {
        File[] files=dir.listFiles();
        for( int j = 0; j < files.length; j++ ) {
            if ( files[j].isDirectory() ) {
                deleteDir( files[j] );
            }
            if (! files[j].delete()) {
                    XMLUtil.LOG.log(Level.WARNING, "Cannot delete {0}", files [j]); //NOI18N
                    assert false : "Cannot delete " + files [j];
            } else {
                XMLUtil.LOG.info("File " + files[j] + " deleted.");
            }
        }
    }

    // [Copied from org.openide.util.Utilities]
    /**
     * Parses parameters from a given string in shell-like manner.
     * Users of the Bourne shell (e.g. on Unix) will already be familiar
     * with the behavior.
     * For example, when using {@link org.openide.execution.NbProcessDescriptor}
     * you should be able to:
     * <ul>
     * <li>Include command names with embedded spaces, such as 
     * <code>c:\Program Files\jdk\bin\javac</code>.
     * <li>Include extra command arguments, such as <code>-Dname=value</code>.
     * <li>Do anything else which might require unusual characters or
     *     processing. For example:
     * <p><code><pre>
     * "c:\program files\jdk\bin\java" -Dmessage="Hello /\\/\\ there!" -Xmx128m
     * </pre></code>
     * <p>This example would create the following executable name and arguments: 
     * <ol>
     * <li> <code>c:\program files\jdk\bin\java</code>
     * <li> <code>-Dmessage=Hello /\/\ there!</code>
     * <li> <code>-Xmx128m</code>
     * </ol>
     * Note that the command string does not escape its backslashes--under the assumption
     * that Windows users will not think to do this, meaningless escapes are just left
     * as backslashes plus following character.
     * </ul>
     * <em>Caveat</em>: even after parsing, Windows programs (such as
     * the Java launcher)
     * may not fully honor certain
     * characters, such as quotes, in command names or arguments. This is because programs
     * under Windows frequently perform their own parsing and unescaping (since the shell
     * cannot be relied on to do this). On Unix, this problem should not occur.
     * @param s a string to parse
     * @return an array of parameters
     */
     private static String[] parseParameters(String s) {
         int NULL = 0x0;  // STICK + whitespace or NULL + non_"
         int INPARAM = 0x1; // NULL + " or STICK + " or INPARAMPENDING + "\ // NOI18N
         int INPARAMPENDING = 0x2; // INPARAM + \
         int STICK = 0x4; // INPARAM + " or STICK + non_" // NOI18N
         int STICKPENDING = 0x8; // STICK + \
        @SuppressWarnings("UseOfObsoleteCollectionType")
         Vector<String> params = new Vector<String>(5,5);
         char c;
 
         int state = NULL;
         StringBuilder buff = new StringBuilder(20);
         int slength = s.length();
         for (int i = 0; i < slength; i++) {
             c = s.charAt(i);
             if (Character.isWhitespace(c)) {
                 if (state == NULL) {
                     if (buff.length () > 0) {
                         params.addElement(buff.toString());
                         buff.setLength(0);
                     }
                 } else if (state == STICK) {
                     params.addElement(buff.toString());
                     buff.setLength(0);
                     state = NULL;
                 } else if (state == STICKPENDING) {
                     buff.append('\\');
                     params.addElement(buff.toString());
                     buff.setLength(0);
                     state = NULL;
                 } else if (state == INPARAMPENDING) {
                     state = INPARAM;
                     buff.append('\\');
                     buff.append(c);
                 } else {    // INPARAM
                     buff.append(c);
                 }
                 continue;
             }
 
             if (c == '\\') {
                 if (state == NULL) {
                     ++i;
                     if (i < slength) {
                         char cc = s.charAt(i);
                         if (cc == '"' || cc == '\\') {
                             buff.append(cc);
                         } else if (Character.isWhitespace(cc)) {
                             buff.append(c);
                             --i;
                         } else {
                             buff.append(c);
                             buff.append(cc);
                         }
                     } else {
                         buff.append('\\');
                         break;
                     }
                     continue;
                 } else if (state == INPARAM) {
                     state = INPARAMPENDING;
                 } else if (state == INPARAMPENDING) {
                     buff.append('\\');
                     state = INPARAM;
                 } else if (state == STICK) {
                     state = STICKPENDING;
                 } else if (state == STICKPENDING) {
                     buff.append('\\');
                     state = STICK;
                 }
                 continue;
             }
 
             if (c == '"') {
                 if (state == NULL) {
                     state = INPARAM;
                 } else if (state == INPARAM) {
                     state = STICK;
                 } else if (state == STICK) {
                     state = INPARAM;
                 } else if (state == STICKPENDING) {
                     buff.append('"');
                     state = STICK;
                 } else { // INPARAMPENDING
                     buff.append('"');
                     state = INPARAM;
                 }
                 continue;
             }
 
             if (state == INPARAMPENDING) {
                 buff.append('\\');
                 state = INPARAM;
             } else if (state == STICKPENDING) {
                 buff.append('\\');
                 state = STICK;
             }
             buff.append(c);
         }
         // collect
         if (state == INPARAM) {
             params.addElement(buff.toString());
         } else if ((state & (INPARAMPENDING | STICKPENDING)) != 0) {
             buff.append('\\');
             params.addElement(buff.toString());
         } else { // NULL or STICK
             if (buff.length() != 0) {
                 params.addElement(buff.toString());
             }
         }
         String[] ret = new String[params.size()];
         params.copyInto(ret);
         return ret;
     }

    private long externalDownload(InputStream spec, File nbm) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(spec));
        for (;;) {
            String line = br.readLine();
            if (line == null) {
                throw new IOException("No CRC in the .external file!");
            }
            if (line.startsWith("CRC:")) {
                return Long.parseLong(line.substring(4).trim());
            }
        }
    }


    
    /** read jvm parameters from jvm parameters file */
    static class MainConfig extends Object {
        
        /** The names of properties from jvm parameters file */
        private final String PAR_MAIN = "mainClass";               // NOI18N
        private final String PAR_RELCP = "relativeClassPath";      // NOI18N
        private final String PAR_JVMPAR = "jvm.parameters";        // NOI18N
        private final String PAR_MAINARGS = "mainClass.arguments"; // NOI18N
        
        /** The names of variables allow to use in jvm parameters file */
        private final String VAR_IDE_HOME = "%IDE_HOME%";          // NOI18N
        private final String VAR_IDE_USER = "%IDE_USER%";          // NOI18N
        private final String VAR_FILE_SEPARATOR = "%FS%";          // NOI18N        
        private final String VAR_PATH_SEPARATOR = "%PS%";          // NOI18N        
        private final String VAR_JAVA_HOME = "%JAVA_HOME%";        // NOI18N        
    
        /** joined all parameters of jvm java command */
        private String parameters = ""; // NOI18N
        private String classpath = ""; // NOI18N
        
        /** is jvm parameters file in valid stucture */
        private boolean valid = false;
        private final File activeCluster;
        
        public MainConfig (String spath, File activeCluster) {
            valid = readParms(spath);
            this.activeCluster = activeCluster;
        }
        
        /** returns all parameters needed by jvm java command */
        public String getCommand() {
            return parameters;
        }
        
        /** returns all parameters needed by jvm java command */
        public String getClasspath() {
            return classpath;
        }
        
        /** is jvm parameters file in valid stucture */
        public boolean isValid() {
            return valid;
        }
        
        /** read jvm parameters from jvm parameters file */
        @SuppressWarnings("empty-statement")
        private boolean readParms(String spath) {
            Properties details = new Properties();
            FileInputStream fis = null;
            try {
                details.load(fis = new FileInputStream(spath)); // NOI18N
            } catch (IOException e) {            
                return false;
            } finally {
                if (fis != null) {
                    try { fis.close(); } catch (IOException e) { /* ignore */ }
                };
            }
            
            String mainclass;
            String relpath;
            String jvmparms;
            String mainargs;        
        
            relpath = details.getProperty(PAR_RELCP,null);
            if (relpath != null) {
                relpath = replaceVars( relpath );
                StringTokenizer token = new StringTokenizer( relpath, UpdateTracking.PATH_SEPARATOR, false );
                while ( token.hasMoreTokens() ) {
                    classpath = classpath + UpdateTracking.PATH_SEPARATOR + changeRelative( token.nextToken() );
                }
            }
        
            parameters = "";
            jvmparms = details.getProperty(PAR_JVMPAR,null);
            if (jvmparms != null) {
                parameters = parameters + " " + jvmparms;  // NOI18N
            }
            
            mainclass = details.getProperty(PAR_MAIN,null);
            if (mainclass == null) {
                return false;
            } else {
                parameters = parameters + " " + mainclass;
            }  // NOI18N
            
            mainargs = details.getProperty(PAR_MAINARGS,null);
            if (mainargs != null) {
                parameters = parameters + " " + mainargs;  // NOI18N
            }
            
            parameters = replaceVars( parameters );
            return true;            
        }
        
        private String replaceVars(String original) {
            original = replaceAll(original, VAR_IDE_HOME,
                UpdateTracking.getPlatformDir () == null ? "" : UpdateTracking.getPlatformDir ().getPath());
            original = replaceAll(original, VAR_IDE_USER,
                UpdateTracking.getUserDir () == null ? "" : UpdateTracking.getUserDir ().getPath());
            original = replaceAll(original, VAR_FILE_SEPARATOR,
                UpdateTracking.FILE_SEPARATOR);            
            original = replaceAll(original, VAR_PATH_SEPARATOR,
                UpdateTracking.PATH_SEPARATOR);            
            original = replaceAll(original, VAR_JAVA_HOME,
                System.getProperty ("java.home")); // NOI18N
            return original;
        }
        
        private String changeRelative(String path) {
            if ( new File( path ).isAbsolute() ) {
                return path;
            } else {
                return getMainDirString (this.activeCluster) + UpdateTracking.FILE_SEPARATOR + path;
            }
        }
        
        
        /** replace all occurrences of String what by String repl in the String sin */
        private String replaceAll(String sin, String what, String repl) {
            StringBuilder sb = new StringBuilder(sin);
            int i = sb.toString().indexOf(what);
            int len = what.length();
            while ( i > -1 ) {
                sb.replace(i,i + len,repl);
                i = sb.toString().indexOf(what,i+1);                
            }

            return sb.toString();
        }
    }
    
    /** Compute the list of modules that should be installed into this 
     * cluster.
     * @param File root of cluster
     * @return List<File> of nbm files
     */
    public static Set<File> getModulesToInstall (File cluster) {
        
        class NbmFilter implements java.io.FilenameFilter {
            @Override
            public boolean accept (File dir, String name) {
                return name.endsWith (ModuleUpdater.NBM_EXTENSION) || name.endsWith (ModuleUpdater.JAR_EXTENSION);
            }
        }
        
        File idir = new File (cluster, ModuleUpdater.DOWNLOAD_DIR);
        File[] arr = idir.listFiles (new NbmFilter ());
        
        if (arr == null) {
            return Collections.emptySet ();
        } else {
            return new HashSet<File> (Arrays.asList (arr));
        }
    }

}
