/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.osgi;

import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.model.TypeContributor;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.registry.classloading.internal.AggregatedClassLoader;
import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence;
import org.hibernate.boot.registry.selector.StrategyRegistrationProvider;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.internal.CoreMessageLogger;
import org.jboss.logging.Logger;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.wiring.BundleWiring;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
 * Hibernate 4.2 and 4.3 still heavily rely on TCCL for ClassLoading.  Although
 * our ClassLoaderService removed some of the reliance, access to the proper ClassLoader
 * via TCCL is still required in a few cases where we call out to external libs.  An OSGi
 * bundle manually creating a SessionFactory would require numerous ClassLoader
 * tricks (or may be impossible altogether).
 * <p/>
 * In order to fully control the TCCL issues and shield users from the
 * knowledge, we're requiring that bundles use this OSGi ServiceFactory.  It
 * configures and provides a SessionFactory as an OSGi service.
 * <p/>
 * Note that an OSGi ServiceFactory differs from a Service.  The ServiceFactory
 * allows individual instances of Services to be created and provided to
 * multiple client Bundles.
 *
 * @author Brett Meyer
 * @author Tim Ward
 */
public class OsgiSessionFactoryService implements ServiceFactory {
	private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class,
			OsgiSessionFactoryService.class.getName());

	private OsgiJtaPlatform osgiJtaPlatform;
	private OsgiServiceUtil osgiServiceUtil;

	/**
	 * Constructs a OsgiSessionFactoryService
	 *
	 * @param osgiJtaPlatform The OSGi-specific JtaPlatform created in HibernateBundleActivator
	 * @param osgiServiceUtil Util object built in HibernateBundleActivator
	 */
	public OsgiSessionFactoryService(OsgiJtaPlatform osgiJtaPlatform, OsgiServiceUtil osgiServiceUtil) {
		this.osgiJtaPlatform = osgiJtaPlatform;
		this.osgiServiceUtil = osgiServiceUtil;
	}

	@Override
	public Object getService(Bundle requestingBundle, ServiceRegistration registration) {
		final OsgiClassLoader osgiClassLoader = new OsgiClassLoader();

		// First, add the client bundle that's requesting the OSGi services.
		osgiClassLoader.addBundle( requestingBundle );

		// Then, automatically add hibernate-core.  These are needed to load resources
		// contained in the core jar.
		osgiClassLoader.addBundle( FrameworkUtil.getBundle(SessionFactory.class) );

		// Some "boot time" code does still rely on TCCL.  "run time" code should all be using
		// ClassLoaderService now.

		final ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
		LinkedHashSet<ClassLoader> newTcclDelegates = new LinkedHashSet<>();
		newTcclDelegates.add( osgiClassLoader );
		newTcclDelegates.add( originalTccl );
		final ClassLoader newTccl = new AggregatedClassLoader(
				newTcclDelegates, TcclLookupPrecedence.NEVER
		);
		Thread.currentThread().setContextClassLoader( newTccl );
		try {
			return buildSessionFactory( requestingBundle, osgiClassLoader );
		}
		finally {
			Thread.currentThread().setContextClassLoader( originalTccl );
		}
	}

	private Object buildSessionFactory(
			Bundle requestingBundle,
			OsgiClassLoader osgiClassLoader) {
		final BootstrapServiceRegistryBuilder bsrBuilder = new BootstrapServiceRegistryBuilder();
		bsrBuilder.applyClassLoaderService( new OSGiClassLoaderServiceImpl( osgiClassLoader, osgiServiceUtil ) );

		final Integrator[] integrators = osgiServiceUtil.getServiceImpls( Integrator.class );
		for ( Integrator integrator : integrators ) {
			bsrBuilder.applyIntegrator( integrator );
		}

		final StrategyRegistrationProvider[] strategyRegistrationProviders
				= osgiServiceUtil.getServiceImpls( StrategyRegistrationProvider.class );
		for ( StrategyRegistrationProvider strategyRegistrationProvider : strategyRegistrationProviders ) {
			bsrBuilder.applyStrategySelectors( strategyRegistrationProvider );
		}

		final BootstrapServiceRegistry bsr = bsrBuilder.build();
		final StandardServiceRegistryBuilder ssrBuilder = new StandardServiceRegistryBuilder( bsr );

		// Allow bundles to put the config file somewhere other than the root level.
		final BundleWiring bundleWiring = (BundleWiring) requestingBundle.adapt( BundleWiring.class );
		final Collection<String> cfgResources = bundleWiring.listResources(
				"/",
				"hibernate.cfg.xml",
				BundleWiring.LISTRESOURCES_RECURSE
		);
		if (cfgResources.size() == 0) {
			ssrBuilder.configure();
		}
		else {
			if (cfgResources.size() > 1) {
				LOG.warn( "Multiple hibernate.cfg.xml files found in the persistence bundle.  Using the first one discovered." );
			}
			String cfgResource = "/" + cfgResources.iterator().next();
			ssrBuilder.configure( cfgResource );
		}

		ssrBuilder.applySetting( AvailableSettings.JTA_PLATFORM, osgiJtaPlatform );

		final StandardServiceRegistry ssr = ssrBuilder.build();

		final MetadataBuilder metadataBuilder = new MetadataSources( ssr ).getMetadataBuilder();
		final TypeContributor[] typeContributors = osgiServiceUtil.getServiceImpls( TypeContributor.class );
		for ( TypeContributor typeContributor : typeContributors ) {
			metadataBuilder.applyTypes( typeContributor );
		}

		return metadataBuilder.build().buildSessionFactory();
	}

	@Override
	public void ungetService(Bundle requestingBundle, ServiceRegistration registration, Object service) {
		((SessionFactory) service).close();
	}

}
