/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2007-2008 Bull S.A.S.
 * 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.jmbeans;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.RuntimeOperationsException;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.modeler.BaseModelMBean;
import org.osgi.framework.Bundle;
import org.osgi.service.packageadmin.ExportedPackage;
import org.osgi.service.packageadmin.PackageAdmin;
import org.ow2.jonas.lib.bootstrap.LoaderManager;
import org.ow2.jonas.lib.loader.FilteringClassLoader;
import org.ow2.jonas.lib.management.javaee.J2EEServerState;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

/**
 * J2EEServer MBean implementation.
 * @author Adriana.Danes@bull.net
 */
public class J2EEServerMBean extends BaseModelMBean {

    public J2EEServerMBean() throws MBeanException, RuntimeOperationsException {
        super();
    }

    /**
     * Implement the state attribute.
     * @return the current server's state
     * @throws InstanceNotFoundException
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InvalidTargetObjectTypeException
     */
    public String getState() throws InstanceNotFoundException, RuntimeOperationsException, MBeanException, InvalidTargetObjectTypeException {
        J2EEServerState state =  ((J2EEServer) getManagedResource()).getState();
        return state.getName();
    }


    /**
     * Implement resources attribute.
     * @return the list of J2EEResource names in the current server
     * @throws InstanceNotFoundException
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InvalidTargetObjectTypeException
     */
    public String[] getResources() throws InstanceNotFoundException, RuntimeOperationsException, MBeanException, InvalidTargetObjectTypeException {
        List<String> resourceList = ((J2EEServer) getManagedResource()).getResources();
        String[] result = new String[resourceList.size()];
        return resourceList.toArray(result);
    }

    /**
     * Implement services attribute.
     * @return
     * @throws InstanceNotFoundException
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InvalidTargetObjectTypeException
     */
    public String[] getServices() throws InstanceNotFoundException, RuntimeOperationsException, MBeanException, InvalidTargetObjectTypeException {
        List<String> services = ((J2EEServer) getManagedResource()).getServices();
        String[] result = new String[services.size()];
        for (int i = 0; i < services.size(); i++) {
            result[i] = services.get(i);
        }
        return result;
    }

    /**
     * Implement deployedObjects attribute cf. to JSR77 requirements.
     * @return Array of OBJECT_NAMEs corresponding to all the deployed modules and apps.
     * @throws InstanceNotFoundException
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InvalidTargetObjectTypeException
     */
    public String[] getDeployedObjects() throws InstanceNotFoundException, RuntimeOperationsException, MBeanException, InvalidTargetObjectTypeException {
        List<String> deployedObjects = ((J2EEServer) getManagedResource()).getDeployedObjects();
        String[] result = new String[deployedObjects.size()];
        for (int i = 0; i < deployedObjects.size(); i++) {
            result[i] = deployedObjects.get(i);
        }
        return result;
    }

    /**
     * Return the state of a given service.
     * @param service name
     * @return The service state
     * @throws InstanceNotFoundException
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InvalidTargetObjectTypeException
     * @throws InvalidTargetObjectTypeExceptio
     */
    public String getServiceState(final String service) throws InstanceNotFoundException, RuntimeOperationsException, MBeanException, InvalidTargetObjectTypeException {
        return ((J2EEServer) getManagedResource()).getServiceState(service);
    }

    /**
     * Gets the exported packages for a given package name.
     * @param packageName the name of a package or a class
     * @return the list of exported packages (and bundles exporting it/importing
     *         it, etc.)
     * @throws InvalidTargetObjectTypeException if MBean operation fails
     * @throws RuntimeOperationsException if MBean operation fails
     * @throws InstanceNotFoundException if MBean operation fails
     * @throws TransformerException if XML transformation fails
     */
    public String getExportedPackagesFromPackage(final String packageName) throws MBeanException, InstanceNotFoundException,
    RuntimeOperationsException, InvalidTargetObjectTypeException, TransformerException {
        return getExportedPackages(packageName, false);
    }

    /**
     * Gets the exported packages for a given package name.
     * @param packageName the name of a package or a class
     * @return the list of exported packages (and bundles exporting it/importing
     *         it, etc.)
     * @throws InvalidTargetObjectTypeException if MBean operation fails
     * @throws RuntimeOperationsException if MBean operation fails
     * @throws InstanceNotFoundException if MBean operation fails
     * @throws TransformerException if XML transformation fails
     */
    public String getExportedPackagesFromClass(final String className) throws MBeanException, InstanceNotFoundException,
    RuntimeOperationsException, InvalidTargetObjectTypeException, TransformerException {
        return getExportedPackages(className, true);
    }


    /**
     * Gets the exported packages for a given package name.
     * @param name the name of a package or a class
     * @param boolean true if it is a class, else false
     * @return the list of exported packages (and bundles exporting it/importing
     *         it, etc.)
     * @throws InvalidTargetObjectTypeException if MBean operation fails
     * @throws RuntimeOperationsException if MBean operation fails
     * @throws InstanceNotFoundException if MBean operation fails
     * @throws TransformerException if XML transformation fails
     */
    public String getExportedPackages(final String name, final boolean isClass) throws MBeanException, InstanceNotFoundException,
            RuntimeOperationsException, InvalidTargetObjectTypeException, TransformerException {
        J2EEServer j2eeServer = ((J2EEServer) getManagedResource());

        // Get OSGi package admin service
        PackageAdmin packageAdmin = j2eeServer.getPackageAdminService();

        // Create XML document...

        // Create builder with factory
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("Cannot build document builder", e);
        }

        // Create Document
        Document document = builder.newDocument();

        // Append root element
        Element root = document.createElement("exported-packages");
        document.appendChild(root);


        String packageName = null;
        Class<?> foundClass = null;
        String className = null;
        // It is not a class, use the name as a package name
        if (!isClass) {
            packageName = name;
        } else {
            className = name;
            ClassLoader systemClassLoader = null;
            // needs to load the given class
            try {
                systemClassLoader = LoaderManager.getInstance().getExternalLoader();
            } catch (Exception e) {
                throw new MBeanException(e, "Unable to get external classloader");
            }

            // Now, try to load the given class
            try {
                foundClass = systemClassLoader.loadClass(name);
                packageName = foundClass.getPackage().getName();
            } catch (ClassNotFoundException e) {
                // not found
            }
        }


        // If package has been found, continue
        if (packageName != null) {
            // Now, ask for the given package name
            ExportedPackage[] exportedPackages = packageAdmin.getExportedPackages(packageName);
            if (exportedPackages != null) {

                // Write content for each exported package
                for (ExportedPackage exportedPackage : exportedPackages) {

                    Element exportedPackageElement = document.createElement("exported-package");
                    root.appendChild(exportedPackageElement);

                    // name
                    exportedPackageElement.setAttribute("name", exportedPackage.getName());

                    // version
                    exportedPackageElement.setAttribute("version",exportedPackage.getVersion().toString());

                    // removal-pending
                    exportedPackageElement.setAttribute("removal-pending",Boolean.toString(exportedPackage.isRemovalPending()));


                    // now, write exporting bundle
                    boolean isDefaultBundle = false;
                    Bundle exportingBundle = exportedPackage.getExportingBundle();
                    if (exportingBundle != null) {
                        Element bundleElement = addBundleNode(document, "exporting-bundle", exportingBundle);
                        exportedPackageElement.appendChild(bundleElement);
                        // class is available, needs to search the class on the bundles in order to know if it will be the default bundle or not
                        if (className != null) {
                            Class<?> bundleClass = null;
                            try {
                                bundleClass = exportingBundle.loadClass(className);
                            } catch (ClassNotFoundException e) {
                               // class not found on this given bundle
                            }
                            if (foundClass.equals(bundleClass)) {
                                isDefaultBundle = true;
                            }
                        }
                    }
                    exportedPackageElement.setAttribute("isDefault", Boolean.toString(isDefaultBundle));


                    // And importing bundles
                    Bundle[] importingBundles = exportedPackage.getImportingBundles();
                    if (importingBundles != null) {
                        Element importingBundlesElement = document.createElement("importing-bundles");
                        exportedPackageElement.appendChild(importingBundlesElement);

                        for (Bundle importingBundle : importingBundles) {
                            importingBundlesElement.appendChild(addBundleNode(document, "importing-bundle", importingBundle));
                        }

                    }

                }
            }
        }

        StringWriter stringWriter = new StringWriter();
        StreamResult streamResult = new StreamResult(stringWriter);

        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();

        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

        // transform OUTPUT
        transformer.transform(new DOMSource(document), streamResult);

        return stringWriter.toString();
    }

    /**
     * Gets data about loading a given class.
     * @param className the class name
     * @return a string description for the given class that needs to be loaded
     */
    public String loadClass(final String className) {

        // Get ClassLoader
        ClassLoader systemClassLoader = null;
        try {
            systemClassLoader = LoaderManager.getInstance().getExternalLoader();
        } catch (Exception e) {
            throw new IllegalStateException("Unable to get LoaderManager", e);
        }


        // Create XML document...

        // Create builder with factory
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("Cannot build document builder", e);
        }

        // Create Document
        Document document = builder.newDocument();

        // Append root element
        Element classElement = document.createElement("class");
        document.appendChild(classElement);

        // name
        classElement.setAttribute("name", className);


        boolean classNotFound = false;
        String error = null;
        Class<?> clazz = null;
        try {
            clazz = systemClassLoader.loadClass(className);
        } catch (ClassNotFoundException e) {
            error = e.toString();
            classNotFound = true;
        } catch (Error e) {
            classNotFound = true;
            error = e.toString();
        }

        // class not found ? (add error content if NotFound)
        classElement.setAttribute("classNotFound", Boolean.toString(classNotFound));
        if (classNotFound) {
            Element errorElement = document.createElement("error");
            classElement.appendChild(errorElement);
            Text errorText = document.createTextNode(error);
            errorElement.appendChild(errorText);
        } else {
            // Class found ! Add details (if any)

            // Search if the classes was loaded from the module, from the application or from the system
            String type = "Module";
            ClassLoader classClassLoader = clazz.getClassLoader();

            // Add where the class has been found
           classElement.setAttribute("where", "System");


           // ClassLoader info (if any)
           if (classClassLoader != null) {
               Element classLoaderElement = document.createElement("class-loader");
               classElement.appendChild(classLoaderElement);
               classLoaderElement.setAttribute("name", classClassLoader.getClass().getName());
               Text classLoaderText = document.createTextNode(classClassLoader.toString());
               classLoaderElement.appendChild(classLoaderText);
            }
        }

        StringWriter stringWriter = new StringWriter();
        StreamResult streamResult = new StreamResult(stringWriter);

        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer;
        try {
            transformer = transformerFactory.newTransformer();
        } catch (TransformerConfigurationException e) {
            throw new IllegalStateException("Unable to get a new transformer", e);
        }

        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

        // transform OUTPUT
        try {
            transformer.transform(new DOMSource(document), streamResult);
        } catch (TransformerException e) {
            throw new IllegalStateException("Unable to transform the document", e);
        }

        return stringWriter.toString();
    }

    /**
     * Gets the system filters of classloaders.
     * @return the filters for the system classloader
     * @throws MBeanException if unable to get the system filters
     */
    public String[] getClassLoaderFilters() throws MBeanException {

        // Gets the system classloader
        ClassLoader systemClassLoader = null;
        try {
            systemClassLoader = LoaderManager.getInstance().getExternalLoader();
        } catch (Exception e) {
            throw new MBeanException(e, "Unable to get the external classloader");
        }

        // Now, get the filters (should be parent classloader)
        ClassLoader parent = systemClassLoader.getParent();
        FilteringClassLoader filteringClassLoader = null;
        if (parent instanceof FilteringClassLoader) {
            filteringClassLoader = (FilteringClassLoader) parent;
        } else {
            throw new IllegalStateException("Unable to get the filtering classloader as parent classloader. Found instead : '"
                    + parent + "'.");
        }

        List<String> filters = filteringClassLoader.getFilters();

        return filters.toArray(new String[filters.size()]);
    }

    /**
     * Gets all the URL to the given resource.
     * @param resourceName the name of the resource
     * @return the list of url, if any
     * @throws MBeanException if unable to get the resource name
     */
    public URL[] getResources(final String resourceName) throws MBeanException {

        // Gets the system classloader
        ClassLoader systemClassLoader = null;
        try {
            systemClassLoader = LoaderManager.getInstance().getExternalLoader();
        } catch (Exception e) {
            throw new MBeanException(e, "Unable to get the external classloader");
        }

        Enumeration<URL> urls = null;
        try {
            urls = systemClassLoader.getResources(resourceName);
        } catch (IOException e) {
            throw new MBeanException(e, "Unable to get the resource '" + resourceName + "'.");
        }


        List<URL> urlsList = new ArrayList<URL>();
        while (urls.hasMoreElements()) {
            urlsList.add(urls.nextElement());
        }

        return urlsList.toArray(new URL[urlsList.size()]);
    }




    /**
     * Helper method for writing a bundle element on a given XML element.
     * @param document the document used to create elements
     * @param elementName the element's name
     * @param bundle the given bundle
     * @return a XML element
     */
    protected static Element addBundleNode(final Document document, final String elementName, final Bundle bundle) {
        // create root element of a bundle
        Element rootElement = addNode(document, elementName, null);

        // now, write each child
        // Bundle ID
        rootElement.appendChild(addNode(document, "bundle-id", Long.toString(bundle.getBundleId())));

        // Symbolic name
        rootElement.appendChild(addNode(document, "symbolic-name", bundle.getSymbolicName()));

        // Location
        rootElement.appendChild(addNode(document, "location", bundle.getLocation()));

        // Version
        rootElement.appendChild(addNode(document, "version", bundle.getVersion().toString()));

        // Last Modified
        rootElement.appendChild(addNode(document, "last-modified", Long.toString(bundle.getLastModified())));

        // State
        rootElement.appendChild(addNode(document, "state", Integer.toString(bundle.getState())));

        return rootElement;
    }

    /**
     * Helper method for writing a new element on a given XML element.
     * @param document the document used to create elements
     * @param elementName the element's name
     * @param value the element's value
     * @param attributes the given attributes to write (if any)
     * @return a XML element
     */
    protected static Element addNode(final Document document, final String elementName, final String value,
            final Map<String, String> attributes) {

        // Create a new element
        Element nodeElement = document.createElement(elementName);

        // Get attributes if any and write them
        if (attributes != null) {
            Set<Entry<String, String>> entries = attributes.entrySet();
            for (Entry<String, String> entry : entries) {
                nodeElement.setAttribute(entry.getKey(), entry.getValue());
            }
        }

        // element's value ? write it
        if (value != null) {
            Text text = document.createTextNode(value);
            nodeElement.appendChild(text);
        }

        return nodeElement;
    }

    /**
     * Helper method for writing a new element on a given XML element.
     * @param document the document used to create elements
     * @param elementName the element's name
     * @param value the element's value
     * @return a XML element
     */
    protected static Element addNode(final Document document, final String elementName, final String value) {
        return addNode(document, elementName, value, null);
    }


}
