/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: JDBCDriverBundleTrackerCustomizer.java 21566 2011-08-08 12:28:12Z cazauxj $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.dbm.internal.osgi;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.sql.Driver;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.jdbc.DataSourceFactory;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.osgi.util.tracker.ServiceTracker;
import org.ow2.jonas.tm.TransactionService;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * This bundle tracker will track new started bundles on the gateway and check
 * if the META-INF/services/java.sql.Driver entry is available.<br />
 * It it is available, it will create JDBC DataSourceFactory for this driver if
 * there is no previous factory registered for this driver.<br />
 * This is part of the JDBC Service specification of OSGi platform r4.2
 * @author Florent Benoit
 */
public class JDBCDriverBundleTrackerCustomizer implements BundleTrackerCustomizer {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(JDBCDriverBundleTrackerCustomizer.class);

    /**
     * Driver Entry.
     */
    private static final String SQL_DRIVER_ENTRY = "/META-INF/services/java.sql.Driver";


    private BundleContext bundleContext = null;
    private TransactionService txService = null;

    public JDBCDriverBundleTrackerCustomizer(final BundleContext bundleContext, final TransactionService txService) {
        this.bundleContext = bundleContext;
        this.txService = txService;
    }

    /**
     * A bundle is being added to the BundleTracker. This method is called
     * before a bundle which matched the search parameters of the BundleTracker
     * is added to the BundleTracker. This method should return the object to be
     * tracked for the specified Bundle. The returned object is stored in the
     * BundleTracker and is available from the getObject method.
     * @param bundle The Bundle being added to the BundleTracker.
     * @param event The bundle event which caused this customizer method to be
     *        called or null if there is no bundle event associated with the
     *        call to this method.
     * @return The object to be tracked for the specified Bundle object or null
     *         if the specified Bundle object should not be tracked.
     */
    @Override
    public Object addingBundle(final Bundle bundle, final BundleEvent event) {
        return register(bundle, null);
    }

    /**
     * A bundle tracked by the BundleTracker has been modified. This method is
     * called when a bundle being tracked by the BundleTracker has had its state
     * modified.
     * @param bundle The Bundle whose state has been modified.
     * @param event The bundle event which caused this customizer method to be
     *        called or null if there is no bundle event associated with the
     *        call to this method.
     * @param object The tracked object for the specified bundle.
     */
    @Override
    @SuppressWarnings("unchecked")
    public void modifiedBundle(final Bundle bundle, final BundleEvent event, final Object object) {
        logger.debug("modifiedBundle Bundle ''{0}'' with event ''{1}''", bundle, event);
        if (bundle.getState() == Bundle.STOPPING || bundle.getState() == Bundle.ACTIVE) {
            unregister(bundle, (List<ServiceRegistration>) object);
        }
        if (bundle.getState() == Bundle.ACTIVE) {
            register(bundle, (List<ServiceRegistration>) object);
        }

    }

    /**
     * A bundle tracked by the BundleTracker has been removed. This method is
     * called after a bundle is no longer being tracked by the BundleTracker.
     * @param bundle The Bundle that has been removed.
     * @param event The bundle event which caused this customizer method to be
     *        called or null if there is no bundle event associated with the
     *        call to this method.
     * @param object The tracked object for the specified bundle.
     */
    @SuppressWarnings("unchecked")
    @Override
    public void removedBundle(final Bundle bundle, final BundleEvent event, final Object object) {
        unregister(bundle,  (List<ServiceRegistration>) object);
    }

    /**
     * Register the new services for the given bundle if the bundle is providing drivers.
     * @param bundle the bundle to analyze
     * @param existingServiceRegistrations the list of the existing or previous registrations
     * @return the updated list of service registrations
     */
    public List<ServiceRegistration> register(final Bundle bundle, final List<ServiceRegistration> existingServiceRegistrations) {
     // Search entry
        URL urlSqlDriver = bundle.getEntry(SQL_DRIVER_ENTRY);
        if (urlSqlDriver == null) {
            // Not found, return null;
            return null;
        }

        // Entry has been found
        // Now, read the value of the SQL drivers by analyzing the URL
        URLConnection urlConnection;
        try {
            urlConnection = urlSqlDriver.openConnection();
        } catch (IOException e) {
            logger.error("Unable to analyze the entry ''{0}'' in bundle ''{1}'' ''{2}''", SQL_DRIVER_ENTRY, bundle
                    .getBundleId(), bundle.getSymbolicName(), e);
            return null;
        }
        // disable cache
        urlConnection.setDefaultUseCaches(false);
        InputStream inputStream;
        try {
            inputStream = urlConnection.getInputStream();
        } catch (IOException e) {
            logger.error("Unable to analyze the entry ''{0}'' in bundle ''{1}'' ''{2}''", SQL_DRIVER_ENTRY, bundle
                    .getBundleId(), bundle.getSymbolicName(), e);
            return null;
        }

        BufferedReader reader = null;
        List<String> driverClassNames = new ArrayList<String>();
        // For each line of the file, add the driver class name to the list
        try {
            reader = new BufferedReader(new InputStreamReader(inputStream));
            String driverClassName = null;
            while ((driverClassName = reader.readLine()) != null) {
                driverClassNames.add(driverClassName);
                logger.debug("Found driver classname ''{0}'' in bundle ID ''{1}'' with symbolic name ''{2}''.", driverClassName,
                        bundle.getBundleId(), bundle.getSymbolicName());
            }
        } catch (IOException e) {
            logger.error("Unable to analyze the entry ''{0}'' in bundle ''{1}'' ''{2}''", SQL_DRIVER_ENTRY, bundle
                    .getBundleId(), bundle.getSymbolicName(), e);
            return null;
        } finally {
            // close opened resources
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    logger.debug("Unable to close resource", e);
                }
                reader = null;
            }
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    logger.debug("Unable to close stream", e);
                }
                inputStream = null;
            }
        }

        // if we don't have drivers, abort
        if (driverClassNames.size() == 0) {
            return null;
        }

        // DataSource Factories to create because there are not yet registered
        List<String> driverFactoriesToCreate = new ArrayList<String>();

        // Now check for each driver if there is an existing DataSourceFactory
        for (String driverClassName : driverClassNames) {
            Filter filter = null;
            try {
                filter = bundleContext.createFilter("(&(objectClass=" + DataSourceFactory.class.getName() + ")("
                        + DataSourceFactory.class.getCanonicalName() + "=" + driverClassName + "))");
            } catch (InvalidSyntaxException e) {
                logger.error("Unable to create a filter for the driver classname ''{0}'', skipping registration",
                        driverClassName, e);
                continue;
            }
            ServiceTracker tracker = new ServiceTracker(bundleContext, filter, null);
            tracker.open();

            // If there is at least one service registered we don't need to
            // create a factory for this driver
            DataSourceFactory datasourceFactory = (DataSourceFactory) tracker.getService();
            // If a service is already existing, cancel registration and go to
            // the next one
            if (datasourceFactory != null) {
                continue;
            } else {
                // need to create a factory for this driver
                driverFactoriesToCreate.add(driverClassName);
            }
        }

        // registrations that are made
        List<ServiceRegistration> serviceRegistrations = null;
        if (existingServiceRegistrations == null) {
            serviceRegistrations = new ArrayList<ServiceRegistration>();
        } else {
            serviceRegistrations = existingServiceRegistrations;
        }

        // For each driver, create a factory and register it
        for (String driverClassName : driverFactoriesToCreate) {

            // Load the class of the driver
            Class<?> driverClass = null;
            try {
                driverClass = bundle.loadClass(driverClassName);
            } catch (ClassNotFoundException e) {
                logger.error("No JDBC DS Factory as driver class ''{0}'' can't be loaded from ''{1}'' ''{2}''",
                        driverClassName, bundle.getBundleId(), bundle.getSymbolicName());
                return null;
            }

            Driver driver;
            try {
                driver = (Driver) driverClass.newInstance();
            } catch (InstantiationException e) {
                logger.error("Aborting JDBC DataSource Factory registration as driver class ''{0}'' can't be loaded.",
                        driverClass);
                return null;
            } catch (IllegalAccessException e) {
                logger.error("Aborting JDBC DataSource Factory registration as driver class ''{0}'' can't be loaded.",
                        driverClass);
                return null;
            }
            int majorVersion = driver.getMajorVersion();
            int minorVersion = driver.getMinorVersion();

            DataSourceFactory dataSourceFactory = new JOnASDataSourceFactory(driverClass, txService);

            // Add properties for the registration
            Properties properties = new Properties();
            properties.put(DataSourceFactory.OSGI_JDBC_DRIVER_CLASS, driverClassName);
            properties.put(DataSourceFactory.OSGI_JDBC_DRIVER_NAME, driverClassName);
            properties.put(DataSourceFactory.OSGI_JDBC_DRIVER_VERSION, majorVersion + "." + minorVersion);

            ServiceRegistration serviceRegistration = bundleContext.registerService(new String[] {DataSourceFactory.class
                    .getName()}, dataSourceFactory, properties);
            serviceRegistrations.add(serviceRegistration);
            logger.info("Registering OSGi DataSource Factory for the driver ''{0}'' v{1}.{2}", driverClass.getName(),
                    majorVersion, minorVersion);
        }

        // return the list of the registrations that have been made
        return serviceRegistrations;
    }

    /**
     * Unregister the given service registrations for the given bundle.
     * @param bundle the bundle that is stopping
     * @param serviceRegistrations the list of services to unregister
     */
    public void unregister(final Bundle bundle, final List<ServiceRegistration> serviceRegistrations) {
        // remove all the registrations
        if (serviceRegistrations != null) {
            for (ServiceRegistration serviceRegistration : serviceRegistrations) {
                serviceRegistration.unregister();
            }
        }

    }

}
