/*
 *  @(#)SPILoader.java
 *
 * Copyright (C) 2002-2003 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Part of the GroboUtils package at:
 *  http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the 
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software. 
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 *  DEALINGS IN THE SOFTWARE.
 */
package net.sourceforge.groboutils.util.classes.v1;

import java.util.Properties;
import java.util.Enumeration;
import java.util.NoSuchElementException;

import java.net.URL;

import java.io.IOException;
import java.io.InputStream;

import org.apache.log4j.Logger;


/**
 * Loads Service Provider Interface (SPI) classes from the given classloader
 * (if any).  This will search for files in the form
 * <tt>/META-INF/services/<i>classname</i></tt>.  The discovered file will be
 * parsed using <tt>java.util.Properties</tt> parsing method, but only the
 * keys will be recognized (note that if a line contains no ':' or '=', then
 * the line will be recognized as a key with an empty-string value).  The found
 * keys will be used as class names, constructed, and returned, using the
 * <tt>ClassLoadHelper</tt> class in this package.
 * <P>
 * This is intended to follow the SPI interface spec, partially described in
 * the JDK 1.4 documentation in the package docs for <tt>java.awt.im.spi</tt>.
 * <P>
 * Note that by using a Properties instance, this class is limited to only
 * one instance of a class type per META-INF service file.  This is assumed to
 * be an adequate restriction, since in most circumstances, there should only
 * be one instance of a service provider loaded per need.
 *
 * @author     Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @since      June 28, 2002
 * @version    $Date: 2003/05/08 14:12:21 $
 */
public class SPILoader
{
    private static final Logger LOG = Logger.getLogger(
        SPILoader.class.getName() );
    
    
    /**
     * List of all resource URLs to the class's SPI files.
     */
    private Enumeration spiUrls;
    
    /**
     * Current SPI url's property (classname) keys.
     */
    private Enumeration propKeys = null;
    
    private String nextKey = null;
    
    private Class base;
    
    private ClassLoadHelper clh;
        
    
    /**
     * Use the context (thread) classloader or the system class loader.
     *
     * @param spiBase the base class that all loaded SPI classes must
     *      implement.  This is also used to define the name of the files
     *      to load from the META-INF directory.
     * @exception IOException if there is an I/O problem loading the
     *      initial set of resources.
     */
    public SPILoader( Class spiBase )
            throws IOException
    {
        this( spiBase, null );
    }
    
    
    /**
     * Create a new SPILoader, loading the service files from the given
     * class loader classpath.  If <tt>cl</tt> is <tt>null</tt>, then the
     * context (thread) class loader or system class loader will be used
     * instead.
     *
     * @param spiBase the base class that all loaded SPI classes must
     *      implement.  This is also used to define the name of the files
     *      to load from the META-INF directory.
     * @param cl classloader to load files from.
     * @exception IOException if there is an I/O problem loading the
     *      initial set of resources.
     */
    public SPILoader( Class spiBase, ClassLoader cl )
            throws IOException
    {
        if (spiBase == null)
        {
            throw new IllegalArgumentException("spiBase cannot be null.");
        }
        
        
        if (cl == null)
        {
            this.clh = new ClassLoadHelper( spiBase );
        }
        else
        {
            this.clh = new ClassLoadHelper( cl );
        }
        
        String metaName = "/META-INF/services/" + spiBase.getName();
        
        this.spiUrls = this.clh.getResources( metaName );
        if (this.spiUrls == null)
        {
            throw new IOException("No URLs were discovered.");
        }
        this.base = spiBase;
    }
    
    
    /**
     * Discovers if there is another provider class instance to load.
     *
     * @return <tt>true</tt> if there is another provider, otherwise
     *      <tt>false</tt>.
     */
    public boolean hasNext()
            throws IOException
    {
        while (this.nextKey == null)
        {
            if (this.propKeys == null || !this.propKeys.hasMoreElements())
            {
                if (!this.spiUrls.hasMoreElements())
                {
                    LOG.debug("No more URLs to process");
                    return false;
                }
                URL url = (URL)this.spiUrls.nextElement();
                LOG.debug( "Processing next URL: "+url );
                Properties props = new Properties();
                LOG.debug( "Opening URL stream" );
                InputStream is = url.openStream();
                try
                {
                    props.load( is );
                }
                finally
                {
                    is.close();
                }
                this.propKeys = props.keys();
                LOG.debug( "URL contains "+props.size()+" class names" );
            }
            else
            {
                this.nextKey = (String)this.propKeys.nextElement();
                LOG.debug( "Next classname is "+this.nextKey );
            }
        }
        return true;
    }
    
    
    /**
     * Returns a new instance of the next class name.  If the defined
     * next class is invalid, then an IOException is thrown.  The returned
     * object is guaranteed to be non-null and an instance of the constructor
     * baseClass.
     *
     * @return the next provider in the list of discovered SPI providers.
     * @exception IOException if there was an I/O error, or if the referenced
     *      resource defines an invalid class to load.
     * @exception NoSuchElementException if the enumeration is already at
     *      the end of the list.
     */
    public Object nextProvier()
            throws IOException
    {
        // ensure we have a next key to go to
        hasNext();
        
        if (this.nextKey == null)
        {
            // end of list
            throw new NoSuchElementException( "end of list" );
        }
        String cn = this.nextKey;
        
        // ensure we advance to the next key next iteration.
        this.nextKey = null;
        
        Object o;
        try
        {
            o = this.clh.createObject( cn );
        }
        catch (IllegalStateException ise)
        {
            LOG.info( "create object for type "+cn+" threw exception.", ise );
            throw new IOException( ise.getMessage() );
        }
        
        if (o == null)
        {
            LOG.info( "create object for type "+cn+" returned null." );
            throw new IOException( "Could not create an instance of type "+
                cn );
        }
        
        if (!this.base.isInstance( o ))
        {
            throw new IOException( "SPI defined class "+cn+
                ", but expected class of type "+this.base.getName() );
        }
        return o;
    }
}

