/*
 *
 * Copyright (c) OSGi Alliance (2002, 2006). All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.osgi.util.xml;

import java.io.*;
import java.net.URL;
import java.util.*;

import javax.xml.parsers.*;

import org.osgi.framework.*;

/**
 * A BundleActivator class that allows any JAXP compliant XML Parser to register
 * itself as an OSGi parser service.
 *
 * Multiple JAXP compliant parsers can concurrently register by using this
 * BundleActivator class. Bundles who wish to use an XML parser can then use the
 * framework's service registry to locate available XML Parsers with the desired
 * characteristics such as validating and namespace-aware.
 *
 * <p>
 * The services that this bundle activator enables a bundle to provide are:
 * <ul>
 * <li><code>javax.xml.parsers.SAXParserFactory</code>({@link #SAXFACTORYNAME})
 * <li><code>javax.xml.parsers.DocumentBuilderFactory</code>(
 * {@link #DOMFACTORYNAME})
 * </ul>
 *
 * <p>
 * The algorithm to find the implementations of the abstract parsers is derived
 * from the JAR file specifications, specifically the Services API.
 * <p>
 * An XMLParserActivator assumes that it can find the class file names of the
 * factory classes in the following files:
 * <ul>
 * <li><code>/META-INF/services/javax.xml.parsers.SAXParserFactory</code> is
 * a file contained in a jar available to the runtime which contains the
 * implementation class name(s) of the SAXParserFactory.
 * <li><code>/META-INF/services/javax.xml.parsers.DocumentBuilderFactory</code>
 * is a file contained in a jar available to the runtime which contains the
 * implementation class name(s) of the <code>DocumentBuilderFactory</code>
 * </ul>
 * <p>
 * If either of the files does not exist, <code>XMLParserActivator</code>
 * assumes that the parser does not support that parser type.
 *
 * <p>
 * <code>XMLParserActivator</code> attempts to instantiate both the
 * <code>SAXParserFactory</code> and the <code>DocumentBuilderFactory</code>.
 * It registers each factory with the framework along with service properties:
 * <ul>
 * <li>{@link #PARSER_VALIDATING}- indicates if this factory supports
 * validating parsers. It's value is a <code>Boolean</code>.
 * <li>{@link #PARSER_NAMESPACEAWARE}- indicates if this factory supports
 * namespace aware parsers It's value is a <code>Boolean</code>.
 * </ul>
 * <p>
 * Individual parser implementations may have additional features, properties,
 * or attributes which could be used to select a parser with a filter. These can
 * be added by extending this class and overriding the
 * <code>setSAXProperties</code> and <code>setDOMProperties</code> methods.
 */
public class XMLParserActivator implements BundleActivator, ServiceFactory {
        /** Context of this bundle */
        private BundleContext           context;
        /**
         * Filename containing the SAX Parser Factory Class name. Also used as the
         * basis for the <code>SERVICE_PID<code> registration property.
         */
        public static final String      SAXFACTORYNAME                  = "javax.xml.parsers.SAXParserFactory";
        /**
         * Filename containing the DOM Parser Factory Class name. Also used as the
         * basis for the <code>SERVICE_PID</code> registration property.
         */
        public static final String      DOMFACTORYNAME                  = "javax.xml.parsers.DocumentBuilderFactory";
        /** Path to the factory class name files */
        private static final String     PARSERCLASSFILEPATH             = "/META-INF/services/";
        /** Fully qualified path name of SAX Parser Factory Class Name file */
        public static final String      SAXCLASSFILE                    = PARSERCLASSFILEPATH + SAXFACTORYNAME;
        /** Fully qualified path name of DOM Parser Factory Class Name file */
        public static final String      DOMCLASSFILE                    = PARSERCLASSFILEPATH + DOMFACTORYNAME;
        /** SAX Factory Service Description */
        private static final String     SAXFACTORYDESCRIPTION   = "A JAXP Compliant SAX Parser";
        /** DOM Factory Service Description */
        private static final String     DOMFACTORYDESCRIPTION   = "A JAXP Compliant DOM Parser";
        /**
         * Service property specifying if factory is configured to support
         * validating parsers. The value is of type <code>Boolean</code>.
         */
        public static final String      PARSER_VALIDATING               = "parser.validating";
        /**
         * Service property specifying if factory is configured to support namespace
         * aware parsers. The value is of type <code>Boolean</code>.
         */
        public static final String      PARSER_NAMESPACEAWARE   = "parser.namespaceAware";
        /**
         * Key for parser factory name property - this must be saved in the parsers
         * properties hashtable so that the parser factory can be instantiated from
         * a ServiceReference
         */
        private static final String     FACTORYNAMEKEY                  = "parser.factoryname";

        /**
         * Called when this bundle is started so the Framework can perform the
         * bundle-specific activities necessary to start this bundle. This method
         * can be used to register services or to allocate any resources that this
         * bundle needs.
         *
         * <p>
         * This method must complete and return to its caller in a timely manner.
         *
         * <p>
         * This method attempts to register a SAX and DOM parser with the
         * Framework's service registry.
         *
         * @param context The execution context of the bundle being started.
         * @throws java.lang.Exception If this method throws an exception, this
         *         bundle is marked as stopped and the Framework will remove this
         *         bundle's listeners, unregister all services registered by this
         *         bundle, and release all services used by this bundle.
         * @see Bundle#start
         */
        public void start(BundleContext context) throws Exception {
                this.context = context;
                Bundle parserBundle = context.getBundle();
                try {
                        // check for sax parsers
                        registerSAXParsers(getParserFactoryClassNames(parserBundle
                                        .getResource(SAXCLASSFILE)));
                        // check for dom parsers
                        registerDOMParsers(getParserFactoryClassNames(parserBundle
                                        .getResource(DOMCLASSFILE)));
                }
                catch (IOException ioe) {
                        // if there were any IO errors accessing the resource files
                        // containing the class names
                        ioe.printStackTrace();
                        throw new FactoryConfigurationError(ioe);
                }
        }

        /**
         * <p>
         * This method has nothing to do as all active service registrations will
         * automatically get unregistered when the bundle stops.
         *
         * @param context The execution context of the bundle being stopped.
         * @throws java.lang.Exception If this method throws an exception, the
         *         bundle is still marked as stopped, and the Framework will remove
         *         the bundle's listeners, unregister all services registered by the
         *         bundle, and release all services used by the bundle.
         * @see Bundle#stop
         */
        public void stop(BundleContext context) throws Exception {
        }

        /**
         * Given the URL for a file, reads and returns the parser class names. There
         * may be multiple classes specified in this file, one per line. There may
         * also be comment lines in the file, which begin with "#".
         *
         * @param parserUrl The URL of the service file containing the parser class
         *        names
         * @return A vector of strings containing the parser class names or null if
         *         parserUrl is null
         * @throws IOException if there is a problem reading the URL input stream
         */
        private Vector getParserFactoryClassNames(URL parserUrl) throws IOException {
                Vector v = new Vector(1);
                if (parserUrl != null) {
                        String parserFactoryClassName = null;
                        InputStream is = parserUrl.openStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));
                        while (true) {
                                parserFactoryClassName = br.readLine();
                                if (parserFactoryClassName == null) {
                                        break; // end of file reached
                                }
                                String pfcName = parserFactoryClassName.trim();
                                if (pfcName.length() == 0) {
                                        continue; // blank line
                                }
                                int commentIdx = pfcName.indexOf("#");
                                if (commentIdx == 0) { // comment line
                                        continue;
                                }
                                else
                                        if (commentIdx < 0) { // no comment on this line
                                                v.addElement(pfcName);
                                        }
                                        else {
                                                v.addElement(pfcName.substring(0, commentIdx).trim());
                                        }
                        }
                        return v;
                }
                else {
                        return null;
                }
        }

        /**
         * Register SAX Parser Factory Services with the framework.
         *
         * @param parserFactoryClassNames - a <code>Vector</code> of
         *        <code>String</code> objects containing the names of the parser
         *        Factory Classes
         * @throws FactoryConfigurationError if thrown from <code>getFactory</code>
         */
        private void registerSAXParsers(Vector parserFactoryClassNames)
                        throws FactoryConfigurationError {
                if (parserFactoryClassNames != null) {
                        Enumeration e = parserFactoryClassNames.elements();
                        int index = 0;
                        while (e.hasMoreElements()) {
                                String parserFactoryClassName = (String) e.nextElement();
                                // create a sax parser factory just to get it's default
                                // properties. It will never be used since
                                // this class will operate as a service factory and give each
                                // service requestor it's own SaxParserFactory
                                SAXParserFactory factory = (SAXParserFactory) getFactory(parserFactoryClassName);
                                Hashtable properties = new Hashtable(7);
                                // figure out the default properties of the parser
                                setDefaultSAXProperties(factory, properties, index);
                                // store the parser factory class name in the properties so that
                                // it can be retrieved when getService is called
                                // to return a parser factory
                                properties.put(FACTORYNAMEKEY, parserFactoryClassName);
                                // release the factory
                                factory = null;
                                // register the factory as a service
                                context.registerService(SAXFACTORYNAME, this, properties);
                                index++;
                        }
                }
        }

        /**
         * <p>
         * Set the SAX Parser Service Properties. By default, the following
         * properties are set:
         * <ul>
         * <li><code>SERVICE_DESCRIPTION</code>
         * <li><code>SERVICE_PID</code>
         * <li><code>PARSER_VALIDATING</code>- instantiates a parser and queries
         * it to find out whether it is validating or not
         * <li><code>PARSER_NAMESPACEAWARE</code>- instantiates a parser and
         * queries it to find out whether it is namespace aware or not
         * <ul>
         *
         * @param factory The <code>SAXParserFactory</code> object
         * @param props <code>Hashtable</code> of service properties.
         */
        private void setDefaultSAXProperties(SAXParserFactory factory,
                        Hashtable props, int index) {
                props.put(Constants.SERVICE_DESCRIPTION, SAXFACTORYDESCRIPTION);
                props.put(Constants.SERVICE_PID, SAXFACTORYNAME + "."
                                + context.getBundle().getBundleId() + "." + index);
                setSAXProperties(factory, props);
        }

        /**
         * <p>
         * Set the customizable SAX Parser Service Properties.
         *
         * <p>
         * This method attempts to instantiate a validating parser and a
         * namespaceaware parser to determine if the parser can support those
         * features. The appropriate properties are then set in the specified
         * properties object.
         *
         * <p>
         * This method can be overridden to add additional SAX2 features and
         * properties. If you want to be able to filter searches of the OSGi service
         * registry, this method must put a key, value pair into the properties
         * object for each feature or property. For example,
         *
         * properties.put("http://www.acme.com/features/foo", Boolean.TRUE);
         *
         * @param factory - the SAXParserFactory object
         * @param properties - the properties object for the service
         */
        public void setSAXProperties(SAXParserFactory factory, Hashtable properties) {
                // check if this parser can be configured to validate
                boolean validating = true;
                factory.setValidating(true);
                factory.setNamespaceAware(false);
                try {
                        factory.newSAXParser();
                }
                catch (Exception pce_val) {
                        validating = false;
                }
                // check if this parser can be configured to be namespaceaware
                boolean namespaceaware = true;
                factory.setValidating(false);
                factory.setNamespaceAware(true);
                try {
                        factory.newSAXParser();
                }
                catch (Exception pce_nsa) {
                        namespaceaware = false;
                }
                // set the factory values
                factory.setValidating(validating);
                factory.setNamespaceAware(namespaceaware);
                // set the OSGi service properties
                properties.put(PARSER_NAMESPACEAWARE, new Boolean(namespaceaware));
                properties.put(PARSER_VALIDATING, new Boolean(validating));
        }

        /**
         * Register DOM Parser Factory Services with the framework.
         *
         * @param parserFactoryClassNames - a <code>Vector</code> of
         *        <code>String</code> objects containing the names of the parser
         *        Factory Classes
         * @throws FactoryConfigurationError if thrown from <code>getFactory</code>
         */
        private void registerDOMParsers(Vector parserFactoryClassNames)
                        throws FactoryConfigurationError {
                if (parserFactoryClassNames != null) {
                        Enumeration e = parserFactoryClassNames.elements();
                        int index = 0;
                        while (e.hasMoreElements()) {
                                String parserFactoryClassName = (String) e.nextElement();
                                // create a dom parser factory just to get it's default
                                // properties. It will never be used since
                                // this class will operate as a service factory and give each
                                // service requestor it's own DocumentBuilderFactory
                                DocumentBuilderFactory factory = (DocumentBuilderFactory) getFactory(parserFactoryClassName);
                                Hashtable properties = new Hashtable(7);
                                // figure out the default properties of the parser
                                setDefaultDOMProperties(factory, properties, index);
                                // store the parser factory class name in the properties so that
                                // it can be retrieved when getService is called
                                // to return a parser factory
                                properties.put(FACTORYNAMEKEY, parserFactoryClassName);
                                // release the factory
                                factory = null;
                                // register the factory as a service
                                context.registerService(DOMFACTORYNAME, this, properties);
                                index++;
                        }
                }
        }

        /**
         * Set the DOM parser service properties.
         *
         * By default, the following properties are set:
         * <ul>
         * <li><code>SERVICE_DESCRIPTION</code>
         * <li><code>SERVICE_PID</code>
         * <li><code>PARSER_VALIDATING</code>
         * <li><code>PARSER_NAMESPACEAWARE</code>
         * <ul>
         *
         * @param factory The <code>DocumentBuilderFactory</code> object
         * @param props <code>Hashtable</code> of service properties.
         */
        private void setDefaultDOMProperties(DocumentBuilderFactory factory,
                        Hashtable props, int index) {
                props.put(Constants.SERVICE_DESCRIPTION, DOMFACTORYDESCRIPTION);
                props.put(Constants.SERVICE_PID, DOMFACTORYNAME + "."
                                + context.getBundle().getBundleId() + "." + index);
                setDOMProperties(factory, props);
        }

        /**
         * <p>
         * Set the customizable DOM Parser Service Properties.
         *
         * <p>
         * This method attempts to instantiate a validating parser and a
         * namespaceaware parser to determine if the parser can support those
         * features. The appropriate properties are then set in the specified props
         * object.
         *
         * <p>
         * This method can be overridden to add additional DOM2 features and
         * properties. If you want to be able to filter searches of the OSGi service
         * registry, this method must put a key, value pair into the properties
         * object for each feature or property. For example,
         *
         * properties.put("http://www.acme.com/features/foo", Boolean.TRUE);
         *
         * @param factory - the DocumentBuilderFactory object
         * @param props - Hashtable of service properties.
         */
        public void setDOMProperties(DocumentBuilderFactory factory, Hashtable props) {
                // check if this parser can be configured to validate
                boolean validating = true;
                factory.setValidating(true);
                factory.setNamespaceAware(false);
                try {
                        factory.newDocumentBuilder();
                }
                catch (Exception pce_val) {
                        validating = false;
                }
                // check if this parser can be configured to be namespaceaware
                boolean namespaceaware = true;
                factory.setValidating(false);
                factory.setNamespaceAware(true);
                try {
                        factory.newDocumentBuilder();
                }
                catch (Exception pce_nsa) {
                        namespaceaware = false;
                }
                // set the factory values
                factory.setValidating(validating);
                factory.setNamespaceAware(namespaceaware);
                // set the OSGi service properties
                props.put(PARSER_VALIDATING, new Boolean(validating));
                props.put(PARSER_NAMESPACEAWARE, new Boolean(namespaceaware));
        }

        /**
         * Given a parser factory class name, instantiate that class.
         *
         * @param parserFactoryClassName A <code>String</code> object containing
         *        the name of the parser factory class
         * @return a parserFactoryClass Object
         * @pre parserFactoryClassName!=null
         */
        private Object getFactory(String parserFactoryClassName)
                        throws FactoryConfigurationError {
                Exception e = null;
                try {
                        return Class.forName(parserFactoryClassName).newInstance();
                }
                catch (ClassNotFoundException cnfe) {
                        e = cnfe;
                }
                catch (InstantiationException ie) {
                        e = ie;
                }
                catch (IllegalAccessException iae) {
                        e = iae;
                }
                throw new FactoryConfigurationError(e);
        }

        /**
         * Creates a new XML Parser Factory object.
         *
         * <p>
         * A unique XML Parser Factory object is returned for each call to this
         * method.
         *
         * <p>
         * The returned XML Parser Factory object will be configured for validating
         * and namespace aware support as specified in the service properties of the
         * specified ServiceRegistration object.
         *
         * This method can be overridden to configure additional features in the
         * returned XML Parser Factory object.
         *
         * @param bundle The bundle using the service.
         * @param registration The <code>ServiceRegistration</code> object for the
         *        service.
         * @return A new, configured XML Parser Factory object or null if a
         *         configuration error was encountered
         */
        public Object getService(Bundle bundle, ServiceRegistration registration) {
                ServiceReference sref = registration.getReference();
                String parserFactoryClassName = (String) sref
                                .getProperty(FACTORYNAMEKEY);
                try {
                        // need to set factory properties
                        Object factory = getFactory(parserFactoryClassName);
                        if (factory instanceof SAXParserFactory) {
                                ((SAXParserFactory) factory).setValidating(((Boolean) sref
                                                .getProperty(PARSER_VALIDATING)).booleanValue());
                                ((SAXParserFactory) factory).setNamespaceAware(((Boolean) sref
                                                .getProperty(PARSER_NAMESPACEAWARE)).booleanValue());
                        }
                        else
                                if (factory instanceof DocumentBuilderFactory) {
                                        ((DocumentBuilderFactory) factory)
                                                        .setValidating(((Boolean) sref
                                                                        .getProperty(PARSER_VALIDATING))
                                                                        .booleanValue());
                                        ((DocumentBuilderFactory) factory)
                                                        .setNamespaceAware(((Boolean) sref
                                                                        .getProperty(PARSER_NAMESPACEAWARE))
                                                                        .booleanValue());
                                }
                        return factory;
                }
                catch (FactoryConfigurationError fce) {
                        fce.printStackTrace();
                        return null;
                }
        }

        /**
         * Releases a XML Parser Factory object.
         *
         * @param bundle The bundle releasing the service.
         * @param registration The <code>ServiceRegistration</code> object for the
         *        service.
         * @param service The XML Parser Factory object returned by a previous call
         *        to the <code>getService</code> method.
         */
        public void ungetService(Bundle bundle, ServiceRegistration registration,
                        Object service) {
        }
}
