/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 * 
 * Nuts Framework 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.mock.jndi;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.spi.InitialContextFactory;
import javax.naming.spi.InitialContextFactoryBuilder;
import javax.naming.spi.NamingManager;

import nuts.core.lang.Classes;
import nuts.core.log.Log;
import nuts.core.log.Logs;


/**
 * Simple implementation of a JNDI naming context builder.
 *
 * <p>Mainly targeted at test environments, where each test case can
 * configure JNDI appropriately, so that <code>new InitialContext()</code>
 * will expose the required objects. Also usable for standalone applications,
 * e.g. for binding a JDBC DataSource to a well-known JNDI location, to be
 * able to use traditional J2EE data access code outside of a J2EE container.
 *
 * <p>There are various choices for DataSource implementations:
 * <ul>
 * <li>SingleConnectionDataSource (using the same Connection for all getConnection calls);
 * <li>DriverManagerDataSource (creating a new Connection on each getConnection call);
 * <li>Apache's Jakarta Commons DBCP offers BasicDataSource (a real pool).
 * </ul>
 *
 * <p>Typical usage in bootstrap code:
 *
 * <pre class="code">
 * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
 * DataSource ds = new DriverManagerDataSource(...);
 * builder.bind("java:comp/env/jdbc/myds", ds);
 * builder.activate();</pre>
 * 
 * Note that it's impossible to activate multiple builders within the same JVM,
 * due to JNDI restrictions. Thus to configure a fresh builder repeatedly, use
 * the following code to get a reference to either an already activated builder
 * or a newly activated one:
 *
 * <pre class="code">
 * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
 * DataSource ds = new DriverManagerDataSource(...);
 * builder.bind("java:comp/env/jdbc/myds", ds);</pre>
 *
 * Note that you <i>should not</i> call <code>activate()</code> on a builder from
 * this factory method, as there will already be an activated one in any case.
 *
 * <p>An instance of this class is only necessary at setup time.
 * An application does not need to keep a reference to it after activation.
 *
 */
public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder {

	/** An instance of this class bound to JNDI */
	private static volatile SimpleNamingContextBuilder activated;

	private static boolean initialized = false;

	private static final Object initializationLock = new Object();


	/**
	 * Checks if a SimpleNamingContextBuilder is active.
	 * @return the current SimpleNamingContextBuilder instance,
	 * or <code>null</code> if none
	 */
	public static SimpleNamingContextBuilder getCurrentContextBuilder() {
		return activated;
	}

	/**
	 * If no SimpleNamingContextBuilder is already configuring JNDI,
	 * create and activate one. Otherwise take the existing activate
	 * SimpleNamingContextBuilder, clear it and return it.
	 * <p>This is mainly intended for test suites that want to
	 * reinitialize JNDI bindings from scratch repeatedly.
	 * @return an empty SimpleNamingContextBuilder that can be used
	 * to control JNDI bindings
	 */
	public static SimpleNamingContextBuilder emptyActivatedContextBuilder() throws NamingException {
		if (activated != null) {
			// Clear already activated context builder.
			activated.clear();
		}
		else {
			// Create and activate new context builder.
			SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
			// The activate() call will cause an assignment to the activated variable.
			builder.activate();
		}
		return activated;
	}


	private final Log logger = Logs.getLog(getClass());

	private final Hashtable<String,Object> boundObjects = new Hashtable<String,Object>();


	/**
	 * Register the context builder by registering it with the JNDI NamingManager.
	 * Note that once this has been done, <code>new InitialContext()</code> will always
	 * return a context from this factory. Use the <code>emptyActivatedContextBuilder()</code>
	 * static method to get an empty context (for example, in test methods).
	 * @throws IllegalStateException if there's already a naming context builder
	 * registered with the JNDI NamingManager
	 */
	public void activate() throws IllegalStateException, NamingException {
		logger.info("Activating simple JNDI environment");
		synchronized (initializationLock) {
			if (!initialized) {
				if (NamingManager.hasInitialContextFactoryBuilder()) {
					throw new IllegalStateException(
							"Cannot activate SimpleNamingContextBuilder: there is already a JNDI provider registered. " +
							"Note that JNDI is a JVM-wide service, shared at the JVM system class loader level, " +
							"with no reset option. As a consequence, a JNDI provider must only be registered once per JVM.");
				}
				NamingManager.setInitialContextFactoryBuilder(this);
				initialized = true;
			}
		}
		activated = this;
	}

	/**
	 * Temporarily deactivate this context builder. It will remain registered with
	 * the JNDI NamingManager but will delegate to the standard JNDI InitialContextFactory
	 * (if configured) instead of exposing its own bound objects.
	 * <p>Call <code>activate()</code> again in order to expose this context builder's own
	 * bound objects again. Such activate/deactivate sequences can be applied any number
	 * of times (e.g. within a larger integration test suite running in the same VM).
	 * @see #activate()
	 */
	public void deactivate() {
		logger.info("Deactivating simple JNDI environment");
		activated = null;
	}

	/**
	 * Clear all bindings in this context builder, while keeping it active.
	 */
	public void clear() {
		this.boundObjects.clear();
	}

	/**
	 * Bind the given object under the given name, for all naming contexts
	 * that this context builder will generate.
	 * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds")
	 * @param obj the object to bind (e.g. a DataSource implementation)
	 */
	public void bind(String name, Object obj) {
		if (logger.isInfoEnabled()) {
			logger.info("Static JNDI binding: [" + name + "] = [" + obj + "]");
		}
		this.boundObjects.put(name, obj);
	}


	/**
	 * Simple InitialContextFactoryBuilder implementation,
	 * creating a new SimpleNamingContext instance.
	 * @see SimpleNamingContext
	 */
	public InitialContextFactory createInitialContextFactory(Hashtable<?,?> environment) {
		if (activated == null && environment != null) {
			Object icf = environment.get(Context.INITIAL_CONTEXT_FACTORY);
			if (icf != null) {
				Class<?> icfClass = null;
				if (icf instanceof Class) {
					icfClass = (Class<?>) icf;
				}
				else if (icf instanceof String) {
					try {
						icfClass = Classes.getClass(getClass().getClassLoader(), (String)icf);
					}
					catch (ClassNotFoundException e) {
						throw new IllegalArgumentException("Invalid class " + icf, e);
					}
				}
				else {
					throw new IllegalArgumentException("Invalid value type for environment key [" +
							Context.INITIAL_CONTEXT_FACTORY + "]: " + icf.getClass().getName());
				}
				if (!InitialContextFactory.class.isAssignableFrom(icfClass)) {
					throw new IllegalArgumentException(
							"Specified class does not implement [" + InitialContextFactory.class.getName() + "]: " + icf);
				}
				try {
					return (InitialContextFactory) icfClass.newInstance();
				}
				catch (Throwable ex) {
					IllegalStateException ise =
							new IllegalStateException("Cannot instantiate specified InitialContextFactory: " + icf);
					ise.initCause(ex);
					throw ise;
				}
			}
		}

		// Default case...
		return new InitialContextFactory() {
			@SuppressWarnings("unchecked")
			public Context getInitialContext(Hashtable<?,?> environment) {
				return new SimpleNamingContext("", boundObjects, (Hashtable<String, Object>) environment);
			}
		};
	}

}
