/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (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.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.autoupdate;

import java.io.File;
import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.*;
import java.util.jar.*;
import org.openide.ErrorManager;
import org.openide.filesystems.*;

import org.openide.xml.XMLUtil;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/** This class represents one module update available on the web
 *
 * @author  akemr
 * @version 
 */
class SafeModule extends Object {    

    public static final String PUBLIC_ID = "-//NetBeans//DTD Module Status 1.0//EN"; // NOI18N
    public static final String SYSTEM_ID = "http://www.netbeans.org/dtds/module-status-1_0.dtd"; // NOI18N

    static final String PROP_AUTOLOAD = "autoload"; // NOI18N
    static final String PROP_EAGER = "eager"; // NOI18N
    private static final String PROP_SPEC = "specversion"; // NOI18N
    private static final String PROP_ENABLED = "enabled"; // NOI18N
    private static final String PROP_JAR = "jar"; // NOI18N
    private static final String PROP_ORIGIN = "origin"; // NOI18N
    private static final String PROP_USER = "user"; // NOI18N
    private static final String PROP_INSTALL = "installation"; // NOI18N
    private static final String PROP_RELEASE = "release"; // NOI18N
    private static final String PROP_RELOADABLE = "reloadable"; // NOI18N

    private static final String ATTR_NAME = "name"; // NOI18N
    private static final String ATTR_PARAM = "param"; // NOI18N
    
    private static final String AUTOLOAD_SLASH = "autoload/"; // NOI18N
    private static final String EAGER_SLASH = "eager/"; // NOI18N
    private static final String FILE_SEPARATOR = System.getProperty ("file.separator");
    
    private static Map prepared = new HashMap();
    
    private static boolean register( ModuleUpdate mu ) {
        boolean success = true;
        Iterator it = mu.getJarList().iterator();
        while ( success && it.hasNext() ) {
            String name = (String)it.next();
            success = register( mu, name );
        }
        return success;
    }
    
    static boolean write( Map installNow ) {
        boolean success = true;
        prepared = new HashMap();
        
        Iterator it = installNow.entrySet().iterator();
        while ( success && it.hasNext() ) {
            Map.Entry me = (Map.Entry)it.next();
            ModuleUpdate mu = (ModuleUpdate)me.getKey();
            File f = (File)me.getValue();
            if ( f.exists() ) {
                success = false;
            }
            else {
                success = register( mu );
            }
        }
        
        if ( !success )
            return false;
        
        try {
            FileSystem deffs = Repository.getDefault().getDefaultFileSystem();
            deffs.runAtomicAction( new DocAtomicAction( deffs ) );
        } catch ( IOException ioe ) {
            return false;
        }

        return true;
    }
    
    private static boolean register( ModuleUpdate mu, String jarName ) {
        
        Module module = new Module();
        try {
            loadManifest( jarName, module, mu );
        } catch ( IOException ioe ) {
            return false;
        }
        
        Map m = new HashMap();
        
        m.put( "name", module.getName() );  // NOI18N
        
        boolean autoloaded = false;
        if ( jarName.startsWith( AUTOLOAD_SLASH ) ) {
            autoloaded = true;
            jarName = jarName.substring( AUTOLOAD_SLASH.length() );
        }
        m.put( PROP_AUTOLOAD, autoloaded ? Boolean.TRUE : Boolean.FALSE );
        
        boolean eagered = false;
        if ( jarName.startsWith( EAGER_SLASH ) ) {
            eagered = true;
            jarName = jarName.substring( EAGER_SLASH.length() );
        }
        m.put( PROP_EAGER, eagered ? Boolean.TRUE : Boolean.FALSE );
        
        if ( !autoloaded && !eagered )
            m.put( PROP_ENABLED, Boolean.TRUE );
        
        m.put( PROP_JAR, jarName );
        
        String origin = ""; // NOI18N
        if ( mu.isToInstallDir() )
            origin = PROP_INSTALL;
        else
            origin = PROP_USER;
        if ( autoloaded )
            origin = origin + '/' + PROP_AUTOLOAD;
        else if ( eagered )
            origin = origin + '/' + PROP_EAGER;
        m.put( PROP_ORIGIN, origin );
        
        if ( module.getRelease() != null ) {
            m.put( PROP_RELEASE, module.getRelease() );
        }
        
        m.put( PROP_RELOADABLE, Boolean.FALSE );
        
        if ( module.getSpecVersion() != null ) {
            m.put( PROP_SPEC, module.getSpecVersion() );
        }
        
        String nameDashes = module.getName().replace('.', '-'); // NOI18N

        prepared.put( nameDashes, m );
        
        return true;
    }
    
    private static void loadManifest(String jar, Module module, ModuleUpdate mu) throws IOException {
        
        String path = null;
        if ( mu.isToInstallDir() )
            path = mu.findInstallDirectory ().toString ();
        else
            path = System.getProperty ("netbeans.user");
        path = path + FILE_SEPARATOR + "modules" + FILE_SEPARATOR + jar;
        
        JarFile jarFile = new JarFile( path );
        try {
            Manifest m = jarFile.getManifest();
            String name = m.getMainAttributes().getValue( "OpenIDE-Module" ); // NOI18N
            int slash = name.indexOf('/');  // NOI18N
            if ( slash > -1 ) {
                module.setRelease( name.substring( slash + 1 ) );
                name = name.substring( 0, slash );
            }
            module.setName( name );
            String spec = m.getMainAttributes().getValue( "OpenIDE-Module-Specification-Version" ); // NOI18N
            module.setSpecVersion( spec );
        } finally {
            jarFile.close();
        }
    }
    
    static class Module extends Object {
        /** Holds value of property name. */
        private String name;

        /** Holds value of property specVersion. */
        private String specVersion;
        
        /** Holds value of property release. */
        private String release;
        
        /** Getter for property name.
         * @return Value of property name.
         */
        public String getName() {
            return name;
        }

        /** Setter for property name.
         * @param name New value of property name.
         */
        public void setName(String name) {
            this.name = name;
        }

        /** Getter for property specVersion.
         * @return Value of property specVersion.
         */
        public String getSpecVersion() {
            return this.specVersion;
        }
        
        /** Setter for property specVersion.
         * @param specVersion New value of property specVersion.
         */
        public void setSpecVersion(String specVersion) {
            this.specVersion = specVersion;
        }
        
        /** Getter for property release.
         * @return Value of property release.
         */
        public String getRelease() {
            return this.release;
        }
        
        /** Setter for property release.
         * @param release New value of property release.
         */
        public void setRelease(String release) {
            this.release = release;
        }
        
    }
    
    private static class DocAtomicAction implements FileSystem.AtomicAction {
        
        private FileSystem fs;
        
        public DocAtomicAction(FileSystem fs) {
            this.fs = fs;
        }
        
        public void run() throws IOException {
            fs.refresh (true); 
            
            FileObject sysm = fs.findResource( "Modules" ); // NOI18N
            
            Iterator it = prepared.entrySet().iterator();
            while ( it.hasNext() ) {
                Map.Entry me = (Map.Entry)it.next();
                String filename = (String)me.getKey();
                Map m = (Map)me.getValue();
                
                if (sysm.getFileObject (filename + ".xml") != null) {
                    // if the file is already created, skip its creation
                    continue;
                }

                FileObject fo = sysm.createData( filename + ".xml" ); // NOI18N
                FileLock lock = fo.lock();
                OutputStream os = null;

                try {
                    os = fo.getOutputStream( lock );
                    writeStatus( m, os );
                } finally {
                    if ( os != null )
                        os.close();
                    lock.releaseLock();
                }
            }
        }
        
        /* copied from core.modules.ModuleList from performance reasons
         * see #27293 for details
        */
        private void writeStatus(Map m, OutputStream os) throws IOException {
            String codeName = (String)m.get("name"); // NOI18N
            if (codeName == null)
                throw new IllegalArgumentException("no code name present"); // NOI18N

            Writer w = new OutputStreamWriter(os, "UTF-8"); // NOI18N
            w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); // NOI18N
            w.write("<!DOCTYPE module PUBLIC \""); // NOI18N
            w.write(PUBLIC_ID);
            w.write("\"\n                        \""); // NOI18N
            w.write(SYSTEM_ID);
            w.write("\">\n"); // NOI18N
            w.write("<module name=\""); // NOI18N
            w.write(XMLUtil.toAttributeValue(codeName)); // NOI18N
            w.write("\">\n");       // NOI18N

            // Use TreeMap to sort the keys by name; since the module status files might
            // be version-controlled we want to avoid gratuitous format changes.
            Iterator it = new TreeMap(m).entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry entry = (Map.Entry)it.next();
                String name = (String)entry.getKey();
                if (name.equals("installerState") || name.equals("name")) { // NOI18N
                    // Skip this one, it is a pseudo-param.
                    continue;
                }

                Object val = entry.getValue();

                w.write("    <param name=\""); // NOI18N
                w.write(XMLUtil.toAttributeValue(name)); // NOI18N
                w.write("\">");     // NOI18N
                w.write(XMLUtil.toElementContent(val.toString()));
                w.write("</param>\n"); // NOI18N
            }

            w.write("</module>\n"); // NOI18N
            w.flush();
        }
    }
    
    static ModuleStatus getModuleStatus(String codenamebase) {
        org.w3c.dom.Document document;

        String name = "Modules/" + codenamebase.replace('.', '-') + ".xml"; // NOI18N
        FileObject fo = Repository.getDefault().getDefaultFileSystem().findResource( name );
        
        if ( fo == null )
            return null;
        
        InputStream is = null;
            
        try {
            is = fo.getInputStream();
            InputSource xmlInputSource = new InputSource( is );
            document = XMLUtil.parse(
                    xmlInputSource,
                    false,
                    false, 
                    new ErrorCatcher(),
                    new EntityResolver() {
                            public InputSource resolveEntity(String pubid, String sysid) throws SAXException, IOException {
                                return new InputSource(SafeModule.class.getResource("module-status-1_0.dtd").toExternalForm()); // NOI18N
                            }
                });
            if (is != null)
                is.close();
        }
        catch ( org.xml.sax.SAXException e ) {
            System.out.println("Bad update_tracking" ); // NOI18N
            e.printStackTrace ();
            return null;
        }
        catch ( java.io.IOException e ) {
            System.out.println("Missing update_tracking" ); // NOI18N
            e.printStackTrace ();
            return null;
        }

        ModuleStatus status = new ModuleStatus();
        
        org.w3c.dom.Element element = document.getDocumentElement();
        if ((element != null) && element.getTagName().equals( "module" )) {  // NOI18N
            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( ATTR_PARAM )) {
                        try {
                            readParam( nodeElement, status );
                        } catch ( Exception e ) {
                        }
                    }
                }
            }
        }
        
        return status;
    }
    
    private static void readParam(org.w3c.dom.Element elem, ModuleStatus status) throws Exception {
        org.w3c.dom.Node n = elem.getChildNodes().item( 0 );
        if ( elem.getAttribute( ATTR_NAME ).equals( PROP_AUTOLOAD ) ) {
            status.setAutoload (Boolean.valueOf (n.getNodeValue ()).booleanValue ());
        } else if ( elem.getAttribute( ATTR_NAME ).equals( PROP_EAGER ) ) {
            status.setEager(Boolean.valueOf (n.getNodeValue ()).booleanValue ());
        } else if ( elem.getAttribute( ATTR_NAME ).equals( PROP_JAR ) ) {
            status.setJar( n.getNodeValue() );
        } else if ( elem.getAttribute( ATTR_NAME ).equals( PROP_ORIGIN ) ) {
            status.setOrigin( n.getNodeValue() );
        }
    }
    
    static class ModuleStatus {
        
        /** Holds value of property autoload. */
        private boolean autoload;
        
        /** Holds value of property eager. */
        private boolean eager;
        
        /** Holds value of property origin. */
        private String origin;
        
        /** Holds value of property jar. */
        private String jar;
        
        /** Getter for property autoload.
         * @return Value of property autoload.
         *
         */
        boolean isAutoload() {
            return this.autoload;
        }
        
        /** Setter for property autoload.
         * @param autoload New value of property autoload.
         *
         */
        void setAutoload(boolean autoload) {
            this.autoload = autoload;
        }
        
        /** Getter for property eager.
         * @return Value of property eager.
         *
         */
        boolean isEager() {
            return this.eager;
        }
        
        /** Setter for property eager.
         * @param eager New value of property eager.
         *
         */
        void setEager(boolean eager) {
            this.eager = eager;
        }
        
        /** Getter for property origin.
         * @return Value of property origin.
         *
         */
        String getOrigin() {
            return this.origin;
        }
        
        /** Setter for property origin.
         * @param origin New value of property origin.
         *
         */
        void setOrigin(String origin) {
            this.origin = origin;
        }
        
        /** Getter for property jar.
         * @return Value of property jar.
         *
         */
        String getJar() {
            return this.jar;
        }
        
        String getJarName() {
            return jar.substring( 0, jar.indexOf( '.' ) );
        }
        
        /** Setter for property jar.
         * @param jar New value of property jar.
         *
         */
        void setJar(String jar) {
            this.jar = jar;
        }
        
        boolean isFromUser() {
            return origin.startsWith( "user" ); // NOI18N
        }
        
        String getJarPath() {
            String path = "modules/";   // NOI18N
            if ( autoload )
                path = path + AUTOLOAD_SLASH;
            else if ( eager )
                path = path + EAGER_SLASH;
            return path;
        }
        
    }
    
    static class ErrorCatcher implements org.xml.sax.ErrorHandler {
        private void message (String level, org.xml.sax.SAXParseException e) {
        }

        public void error (org.xml.sax.SAXParseException e) {
        }

        public void warning (org.xml.sax.SAXParseException e) {
            ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);            
        }

        public void fatalError (org.xml.sax.SAXParseException e) {
        }
    }
}
