/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2012 Oracle and/or its affiliates. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package org.netbeans.updater;

import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.zip.CRC32;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/** This class represents module updates tracking
 *
 * @author  Ales Kemr
 */
public final class UpdateTracking {
    
    /** Platform dependent file name separator */
    public static final String FILE_SEPARATOR = System.getProperty ("file.separator");
    public static final String PATH_SEPARATOR = System.getProperty ("path.separator");
    
    public static final String ELEMENT_MODULES = "installed_modules"; // NOI18N
    public static final String ELEMENT_MODULE = "module"; // NOI18N
    public static final String ATTR_CODENAMEBASE = "codename"; // NOI18N
    public static final String ELEMENT_VERSION = "module_version"; // NOI18N
    public static final String ATTR_VERSION = "specification_version"; // NOI18N
    public static final String ATTR_LAST = "last"; // NOI18N
    public static final String ATTR_INSTALL = "install_time"; // NOI18N
    public static final String ELEMENT_FILE = "file"; // NOI18N
    public static final String ATTR_FILE_NAME = "name"; // NOI18N
    public static final String ATTR_ORIGIN = "origin"; // NOI18N
    public static final String UPDATER_ORIGIN = "updater"; // NOI18N
    public static final String INSTALLER_ORIGIN = "installer"; // NOI18N
    
    private static final String ATTR_CRC = "crc"; // NOI18N    
    private static final String NBM_ORIGIN = "nbm"; // NOI18N
    
    public static final String ELEMENT_ADDITIONAL = "module_additional"; // NOI18N
    public static final String ELEMENT_ADDITIONAL_MODULE = "module"; // NOI18N
    public static final String ATTR_ADDITIONAL_NBM_NAME = "nbm_name"; // NOI18N
    public static final String ATTR_ADDITIONAL_SOURCE = "source-display-name"; // NOI18N
    
    public static final String EXTRA_CLUSTER_NAME = "extra";
    
    private static final String LOCALE_DIR = FILE_SEPARATOR + "locale" + FILE_SEPARATOR; // NOI18N

    public static final String TRACKING_FILE_NAME = "update_tracking"; // NOI18N
    public static final String ADDITIONAL_INFO_FILE_NAME = "additional_information.xml"; // NOI18N
    private static final String XML_EXT = ".xml"; // NOI18N
    private static final String FORBID_AUTOUPDATE = ".noautoupdate"; // NOI18N

    /** maps root of clusters to tracking files. (File -> UpdateTracking) */
    private static final Map<File, UpdateTracking> trackings = new HashMap<File, UpdateTracking> ();
    private static final Map<File, UpdateTracking.AdditionalInfo> infos = new HashMap<File, UpdateTracking.AdditionalInfo> ();
    
    /** Mapping from files defining modules to appropriate modules objects.
     */
    private LinkedHashMap<File, Module> installedModules = new LinkedHashMap<File, Module> ();

    private final File directory;
    private final File trackingFile;
    private String origin = NBM_ORIGIN;
    private final UpdatingContext context;
    
    /** Private constructor.
     */
    private UpdateTracking( File nbPath, UpdatingContext context ) {
        assert nbPath != null : "Path cannot be null";
        
        trackingFile = new File( nbPath + FILE_SEPARATOR + TRACKING_FILE_NAME);
        directory = nbPath;
        origin = UPDATER_ORIGIN;
        this.context = context;
    }
    
    //
    // Various factory and utility methods
    //
    
    /** Finds update tracking for given cluster root.
     * @path root of a cluster
     * @param createIfDoesNotExists should new tracking be created if it does not exists
     * @return the tracking for that cluster
     */    
    static UpdateTracking getTracking (File path, boolean createIfDoesNotExists, UpdatingContext context) {
        synchronized (trackings) {
            UpdateTracking track = trackings.get (path);
            if (track == null) {
                File utFile = new File (path, TRACKING_FILE_NAME);
                if (!createIfDoesNotExists && !utFile.isDirectory ()) {
                    // if the update_tracking directory is missing
                    // do not allow creation at all (only in userdir)
                    return null;
                }
                File noAU = new File(path, FORBID_AUTOUPDATE); // NOI18N
                if (noAU.exists()) {
                    // ok, this prevents autoupdate from accessing this 
                    // directory completely
                    return null;
                }
                
                track = new UpdateTracking (path, context);
                trackings.put (path, track);
                track.read ();
                track.scanDir ();
            }
            return track;
        }
    }
    

    /** Finds update tracking for given cluster root.
     * @path root of a cluster
     * @return the additional information for that cluster
     */    
    static UpdateTracking.AdditionalInfo getAdditionalInformation (File path, UpdatingContext context) {
        synchronized (infos) {
            UpdateTracking.AdditionalInfo additionalInfo = infos.get (path);
            if (additionalInfo == null) {
                getTracking (path, false, context);
                File downloadDir = new File (path, ModuleUpdater.DOWNLOAD_DIR);
                if (downloadDir.exists () && downloadDir.isDirectory ()) {
                    File addInfo = new File (downloadDir, ADDITIONAL_INFO_FILE_NAME);
                    if (addInfo.exists ()) {
                        additionalInfo = new UpdateTracking.AdditionalInfo (addInfo);
                    }
                }
            }
            return additionalInfo;
        }
    }
    

    /** Returns the platform installatiion directory.
     * @return the File directory.
     */
    public static File getPlatformDir () {
        String platform = System.getProperty ("netbeans.home");
        return platform == null ? null : new File (platform); // NOI18N
    }
    
    public static File getUserDir () {
        // bugfix #50242: the property "netbeans.user" can return dir with non-normalized file e.g. duplicate //
        // and path and value of this property wrongly differs
        String user = System.getProperty ("netbeans.user");
        File userDir = null;
        if (user != null) {
            // XXX cannot use FileUtil.normalizeFile from here
            userDir = new File (user);
            if (userDir.getPath ().startsWith ("\\\\")) {
                // Could use URI.normalize but only on userDir.getPath().toUri() in JDK 7 (#4723726 breaks UNC for userDir.toURI())
                try {
                    userDir = userDir.getCanonicalFile ();
                } catch (IOException ex) {
                    // fallback when getCanonicalFile fails
                    userDir = userDir.getAbsoluteFile ();
                }
            } else {
                userDir = new File (userDir.toURI ().normalize ()).getAbsoluteFile ();
            }
        }
        
        return userDir;
    }
    
    /** Returns enumeration of Files that represent each possible install
     * directory.
     * @param includeUserDir whether to include also user dir
     * @return List<File>
     */
    public static List<File> clusters (boolean includeUserDir) {
        List<File> files = new ArrayList<File> ();
        
        if (includeUserDir) {
            File ud = getUserDir ();
            if (ud != null) {
                // this prevents autoupdate from accessing this 
                // directory completely
                File noAU = new File (ud, FORBID_AUTOUPDATE); // NOI18N
                if (! noAU.exists ()) {
                    files.add (ud);
                }
            }
        }
        
        String dirs = System.getProperty("netbeans.dirs"); // NOI18N
        if (dirs != null) {
            Enumeration en = new StringTokenizer (dirs, File.pathSeparator);
            while (en.hasMoreElements ()) {
                File f = new File ((String)en.nextElement ());
                // this prevents autoupdate from accessing this 
                // directory completely
                File noAU = new File (f, FORBID_AUTOUPDATE); // NOI18N
                if (! noAU.exists ()) {
                    files.add (f);
                }
            }
        }
        
        
        File id = getPlatformDir ();
        if (id != null) {
            // this prevents autoupdate from accessing this 
            // directory completely
            File noAU = new File (id, FORBID_AUTOUPDATE); // NOI18N
            if (! noAU.exists ()) {
                files.add (id);
            }
        }
        
        return java.util.Collections.unmodifiableList (files);
    }
    
    //
    // Useful search methods
    //
    
    /** Returns true if module with given code base is installed here
     * @param codeBase name of the module
     * @return true or false
     */
    public boolean isModuleInstalled (String codeBase) {
        for (Module m: installedModules.values ()) {
            String mm = m.codenamebase;
            int indx = mm.indexOf ('/');
            if (indx >= 0) {
                mm = mm.substring (0, indx);
            }
            if (codeBase.equals (mm)) {
                return true;
            }
        }
        return false;
    }
    
    //
    // Private impls
    //
    private static ErrorHandler DUMMY_ERROR_HANDLER = new ErrorHandler() {

                @Override
                public void warning(SAXParseException exception) throws SAXException {
                }

                @Override
                public void error(SAXParseException exception) throws SAXException {
                }

                @Override
                public void fatalError(SAXParseException exception) throws SAXException {
                }
            };
    
    /** Scan through org.w3c.dom.Document document. */
    private void read() {
        /** org.w3c.dom.Document document */
        org.w3c.dom.Document document;

        File file;
        InputStream is;
        int avail = 0;
        try {
            file = trackingFile;
            
            if ( ! file.isFile () ) {
                return;
            }
            
            is = new FileInputStream( file );
            avail = is.available();

            InputSource xmlInputSource = new InputSource( is );
            document = XMLUtil.parse(xmlInputSource, false, false, DUMMY_ERROR_HANDLER, XMLUtil.createAUResolver());
            if (is != null) {
                is.close();
            }
        }
        catch ( org.xml.sax.SAXException e ) {
            XMLUtil.LOG.log(Level.SEVERE, "Bad update_tracking: " + trackingFile + ", available bytes: " + avail, e); // NOI18N
            return;
        }
        catch ( java.io.IOException e ) {
            XMLUtil.LOG.log(Level.SEVERE, "Missing update_tracking: " + trackingFile + ", available bytes: " + avail, e); // NOI18N
            return;
        }

        org.w3c.dom.Element element = document.getDocumentElement();
        if ((element != null) && element.getTagName().equals(ELEMENT_MODULES)) {
            scanElement_installed_modules(element);
        }            
    }    
    
    /** Scan through org.w3c.dom.Element named installed_modules. */
    void scanElement_installed_modules(org.w3c.dom.Element element) { // <installed_modules>
        // element.getValue();
        org.w3c.dom.NodeList nodes = element.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            org.w3c.dom.Node node = nodes.item(i);
            if ( node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE ) {
                org.w3c.dom.Element nodeElement = (org.w3c.dom.Element)node;
                if (nodeElement.getTagName().equals(ELEMENT_MODULE)) {
                    if (true) {
                        throw new IllegalStateException ("What now!?");
                    }
                    // XXX  - should put the module into installedModules but do not know the key
                    // modules.add( scanElement_module(nodeElement, fromuser) );
                }                
            }
        }
    }
    
    /** Scan through org.w3c.dom.Element named module. */
    Module scanElement_module(org.w3c.dom.Element element) { // <module>
        Module module = new Module ();
        org.w3c.dom.NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            org.w3c.dom.Attr attr = (org.w3c.dom.Attr)attrs.item(i);
            if (attr.getName().startsWith(ATTR_CODENAMEBASE)) { 
                // <module codename="???"> or old version <module codenamebase="???">
                module.setCodenamebase( attr.getValue() );
            }
        }
        org.w3c.dom.NodeList nodes = element.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            org.w3c.dom.Node node = nodes.item(i);
            if ( node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE ) {
                org.w3c.dom.Element nodeElement = (org.w3c.dom.Element)node;
                if (nodeElement.getTagName().equals(ELEMENT_VERSION)) {
                    scanElement_module_version(nodeElement, module);
                }
            }
        }
        return module;
    }
    
    /** Scan through org.w3c.dom.Element named module_version. */
    private void scanElement_module_version(org.w3c.dom.Element element, Module module) { // <module_version>
        Version version = new Version(module);        
        org.w3c.dom.NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            org.w3c.dom.Attr attr = (org.w3c.dom.Attr)attrs.item(i);
            if (attr.getName().equals(ATTR_VERSION)) { // <module_version specification_version="???">
                version.setVersion( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_ORIGIN)) { // <module_version origin="???">
                version.setOrigin( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_LAST)) { // <module_version last="???">
                version.setLast( Boolean.valueOf(attr.getValue() ).booleanValue());
            }
            if (attr.getName().equals(ATTR_INSTALL)) { // <module_version install_time="???">
                long li = 0;
                try {
                    li = Long.parseLong( attr.getValue() );
                } catch ( NumberFormatException nfe ) {
                }
                version.setInstall_time( li );
            }
        }
        org.w3c.dom.NodeList nodes = element.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            org.w3c.dom.Node node = nodes.item(i);
            if ( node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE ) {
                org.w3c.dom.Element nodeElement = (org.w3c.dom.Element)node;
                if (nodeElement.getTagName().equals(ELEMENT_FILE)) {
                    scanElement_file(nodeElement, version);
                }
            }
        }
        module.addOldVersion( version );
    }
    
    /** Scan through org.w3c.dom.Element named file. */
    void scanElement_file(org.w3c.dom.Element element, Version version) { // <file>
        ModuleFile file = new ModuleFile();        
        org.w3c.dom.NamedNodeMap attrs = element.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            org.w3c.dom.Attr attr = (org.w3c.dom.Attr)attrs.item(i);
            if (attr.getName().equals(ATTR_FILE_NAME)) { // <file name="???">
                file.setName( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_CRC)) { // <file crc="???">
                file.setCrc( attr.getValue() );
            }
            if (attr.getName().equals(ATTR_VERSION)) {
                file.setLocaleversion( attr.getValue() );
            }
        }
        version.addFile (file );
    }
    
    Module readModuleTracking (String codename, boolean create ) {
        new File(directory, TRACKING_FILE_NAME).mkdirs();
        File file = new File (
            new File(directory, TRACKING_FILE_NAME), 
            getTrackingName( codename ) + XML_EXT 
        );
        
        // fix for #34355
        try {
            if ( file.exists() && file.length()==0 ) {
                file.delete();
            }
        } catch (Exception e) {
            // ignore
        }
        
        if ( ! file.exists() ) {
            if ( create ) {
                return new Module( codename, file);
            } else {
                return null;
            }
        }

        return readModuleFromFile( file, codename, create );
    }
    
    Version createVersion(String specversion) {
        Version ver = new Version(null);
        ver.setVersion( specversion );
        return ver;
    }
    
    private Module readModuleFromFile( File file, String codename, boolean create ) {
        
        /** org.w3c.dom.Document document */
        org.w3c.dom.Document document;
        InputStream is;
        try {
            is = new FileInputStream( file );

            InputSource xmlInputSource = new InputSource( is );
            document = XMLUtil.parse(xmlInputSource, false, false, DUMMY_ERROR_HANDLER, XMLUtil.createAUResolver());
            if (is != null) {
                is.close();
            }
        } catch ( org.xml.sax.SAXException e ) {
            XMLUtil.LOG.log(Level.SEVERE, "Bad update_tracking", e); // NOI18N
            return null;
        }
        catch ( java.io.IOException e ) {
            if ( create ) {
                return new Module (codename, file);
            } else {
                return null;
            }
        }

        org.w3c.dom.Element element = document.getDocumentElement();
        if ((element != null) && element.getTagName().equals(ELEMENT_MODULE)) {
            
            Module m = scanElement_module (element);
            m.setFile( file );
            installedModules.put (file, m);
            return m;
        }
        if ( create ) {
            return new Module (codename, file);
        } else {
            return null;
        }
    }
    
    private static String getTrackingName(String codename) {
        String trackingName = codename;
        int pos = trackingName.indexOf('/');    // NOI18N
        if ( pos > -1 ) {
            trackingName = trackingName.substring( 0, pos );
        }
        return trackingName.replace( '.', '-' );       // NOI18N
    }
    
    void deleteUnusedFiles() {
        List<Module> newModules = new ArrayList<Module> (installedModules.values ());
        for (Module mod: newModules) {
            mod.deleteUnusedFiles();
        }
        scanDir ();
    }
    
    public static long getFileCRC(File file) throws IOException {
        BufferedInputStream bsrc = null;
        CRC32 crc = new CRC32();
        try {
            bsrc = new BufferedInputStream( new FileInputStream( file ) );
            byte[] bytes = new byte[1024];
            int i;
            while( (i = bsrc.read(bytes)) != -1 ) {
                crc.update(bytes, 0, i );
            }
        }
        finally {
            if ( bsrc != null ) {
                bsrc.close();
            }
        }
        return crc.getValue();
    }
    
    private void scanDir () {
        File dir = new File (directory, TRACKING_FILE_NAME);
        File[] files = dir.listFiles( new FileFilter() {
                               @Override
                               public boolean accept( File file ) {
                                   if ( !file.isDirectory() && file.getName().toUpperCase().endsWith(".XML") ) {
                                       return true;
                                   } else {
                                       return false;
                                   }
                               }
                           } );
                           
        if (files == null) {
            return;
        }
                           
        for ( int i = 0; i < files.length; i++ ) {
            if (!installedModules.containsKey (files[i])) {
                readModuleFromFile( files[i], null, true );
            }
                
        }
    }
    
    @Override
    public String toString() {
        return "UpdateTracing[" + this.directory + ", origin: " + this.origin + "]";
    }
    
    class Module extends Object {        
        
        /** Holds value of property codenamebase. */
        private String codenamebase;
        
        /** Holds value of property versions. */
        private List<Version> versions = new ArrayList<Version>();
        
        private File file = null;
        
        public Module() {
        }
        
        public Module(String codenamebase, File file) {
            this.codenamebase = codenamebase;
            this.file = file;
        }
        
        private Version lastVersion = null;
        private Version newVersion = null;
        private boolean osgi = false;
        
        /** Getter for property codenamebase.
         * @return Value of property codenamebase.
         */
        String getCodenamebase() {
            return codenamebase;
        }
        
        /** Setter for property codenamebase.
         * @param codenamebase New value of property codenamebase.
         */
        void setCodenamebase(String codenamebase) {
            this.codenamebase = codenamebase;
        }

        void setOSGi(boolean isOSGi) {
            this.osgi = isOSGi;
        }
        boolean isOSGi() {
            return osgi;
        }

        
        /** Getter for property versions.
         * @return Value of property versions.
         */
        List<Version> getVersions() {
            return versions;
        }
        
        /** Setter for property versions.
         * @param versions New value of property versions.
         */
        void setVersions(List<Version> versions) {
            this.versions = versions;
        }
        
        private Version getNewOrLastVersion() {
            if ( newVersion != null ) {
                return newVersion;
            } else {
                return lastVersion;
            }
        }
        
        boolean hasNewVersion() {
            return newVersion != null;
        }
        
        void setFile(File file) {
            this.file = file;
        }
        
        public Version addNewVersion( String spec_version, String origin ) {
            if ( lastVersion != null ) {
                lastVersion.setLast ( false );
            }
            Version version = new Version(this);        
            newVersion = version;
            version.setVersion( spec_version );
            version.setOrigin( origin );
            version.setLast( true );
            version.setInstall_time( System.currentTimeMillis() );
            versions.add( version );
            return version;
        }
        
        void addOldVersion( Version version ) {
            if ( version.isLast() ) {
                lastVersion = version;
            }
                    
            versions.add( version );
        }
        
        void addL10NVersion( Version l_version ) {
            if ( lastVersion != null ) {
                lastVersion.addL10NFiles( l_version.getFiles() );
            } else {
                l_version.setOrigin( origin );
                l_version.setLast( true );
                l_version.setInstall_time( System.currentTimeMillis() );
                versions.add( l_version );
            }
        }
        
        void writeConfigModuleXMLIfMissing () {
            File configDir = new File (new File (directory, ModuleDeactivator.CONFIG), ModuleDeactivator.MODULES); // NOI18N
            
            String candidate = null;
            String oldCandidate = null;
            String newCandidate = null;
            
            String name = codenamebase;
            int indx = name.indexOf ('/');
            if (indx > 0) {
                name = name.substring (0, indx);
            }
            
            // check module name from config file
            String replaced = name.replace ('.', '-'); // NOI18N
            String searchFor;
            
            if (replaced.indexOf (ModuleDeactivator.MODULES) > 0) { // NOI18N
                // standard module
                searchFor = replaced + ".jar"; // NOI18N
            } else if(osgi) {
                searchFor = replaced + ".jar"; // NOI18N
            } else {
                // core module
                searchFor = replaced.substring (replaced.lastIndexOf ('-') > 0 ? replaced.lastIndexOf ('-') + 1 : 0) + ".jar"; // NOI18N
            }
            
            String dash = name.replace ('.', '-');

            {
                boolean needInfoInUserDir = false;
                boolean afterNBMsCluster = false;
                for (File c : clusters(true)) {
                    File hidden = new File(new File(new File(c, "config"), "Modules"), dash + ".xml_hidden");
                    if (hidden.exists()) {
                        hidden.delete();
                        XMLUtil.LOG.info("File " + hidden + " deleted.");
                    }

                    if (directory.equals(c)) {
                        afterNBMsCluster = true;
                        continue;
                    }

                    if (afterNBMsCluster) {
                        continue;
                    }

                    File customConfigs = new File(new File(new File(c, "config"), "Modules"), dash + ".xml");
                    if (customConfigs.exists()) {
                        needInfoInUserDir = lastVersion == null;
                    }
                }

                if (needInfoInUserDir) {
                    // there is a definition for the same XML file in some cluster
                    // already and
                    File userConfig = new File(new File(new File(getUserDir(), "config"), "Modules"), dash + ".xml");
                    writeModulesConfig(userConfig, searchFor, candidate, newCandidate, oldCandidate, name);
                    return;
                }
            }

            File config = new File (configDir,  dash + ".xml"); // NOI18N
            if (config.isFile ()) {
                // already written
                return;
            }
            writeModulesConfig(config, searchFor, candidate, newCandidate, oldCandidate, name);
        }
        
        void write( ) {
            Document document = XMLUtil.createDocument(ELEMENT_MODULE);

            Element e_module = document.getDocumentElement();
            Element e_version;
            Element e_file;

            e_module.setAttribute(ATTR_CODENAMEBASE, getCodenamebase());

            for (Version ver : getVersions()) {
                e_version = document.createElement(ELEMENT_VERSION);
                if (ver.getVersion() != null) {
                    e_version.setAttribute(ATTR_VERSION, ver.getVersion());
                }
                e_version.setAttribute(ATTR_ORIGIN, ver.getOrigin());
                e_version.setAttribute(ATTR_LAST, Boolean.valueOf(ver.isLast()).toString());
                e_version.setAttribute(ATTR_INSTALL, Long.toString(ver.getInstall_time()));
                e_module.appendChild(e_version);
                
                for (ModuleFile moduleFile : ver.getFiles()) {
                    e_file = document.createElement(ELEMENT_FILE);
                    e_file.setAttribute(ATTR_FILE_NAME, moduleFile.getName());
                    e_file.setAttribute(ATTR_CRC, moduleFile.getCrc());
                    if (moduleFile.getLocaleversion() != null) {
                        e_file.setAttribute(ATTR_VERSION, moduleFile.getLocaleversion());
                    }
                    e_version.appendChild(e_file);
                }
            }

            document.getDocumentElement().normalize();

            OutputStream os = null;
            try {
                os = context.createOS(file);
            } catch (Exception e) {
                XMLUtil.LOG.log(Level.WARNING, "Cannot read " + file, e);
                //#154904
                if (!file.delete()) {
                    XMLUtil.LOG.log(Level.SEVERE, null, new IOException("Corresponding update would not be installed since it is not possible to modify or delete update tracking file " + file));
                } else {
                    XMLUtil.LOG.log(Level.SEVERE, null, new IOException("Update tracking file was deleted since permissions does not allow to modify it: " + file));
                    try {
                        os = context.createOS(file);
                    } catch (Exception ex) {
                        XMLUtil.LOG.log(Level.WARNING, "Cannot read", ex);
                    }
                }
            }

            if (os != null) {
                try {
                    XMLUtil.write(document, os);
                    XMLUtil.LOG.info("File " + file + " modified.");
                } catch (IOException e) {
                    XMLUtil.LOG.log(Level.WARNING, "Cannot write " + file, e);
                } finally {
                    try {
                        os.close();
                    } catch (IOException e) {
                        XMLUtil.LOG.log(Level.WARNING, "Cannot close " + file, e);
                    }
                }
            }
        }

        void deleteUnusedFiles() {
            if ( lastVersion == null || newVersion == null ) {
                return;
            }
            for (ModuleFile modFile : lastVersion.getFiles()) {
                if ( ! newVersion.containsFile( modFile ) && modFile.getName().indexOf( LOCALE_DIR ) == -1 ) {
                    safeDelete( modFile );
                }
            }
        }
        
        private void safeDelete(ModuleFile modFile) {
            // test file existence
            File f = new File( file.getParentFile().getParent() + FILE_SEPARATOR + modFile.getName() );
            if ( f.exists() ) {
                // test crc
                try {
                    if (! Long.toString(getFileCRC(f)).equals(modFile.getCrc())) {
                        return;
                    }
                } catch ( IOException ioe ) {
                    return;
                }

                // test if file is referenced from other module
                scanDir();
                boolean found = false;
                Iterator<Module> it = installedModules.values ().iterator();
                while ( !found && it.hasNext() ) {
                    Module mod = it.next();
                    if ( ! mod.equals( this ) ) {
                        Version v = mod.getNewOrLastVersion();
                        if ( v != null && v.containsFile( modFile ) ) {
                            found = true;
                        }
                    }
                }
                if ( ! found ) {
                    XMLUtil.LOG.info("Deleting file: " + f);
                    boolean deleted = f.delete();
                    XMLUtil.LOG.info(".... " + f + " was deleted? " + deleted);
                }
            }
        }
        
        String getL10NSpecificationVersion(String jarpath) {
            String localever;
            Collections.<Version>sort( versions );
            for (Version ver: versions) {
                localever = ver.getLocaleVersion( jarpath );
                if ( localever != null ) {
                    return localever;
                }
            }
            return null;
        }

        private void writeModulesConfig(File config, String searchFor, String candidate, String newCandidate, String oldCandidate, String name) {
            config.getParentFile().mkdirs();
            Boolean isAutoload = null;
            Boolean isEager = null;
            java.util.Iterator it = newVersion.getFiles().iterator();
            boolean needToWrite = false;
            while (it.hasNext()) {
                ModuleFile f = (ModuleFile) it.next ();
                String n = f.getName();
                String parentDir;
                {
                    File p = new File(f.getName()).getParentFile();
                    parentDir = p != null ? p.getName() : "";
                }
                needToWrite = needToWrite || n.indexOf(ModuleDeactivator.MODULES) >= 0 || osgi;
                if (n.endsWith(".jar") && ! parentDir.equals("ext")) { // NOI18N
                    // ok, module candidate
                    candidate = f.getName();
                    // the correct candidate looks as e.g. org.netbeans.modules.mymodule
                    // if no jar looks as codenamebase then the jar file will be found as module's jar
                    if (searchFor.endsWith(candidate) || candidate.endsWith(searchFor)) {
                        newCandidate = candidate;
                        oldCandidate = null;
                        // autoload and eager will set by module's jar
                        if ("autoload".equals(parentDir)) {
                            // NOI18N
                            isAutoload = Boolean.TRUE;
                        } else {
                            isAutoload = Boolean.FALSE;
                        }
                        if ("eager".equals(parentDir)) {
                            // NOI18N
                            isEager = Boolean.TRUE;
                        } else {
                            isEager = Boolean.FALSE;
                        }
                    } else {
                        if (newCandidate == null) {
                            oldCandidate = (oldCandidate == null ? "" : oldCandidate + ", ") + candidate; // NOI18N
                        }
                    }
                }
                // if no correct name found => set autoload/eager by the last jar file
                if (isAutoload == null && "autoload".equals(parentDir)) {
                    // NOI18N
                    isAutoload = Boolean.TRUE;
                }
                if (isEager == null && "eager".equals(parentDir)) {
                    // NOI18N
                    isEager = Boolean.TRUE;
                }
            }
            if (!needToWrite) {
                XMLUtil.LOG.log(Level.WARNING, "No config file written for module {0}. No jar file present in \"modules\" directory.", codenamebase);
                return;
            }
            assert newCandidate != null || oldCandidate != null : "No jar file present!";
            if (newCandidate == null) {
                // PENDING: should check but some NBM assumed wrong behaviour before bugfix 53316
                assert oldCandidate.equals(candidate) : "More files look as module: " + oldCandidate;
                // only temporary
                if (!oldCandidate.equals(candidate)) {
                    XMLUtil.LOG.log(Level.WARNING, "More files look as module: {0}", oldCandidate);
                    oldCandidate = candidate;
                }
                // end of temp
            }
            String moduleName = newCandidate == null ? oldCandidate : newCandidate;
            boolean autoload = isAutoload != null && isAutoload.booleanValue();
            boolean eager = isEager != null && isEager.booleanValue();
            boolean isEnabled = !autoload && !eager;
            String spec = newVersion.getVersion();
            OutputStream os;
            try {
                os = context.createOS(config);
                PrintWriter pw = new PrintWriter(new java.io.OutputStreamWriter(os, "UTF-8"));
                // Please make sure formatting matches what the IDE actually spits
                // out; it could matter.
                pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
                pw.println("<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"");
                pw.println("                        \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">");
                pw.println("<module name=\"" + name + "\">");
                pw.println("    <param name=\"autoload\">" + autoload + "</param>");
                pw.println("    <param name=\"eager\">" + eager + "</param>");
                if (isEnabled) {
                    pw.println("    <param name=\"enabled\">" + isEnabled + "</param>");
                }
                pw.println("    <param name=\"jar\">" + moduleName + "</param>");
                pw.println("    <param name=\"reloadable\">false</param>");
                pw.println("    <param name=\"specversion\">" + spec + "</param>");
                pw.println("</module>");
                pw.flush();
                pw.close();
                XMLUtil.LOG.info("New config was written in " + config);
            } catch (IOException ex) {
                XMLUtil.LOG.log(Level.INFO, null, ex);
            }
        }
        
        @Override
        public String toString() {
            return "UpdateTracing.Module[" + this.codenamebase + "(" + this.file + "), OSGI? " + this.osgi + "]";
        }
    }
    
    public class Version extends Object implements Comparable<Version> {
        private final Module module;
        
        Version(Module m) {
            this.module = m;
        }
        
        /** Holds value of property version. */
        private String version;
        
        /** Holds value of property origin. */
        private String origin;
        
        /** Holds value of property last. */
        private boolean last;
        
        /** Holds value of property install_time. */
        private long install_time = 0;
        
        /** Holds value of property files. */
        private List<ModuleFile> files = new ArrayList<ModuleFile>();
        
        /** Getter for property version.
         * @return Value of property version.
         */
        String getVersion() {
            return version;
        }
        
        /** Setter for property version.
         * @param version New value of property version.
         */
        void setVersion(String version) {
            this.version = version;
        }
        
        /** Getter for property origin.
         * @return Value of property origin.
         */
        String getOrigin() {
            return origin;
        }
        
        /** Setter for property origin.
         * @param origin New value of property origin.
         */
        void setOrigin(String origin) {
            this.origin = origin;
        }
        
        /** Getter for property last.
         * @return Value of property last.
         */
        boolean isLast() {
            return last;
        }
        
        /** Setter for property last.
         * @param last New value of property last.
         */
        void setLast(boolean last) {
            this.last = last;
        }
        
        /** Getter for property install_time.
         * @return Value of property install_time.
         */
        long getInstall_time() {
            return install_time;
        }
        
        /** Setter for property install_time.
         * @param install_time New value of property install_time.
         */
        void setInstall_time(long install_time) {
            this.install_time = install_time;
        }
        
        /** Getter for property files.
         * @return Value of property files.
         */
        List<ModuleFile> getFiles() {
            return files;
        }
        
        /** Setter for property files.
         * @param files New value of property files.
         */
        void addL10NFiles(List<ModuleFile> l10nfiles) {
            for (ModuleFile lf : l10nfiles) {
                String lname = lf.getName();
                for ( int i = files.size() - 1; i >=0; i-- ) {
                    ModuleFile f = files.get( i );
                    if ( f.getName().equals( lname ) ) {
                        files.remove( i );
                    }
                }
            }
            files.addAll( l10nfiles );
        }
        
        void addFile( ModuleFile file ) {
            files.add( file );
        }
        
        public void addFileWithCrc( String filename, String crc ) {
            ModuleFile file = new ModuleFile();
            file.setName( filename );
            file.setCrc( crc );
            files.add( file );
        }
        
        public void addL10NFileWithCrc( String filename, String crc, String specver ) {
            ModuleFile file = new ModuleFile();
            file.setName( filename );
            file.setCrc( crc );
            file.setLocaleversion( specver );
            files.add( file );
        }
        
        boolean containsFile( ModuleFile file ) {
            for (ModuleFile f : files) {
                if ( f.getName().equals( file.getName() ) ) {
                    return true;
                }
            }
            return false;
        }
        
        ModuleFile findFile(String filename) {
            for (ModuleFile f : files) {
                if ( f.getName().equals( filename ) ) {
                    return f;
                }
            }
            return null;
        }
        
        String getLocaleVersion(String filename) {
            String locver = null;
            ModuleFile f = findFile( filename );
            if ( f != null ) {
                locver = f.getLocaleversion();
                if ( locver == null ) {
                    locver = version;
                }
            }
            return locver;
        }
        
        @Override
        public int compareTo (Version oth) {
            if ( install_time < oth.getInstall_time() ) {
                return 1;
            }
            else if ( install_time > oth.getInstall_time() ) {
                return -1;
            } else {
                return 0;
            }
        }
        
        @Override
        public String toString() {
            return "UpdateTracing.Version[" + this.module + "/" + this.version + ", last? " + this.isLast() + "]";
        }
    }
    
    class ModuleFile extends Object {        
        
        /** Holds value of property name. */
        private String name;
        
        /** Holds value of property crc. */
        private String crc;
        
        /** Holds value of property localeversion. */
        private String localeversion = null;
        
        /** Getter for property name.
         * @return Value of property name.
         */
        String getName() {
            return name;
        }
        
        /** Setter for property name.
         * @param name New value of property name.
         */
        void setName(String name) {
            this.name = name;
        }
        
        /** Getter for property crc.
         * @return Value of property crc.
         */
        String getCrc() {
            return crc;
        }
        
        /** Setter for property crc.
         * @param crc New value of property crc.
         */
        void setCrc(String crc) {
            this.crc = crc;
        }
        
        /** Getter for property localeversion.
         * @return Value of property localeversion.
         *
         */
        public String getLocaleversion() {
            return this.localeversion;
        }
        
        /** Setter for property localeversion.
         * @param localeversion New value of property localeversion.
         *
         */
        public void setLocaleversion(String localeversion) {
            this.localeversion = localeversion;
        }
        
        @Override
        public String toString() {
            return "UpdateTracing.ModuleFile[" + this.name + "(" + this.crc + ")" + "]";
        }
        
    }

    public static class AdditionalInfo extends Object {
        private Map<String, String> sources;
        
        private AdditionalInfo (File additionalInfoFile) {
            sources = readAdditionalInfoFile (additionalInfoFile);
        }
        
        public String getSource (String nbmFileName) {
            return sources != null ? sources.get (nbmFileName) : null;
        }
        
        private Map<String, String> readAdditionalInfoFile (File f) {
            if (f == null || ! f.exists ()) {
                throw new IllegalArgumentException ("AdditionalInfo file " + f + " must exists.");
            }

            Map<String, String> res = null;

            /** org.w3c.dom.Document document */
            org.w3c.dom.Document document;

            InputStream is = null;
            try {
                is = new FileInputStream (f);
                document = XMLUtil.parse (new InputSource (is), false, false, null, null);
            } catch (org.xml.sax.SAXException e) {
                XMLUtil.LOG.log (Level.WARNING,"Bad " + UpdateTracking.ADDITIONAL_INFO_FILE_NAME + f, e); // NOI18N
                return res;
            } catch (java.io.IOException e) {
                XMLUtil.LOG.log (Level.WARNING,"Missing " + UpdateTracking.ADDITIONAL_INFO_FILE_NAME + f, e); // NOI18N
                return res;
            } finally {
                if (is != null) {
                    try {
                        is.close ();
                    } catch (IOException ioe) {
                        XMLUtil.LOG.log (Level.INFO, "Cannot close stream for file " + f, ioe); // NOI18N
                        return res;
                    }
                }
            }

            org.w3c.dom.Element element = document.getDocumentElement ();
            if ((element != null) && element.getTagName ().equals (ELEMENT_ADDITIONAL)) {
                res = scanModuleAdditional (element);
            }         

            return res;
        }
        
        private Map<String, String> scanModuleAdditional (org.w3c.dom.Element element) {
            Map<String, String> res = new HashMap<String, String> ();
            org.w3c.dom.NodeList nodes = element.getChildNodes ();
            for (int i = 0; i < nodes.getLength (); i++) {
                org.w3c.dom.Node node = nodes.item (i);
                if (node.getNodeType () == org.w3c.dom.Node.ELEMENT_NODE) {
                    org.w3c.dom.Element nodeElement = (org.w3c.dom.Element) node;
                    if (nodeElement.getTagName ().equals (ELEMENT_ADDITIONAL_MODULE)) {
                        String fileSpec = nodeElement.getAttribute (ATTR_ADDITIONAL_NBM_NAME);
                        String source = nodeElement.getAttribute (ATTR_ADDITIONAL_SOURCE);
                        res.put (fileSpec, source);
                    }                
                }
            }
            return res;
        }
    }
    
}
