/**
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 * OW2 Specifications
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: easybeans@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: OW2ProviderRegistry.java 6339 2013-01-28 13:59:36Z albertil $
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 */
package org.ow2.spec.ee.osgi.registry.internal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Instantiate;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Validate;
import org.apache.geronimo.osgi.registry.api.ProviderRegistry;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.util.tracker.BundleTracker;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * <p>This class aims to replace the default Geronimo's {@link ProviderRegistry} as its behavior doesn't fit our needs. The main
 * differences between our implementation and Geronimo's one are:
 * <ul>
 * <li>Look at META-INF/services/* even if bundle do not have an header manifest called "SPI-Provider"</li>
 * <li>Do not take care of OSGI-INF/services/* (for now anywise)</li>
 * <li>Do not take care of "Export-SPI-Provider" bundle header</li>
 * </ul>
 *
 * </p>
 *
 * @author Loic Albertin
 */
@Component(immediate = true)
@Instantiate
@Provides
public class OW2ProviderRegistry implements ProviderRegistry {

    private static Log log = LogFactory.getLog(OW2ProviderRegistry.class);

    private ProviderBundleTracker bundleTracker;

    private Map<String, Set<SPIProvider>> providerIdToSPIProvider;

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public OW2ProviderRegistry(BundleContext context) {
        bundleTracker = new ProviderBundleTracker(context, Bundle.ACTIVE, null);
        providerIdToSPIProvider = new HashMap<String, Set<SPIProvider>>();
    }

    @Validate
    private void startup() {
        bundleTracker.open();
    }

    @Invalidate
    private void shutdown() {
        bundleTracker.close();
    }

    public Class<?> locate(String factoryId) {
        lock.readLock().lock();
        try {
            for (Set<SPIProvider> spiProviders : providerIdToSPIProvider.values()) {
                for (SPIProvider spiProvider : spiProviders) {
                    if (spiProvider.getFactoryId().equals(factoryId)) {
                        try {
                            return spiProvider.loadFactory();
                        } catch (ClassNotFoundException e) {
                            log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                    spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                            // Lets try another one
                        }
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return null;
    }

    public List<Class<?>> locateAll(String factoryId) {
        lock.readLock().lock();
        List<Class<?>> classes;
        try {
            classes = new ArrayList<Class<?>>();
            for (Set<SPIProvider> spiProviders : providerIdToSPIProvider.values()) {
                for (SPIProvider spiProvider : spiProviders) {
                    if (spiProvider.getFactoryId().equals(factoryId)) {
                        try {
                            classes.add(spiProvider.loadFactory());
                        } catch (ClassNotFoundException e) {
                            log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                    spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                            // Lets try another one
                        }
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return classes;
    }

    public Object getService(String providerId) throws Exception {
        lock.readLock().lock();
        try {
            if (providerIdToSPIProvider.containsKey(providerId)) {
                Set<SPIProvider> spiProviders = providerIdToSPIProvider.get(providerId);
                for (SPIProvider spiProvider : spiProviders) {
                    try {
                        return spiProvider.createFactoryInstance();
                    } catch (ClassNotFoundException e) {
                        log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    } catch (IllegalAccessException e) {
                        log.warn("Could not instantiate SPI provider '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    } catch (InstantiationException e) {
                        log.warn("Could not instantiate SPI provider '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return null;
    }

    public List<Object> getServices(String providerId) {
        lock.readLock().lock();
        List<Object> instances;
        try {
            instances = new LinkedList<Object>();
            if (providerIdToSPIProvider.containsKey(providerId)) {
                Set<SPIProvider> spiProviders = providerIdToSPIProvider.get(providerId);
                for (SPIProvider spiProvider : spiProviders) {
                    try {
                        instances.add(spiProvider.loadFactory());
                    } catch (ClassNotFoundException e) {
                        log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return instances;
    }

    public Class<?> getServiceClass(String providerId) throws ClassNotFoundException {
        lock.readLock().lock();
        try {
            if (providerIdToSPIProvider.containsKey(providerId)) {
                Set<SPIProvider> spiProviders = providerIdToSPIProvider.get(providerId);
                for (SPIProvider spiProvider : spiProviders) {
                    try {
                        return spiProvider.loadFactory();
                    } catch (ClassNotFoundException e) {
                        log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return null;
    }

    public List<Class<?>> getServiceClasses(String providerId) {
        lock.readLock().lock();
        List<Class<?>> classes;
        try {
            classes = new LinkedList<Class<?>>();
            if (providerIdToSPIProvider.containsKey(providerId)) {
                Set<SPIProvider> spiProviders = providerIdToSPIProvider.get(providerId);
                for (SPIProvider spiProvider : spiProviders) {
                    try {
                        classes.add(spiProvider.loadFactory());
                    } catch (ClassNotFoundException e) {
                        log.warn("Could not load SPI provider class '{0}' for SPI factory '{1}' through bundle '{2}'.",
                                spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getBundleId(), e);
                        // Lets try another one if any
                    }
                }
            }
        } finally {
            lock.readLock().unlock();
        }
        return classes;
    }

    /**
     * This {@link BundleTracker} tracks active bundles arrivals and departures. It inspects theirs META-INF/services directory and lookup
     * for available SPI declarations. It aims to maintain the Provider Registry consistency.
     */
    final class ProviderBundleTracker extends BundleTracker<Set<SPIProvider>> {

        public ProviderBundleTracker(BundleContext context, int stateMask,
                BundleTrackerCustomizer<Set<SPIProvider>> customizer) {
            super(context, stateMask, customizer);
        }

        @Override
        public Set<SPIProvider> addingBundle(Bundle bundle, BundleEvent event) {
            Set<SPIProvider> spiProviders = new HashSet<SPIProvider>();
            Enumeration<URL> entries = bundle.findEntries("META-INF/services", "*", false);
            if (entries != null) {
                while (entries.hasMoreElements()) {
                    URL url = entries.nextElement();
                    String entry = url.toString().trim();
                    if (entry.endsWith("/")) {
                        // ignore directories
                        continue;
                    }
                    String providerId = entry.substring(entry.lastIndexOf("/") + 1);
                    try {
                        BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
                        String line = br.readLine();
                        while (line != null) {
                            // Skip comments
                            if (line.contains("#")) {
                                line = line.substring(0, line.indexOf("#"));
                            }
                            line = line.trim();
                            if (!line.equals("")) {
                                SPIProvider spiProvider = new SPIProvider(bundle, providerId, line);
                                spiProviders.add(spiProvider);
                                lock.writeLock().lock();
                                Set<SPIProvider> existingProviders;
                                try {
                                    existingProviders = providerIdToSPIProvider.get(providerId);
                                    if (existingProviders == null) {
                                        existingProviders = new HashSet<SPIProvider>();
                                        providerIdToSPIProvider.put(providerId, existingProviders);
                                    }
                                    existingProviders.add(spiProvider);
                                    log.debug("registering factory ''{0}'' for providerId ''{1}'' within bundle {2}",
                                            spiProvider.getFactoryId(), spiProvider.getProviderId(),
                                            spiProvider.getBundle().getSymbolicName());
                                } finally {
                                    lock.writeLock().unlock();
                                }
                            }
                            line = br.readLine();
                        }
                    } catch (IOException e) {
                        log.warn("Unable to read SPI file {0} for bundle {1}", entry, bundle.getBundleId(), e);
                    }
                }
            }
            return spiProviders;
        }

        @Override
        public void modifiedBundle(Bundle bundle, BundleEvent event, Set<SPIProvider> object) {
            // Recompute providers
            removedBundle(bundle, event, object);
            object.clear();
            object.addAll(addingBundle(bundle, event));
        }

        @Override
        public void removedBundle(Bundle bundle, BundleEvent event, Set<SPIProvider> object) {
            for (SPIProvider spiProvider : object) {
                log.debug("unregistering factory ''{0}'' for providerId ''{1}'' within bundle {2}",
                        spiProvider.getFactoryId(), spiProvider.getProviderId(), spiProvider.getBundle().getSymbolicName());
                lock.writeLock().lock();
                try {
                    Set<SPIProvider> spiProviders = providerIdToSPIProvider.get(spiProvider.getProviderId());
                    spiProviders.remove(spiProvider);
                } finally {
                    lock.writeLock().unlock();
                }
            }
        }


    }

    /**
     * An SPIProvider holds information about an SPI provider. Basically an SPIProvider is composed by 3 main items :
     * <ul>
     * <li>The bundle that had declared an SPI factory, this bundle is used to load the factory Class</li>
     * <li>The declared SPI provider identifier</li>
     * <li>The declared SPI factory identifier</li>
     * </ul>
     */
    final class SPIProvider {
        private Bundle bundle;
        private String providerId;
        private String factoryId;

        /**
         * Default constructor
         *
         * @param bundle     The {@link Bundle} that provides the factory.
         * @param providerId The provider identifier
         * @param factoryId  The factory identifier
         */
        private SPIProvider(Bundle bundle, String providerId, String factoryId) {
            this.bundle = bundle;
            this.providerId = providerId;
            this.factoryId = factoryId;
        }

        public Bundle getBundle() {
            return bundle;
        }

        public String getProviderId() {
            return providerId;
        }

        public String getFactoryId() {
            return factoryId;
        }

        /**
         * Loads the declared factory using its {@link Bundle}
         *
         * @return The actual {@link Class} of the declared factory
         *
         * @throws ClassNotFoundException If the {@link Bundle} can't load the {@link Class}
         */
        public Class<?> loadFactory() throws ClassNotFoundException {
            return bundle.loadClass(factoryId);
        }

        /**
         * Create a new instance of the declared factory
         *
         * @return An instance of the declared factory
         *
         * @throws ClassNotFoundException If the {@link Bundle} can't load the {@link Class}
         * @throws IllegalAccessException If the class or its nullary constructor is not accessible.
         * @throws InstantiationException If this Class represents an abstract class, an interface, an array class, a primitive type, or
         *                                void; or if the class has no nullary constructor; or if the instantiation fails for some other
         *                                reason.
         * @see org.ow2.spec.ee.osgi.registry.internal.OW2ProviderRegistry.SPIProvider#loadFactory()
         */
        public Object createFactoryInstance() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            return this.loadFactory().newInstance();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            SPIProvider that = (SPIProvider) o;

            if (bundle != null ? !bundle.equals(that.bundle) : that.bundle != null) {
                return false;
            }
            if (factoryId != null ? !factoryId.equals(that.factoryId) : that.factoryId != null) {
                return false;
            }
            if (providerId != null ? !providerId.equals(that.providerId) : that.providerId != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = bundle != null ? bundle.hashCode() : 0;
            result = 31 * result + (providerId != null ? providerId.hashCode() : 0);
            result = 31 * result + (factoryId != null ? factoryId.hashCode() : 0);
            return result;
        }
    }

}
