/*
 * $Header: /cvshome/build/org.osgi.util.xml/src/org/osgi/util/xml/XMLParserActivator.java,v 1.10 2006/06/21 17:41:20 hargrave Exp $
 * 
 * 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) {
	}
}
