/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.resource.internal.mbean;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import javax.management.MBeanException;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.apache.commons.modeler.BaseModelMBean;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

import org.ow2.jonas.deployment.common.lib.JLSResourceResolver;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.lib.util.XMLSerializer;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.SAXException;

/**
 * A generic archive configuration MBean class which provides
 * the ability to extract an XML configuration file from an archive
 * (jar/war/ear/rar) as either a Document or as a String of XML.  As
 * well as storing an updated configuration file.
 *
 * @author Patrick Smith
 * @author Greg Lapouchnian
 */
public class ArchiveConfigMBean extends BaseModelMBean {

    /**
     * Default constructor, constructs a BaseModelMBean.
     * @throws MBeanException from the super class.
     */
    public ArchiveConfigMBean() throws MBeanException {
        super();
    }

    /**
     * Takes an <code>org.w3c.dom.Document</code> object
     * and returns a <code>String</code> representation of the
     * XML Documnt.
     * @param doc the Document representation of the XML.
     * @return a String representation of the XML.
     */
    private static String serializeDocument(final Document doc) {
        XMLSerializer ser = new XMLSerializer(doc);

        OutputStream out = new java.io.ByteArrayOutputStream();
        try {
            ser.serialize(out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return out.toString();
    }

    public void createArchiveWithXmlFile(final String archiveName, final String xmlFilePath, final String doc)
    throws IOException {
        JarEntry newEntry;
        File tempJarFile;
        JarOutputStream tempJar;

        try {
            newEntry = new JarEntry(xmlFilePath);
            tempJarFile = new File(archiveName);

                if(tempJarFile.exists()) {
                    throw new Exception("File already exists.");
                }

            tempJar = new JarOutputStream(new FileOutputStream(tempJarFile));
        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        }

        try {

            byte[] bytes = doc.getBytes();
            tempJar.putNextEntry(newEntry);
            tempJar.write(bytes, 0, bytes.length);

        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        } finally {
            tempJar.close();
        }
    }

    public void addXML(final String archiveName, final String xmlFilePath, final String docString)
            throws IOException {
        File jarFile;
        JarFile rarFile;
        JarEntry newEntry;
        File tempJarFile;
        JarOutputStream tempJar;
        boolean updated = false;

        try {
            jarFile = new File(archiveName);
            rarFile = new JarFile(jarFile);
            newEntry = new JarEntry(xmlFilePath);
            tempJarFile = new File(archiveName + ".tmp");

            tempJar = new JarOutputStream(new FileOutputStream(tempJarFile));
        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        }

        try {

            // Allocate a buffer for reading entry data.
            byte[] buffer = new byte[1024];
            int bytesRead;

            byte[] bytes = docString.getBytes();
            tempJar.putNextEntry(newEntry);
            tempJar.write(bytes, 0, bytes.length);

            for (Enumeration entries = rarFile.entries(); entries
                    .hasMoreElements();) {
                // Get the next entry.
                JarEntry entry = (JarEntry) entries.nextElement();

                // If the entry has not been added already, add it.
                if (!entry.getName().equals(xmlFilePath)) {
                    // Get an input stream for the entry.
                    InputStream entryStream = rarFile.getInputStream(entry);

                    // Read the entry and write it to the temp jar.
                    tempJar.putNextEntry(entry);

                    while ((bytesRead = entryStream.read(buffer)) != -1) {
                        tempJar.write(buffer, 0, bytesRead);
                    }
                }
            }
            updated = true;
        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        } finally {
            rarFile.close();
            tempJar.close();

            if (updated) {
                jarFile.delete();
                tempJarFile.renameTo(jarFile);
            }
        }

    }

    /**
     * Saves the given <code>org.w3c.dom.Document</code> back into an archive
     * of the name given by archiveName.
     *
     * The Java Jar/Archive classes apparently do not provide support for
     * simply updating a file in place in an archive, so a temporary archive
     * must be created and have the contents of the existing archive copied over
     * (with the exception of the updated Document file) then remove the existing
     * archive and rename the temporary one.
     *
     * @param archiveName The name (and path) of the archive to updated.
     * @param xmlFilePath The path within the archive to the XML file being updated.
     * @param doc The Document representation of the XML file being updated.
     * @throws IOException If the updating fails.
     */
    public void saveXML(final String archiveName, final String xmlFilePath, final Document doc)
            throws IOException {
        File jarFile;
        JarFile rarFile;
        JarEntry newEntry;
        File tempJarFile;
        JarOutputStream tempJar;
        boolean updated = false;

        try {
            jarFile = new File(archiveName);
            rarFile = new JarFile(jarFile);
            newEntry = new JarEntry(xmlFilePath);
            tempJarFile = new File(archiveName + ".tmp");

            tempJar = new JarOutputStream(new FileOutputStream(tempJarFile));
        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        }

        try {

            // Allocate a buffer for reading entry data.
            byte[] buffer = new byte[1024];
            int bytesRead;

            String docString = serializeDocument(doc);
            byte[] bytes = docString.getBytes();
            tempJar.putNextEntry(newEntry);
            tempJar.write(bytes, 0, bytes.length);

            for (Enumeration entries = rarFile.entries(); entries
                    .hasMoreElements();) {
                // Get the next entry.
                JarEntry entry = (JarEntry) entries.nextElement();

                // If the entry has not been added already, add it.
                if (!entry.getName().equals(xmlFilePath)) {
                    // Get an input stream for the entry.
                    InputStream entryStream = rarFile.getInputStream(entry);

                    // Read the entry and write it to the temp jar.
                    tempJar.putNextEntry(entry);

                    while ((bytesRead = entryStream.read(buffer)) != -1) {
                        tempJar.write(buffer, 0, bytesRead);
                    }
                }
            }
            updated = true;
        } catch (Exception e) {
            throw new IOException("Unable to update archive: " + archiveName
                    + "\n" + e.getMessage());
        } finally {
            rarFile.close();
            tempJar.close();

            if (updated) {
                jarFile.delete();
                tempJarFile.renameTo(jarFile);
            }
        }

    }

    /**
     * Verifies that the Document is correct with respect to the schemas
     * associated with the XML.  If no exception is thrown then the XML is
     * either valid or no schemas were available for this XML.  In which case
     * the Document can be considered verified.
     * @param doc The Document to validate.
     * @throws SAXException If the document fails validation.
     * @throws IOException If there is an error opening a schema.
     */
    public void verifyDocument(final Document doc) throws SAXException, IOException {
        SchemaFactory factory = SchemaFactory
                .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

        // use a custom ResourceResolver in order to load Schemas and DTDs
        // locally when validating
        factory
                .setResourceResolver(new JLSResourceResolver(
                        getEntityResolver()));

        String schemaPath = getSchema(doc);

        if (schemaPath != null) {
            java.net.URL url = new java.net.URL(schemaPath);

            Schema schema = factory.newSchema(url);
            // Create a Validator object, which can be used to validate
            // an instance document.
            Validator validator = schema.newValidator();
            // Validate the DOM tree.
            validator.validate(new DOMSource(doc));
        } else {
            // No schemas were provided for verification.
            // The generic "ArchiveConfigMBean" will not validate, this is
            // left for MBean extension classes to do.
            // This is not an abstract method/class because we may want
            // to use this as a generic, non-validating editor.
        }
    }

    /**
     * Return an EntityResolver for this MBean.
     * @return an EntityResolver for this MBean.
     */
    protected EntityResolver getEntityResolver() {
        // For the generic MBean (for now) return null
        // because we don't know which schemas/dtds to add to the resolver.
        return null;
    }

    /**
     * Extracts a Document representation of the XML file provided by
     * <code>xmlFilePath</code> within the archive <code>archiveName</code>.
     * @param archiveName The archive from which to extract the XML.
     * @param xmlFilePath The path within the archive to the XML file.
     * @return A document representation of the XML file.
     * @throws Exception if the document cannot be extracted.
     */
    public Document extractDocument(final String archiveName, final String xmlFilePath)
            throws Exception {
        Document result = null;
        // Input Stream
        InputStream raInputStream = null;
        // ZipEntry
        ZipEntry raZipEntry = null;
        // Rar file
        JarFile rarFile = null;
        try {
            rarFile = new JarFile(archiveName);
            //Check the ra entry
            raZipEntry = rarFile.getEntry(xmlFilePath);
            if (raZipEntry != null) {
                //Get the stream
                raInputStream = rarFile.getInputStream(raZipEntry);
                result = newDocument(raInputStream, xmlFilePath);
            } else {
                throw new FileNotFoundException("Could not find " + xmlFilePath
                        + " in " + archiveName);
            }
        } catch (Exception e) {
            Logger logger = Log.getLogger(Log.JONAS_ADMIN_PREFIX);
            logger.log(BasicLevel.WARN, "Could not load XML file '"
                    + xmlFilePath + "' from '" + archiveName + "': "
                    + e.getMessage());
            throw e;
        } finally {
            if (raInputStream != null) {
                raInputStream.close();
            }
            if (rarFile != null) {
                rarFile.close();
            }
        }

        return result;
    }

    /**
     * Extracts a String representation of the XML file provided by
     * <code>xmlFilePath</code> within the archive <code>archiveName</code>.
     * @param archiveName The archive from which to extract the XML.
     * @param xmlFilePath The path within the archive to the XML file.
     * @return A String representation of the XML file.
     * @throws Exception if the XML cannot be extracted.
     */
    public String extractXML(final String archiveName, final String xmlFilePath)
            throws Exception {

        String result = null;
        InputStream inputStream = null;
        ZipEntry zipEntry = null;

        try {
            JarFile rarFile = new JarFile(archiveName);

            zipEntry = rarFile.getEntry(xmlFilePath);
            if (zipEntry != null) {
                //Get the stream
                inputStream = rarFile.getInputStream(zipEntry);

                // read from the stream and create the string representing the xml
                InputStreamReader reader = new InputStreamReader(inputStream);
                BufferedReader br = new BufferedReader(reader);
                StringBuffer sb = new StringBuffer();
                String line = null;

                while ((line = br.readLine()) != null) {
                    sb.append(line + "\n");
                }
                result = sb.toString();

            } else {
                throw new FileNotFoundException("Could not find " + xmlFilePath
                        + " in " + archiveName);
            }
        } catch (Exception e) {
            Logger logger = Log.getLogger(Log.JONAS_ADMIN_PREFIX);
            logger.log(BasicLevel.WARN, "Could not load XML file '"
                    + xmlFilePath + "' from '" + archiveName + "': "
                    + e.getMessage());
            throw e;
        }

        return result;
    }

    /**
     * Returns a new Document representation of the XML within the
     * InputStream with the name <code>name</code>.
     * @param is The InputStream containing the XMl.
     * @param name The name of the XML file.
     * @return A new Document representation of the XML.
     * @throws ParserConfigurationException if the parser configuration is incorrect.
     * @throws SAXException if the parsing fails.
     * @throws IOException if the files cannot be opened.
     */
    private Document newDocument(final InputStream is, final String name)
            throws ParserConfigurationException, SAXException, IOException {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();

        // get the entity resolved if one was provided by the subclass
        if (getEntityResolver() != null) {
            builder.setEntityResolver(getEntityResolver());
        }
        Document doc = builder.parse(is);

        // close InputStream when finished parsing
        is.close();

        return doc;
    }

    /**
     * Returns the path for the schema based on the xsi:schemalocation property
     * @param doc The Document to get schemas for.
     * @return The schema for the specified document.
     */
    private String getSchema(final Document doc) {
        String schemaPath = null;
        if (doc.getDocumentElement().getAttribute("xsi:schemaLocation") != null) {
            String[] parts = doc.getDocumentElement().getAttribute(
                    "xsi:schemaLocation").trim().split(" ");
            String schemaURL = parts[parts.length - 1];

            // get the name of the schema file referenced
            String schemaName = schemaURL
                    .substring(schemaURL.lastIndexOf('/') + 1);

            if (getSchemaList() != null) {
                // find the schema that ends in the same file name as the one
                // specified in the document
                Iterator i = getSchemaList().iterator();
                while (i.hasNext()) {
                    String schema = (String) i.next();
                    String file = schema.substring(schema.lastIndexOf('/') + 1);

                    // we've found the right schema, we will return its URL
                    if (file.equals(schemaName)) {
                        schemaPath = schema;
                    }
                }
            }
        }
        return schemaPath;
    }

    /**
     * Get a list of possible schema locations to validate against.
     * @return a list of schemas that are available for this type of deployment
     * descriptor.
     */
    protected List getSchemaList() {
        return null;
    }
}
