/********************************************************* {COPYRIGHT-TOP} ***
* Copyright 2016 IBM Corporation
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the MIT License
* which accompanies this distribution, and is available at
* http://opensource.org/licenses/MIT
********************************************************** {COPYRIGHT-END} **/

package com.ibm.uk.hursley.perfharness.jms.providers;

import java.io.File;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;

import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicSession;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;

import com.ibm.uk.hursley.perfharness.Config;
import com.ibm.uk.hursley.perfharness.Log;
import com.ibm.uk.hursley.perfharness.jms.DestinationWrapper;

/**
 * Provider independent access to JMS resources.  All destination names will be
 * interpreted as the lookup name rather than the absolute name.
 * <p>This class also provides a base on which to build vendor-specific JMSProvider
 * implementations which may also use JNDI. 
 */
public class JNDI extends AbstractJMSProvider {

	@SuppressWarnings("unused")
	private static final String c = com.ibm.uk.hursley.perfharness.Copyright.COPYRIGHT; // IGNORE compiler warning

	public static final String PSMODE = "PUBSUBMODE";
	public static final String PSMODE_PUB = "PUB";
	public static final String PSMODE_SUB = "SUB";
	
	protected static boolean usingJNDI;	

	/**
	 * Validate parameters.  The contents of <code>-ii</code> and <code>-iu</code> are not checked at this stage.
	 */
	public static void registerConfig() {

		Config.registerSelf( JNDI.class );
		usingJNDI = ! "".equals( Config.parms.getString("cf") ) || Config.parms.getString("pc").equalsIgnoreCase("jndi");
		
	}
	
	protected InitialContext initialContext = null;

	
	//
	// JMS 1.0.2 Methods
	//

	/**
	 * Return a Queue object from JNDI
	 */
	protected DestinationWrapper<Queue> lookupQueueFromJNDI(String uri)	throws NamingException {
		return new DestinationWrapper<Queue>(uri, (Queue) getInitialContext().lookup(uri));
	}

	/**
	 * Return a Topic object from JNDI
	 */
	protected DestinationWrapper<Topic> lookupTopicFromJNDI(String uri) throws NamingException {
		return new DestinationWrapper<Topic>(uri, (Topic) getInitialContext().lookup(uri));
	}

	/**
	 * <b>JMS 1.0.2</b>  Calls the JNDI-specific version of this method.  This method is expected to be overidden
	 * by vendor-specific subclasses.
	 * @return A valid ConnectionFactory.
	 */
	public TopicConnectionFactory lookupTopicConnectionFactory(String name)	throws JMSException,NamingException {
		return lookupTopicConnectionFactoryFromJNDI(name==null?Config.parms.getString("cf"):name);
	}
	
	/**
	 * <b>JMS 1.0.2</b>  Calls the JNDI-specific version of this method.  This method is expected to be overidden
	 * by vendor-specific subclasses.
	 * @return A valid ConnectionFactory.
	 */
	public QueueConnectionFactory lookupQueueConnectionFactory(String name)	throws JMSException, NamingException {
		return lookupQueueConnectionFactoryFromJNDI(name==null?Config.parms.getString("cf"):name);
	}
	
	/**
	 * <b>JMS 1.0.2</b>
	 * @return A valid Queue object created either from JNDI lookup or directly from the given session.
	 */
	public DestinationWrapper<Queue> lookupQueue(String uri, QueueSession session) throws JMSException, NamingException {
		if ( usingJNDI ) {
			return lookupQueueFromJNDI( uri );
		} else {
			return new DestinationWrapper<Queue>( uri, session.createQueue( uri ) );
		}
	}

	/**
	 * <b>JMS 1.0.2</b>
	 * @return A valid Topic object created either from JNDI lookup or directly from the given session.
	 */
	public DestinationWrapper<Topic> lookupTopic(String uri, TopicSession session) throws JMSException, NamingException {
		if ( usingJNDI ) {
			return lookupTopicFromJNDI( uri );
		} else {
			return new DestinationWrapper<Topic>( uri, session.createTopic( uri ) );
		}
	}	
	
	//
	// JMS 1.1 methods
	//
	
	/**
	 * Return a (JMS 1.1) Destination object from JNDI.
	 */
	protected DestinationWrapper<Destination> lookupDestinationFromJNDI(String uri) throws NamingException {
		return new DestinationWrapper<Destination>(uri,	(Destination) getInitialContext().lookup(uri));
	}
	
	/**
	 * <b>JMS 1.1</b>  Calls the JNDI-specific version of this method.  This method is expected to be overidden
	 * by vendor-specific subclasses.
	 * @return A valid ConnectionFactory.
	 */
	public ConnectionFactory lookupConnectionFactory(String name) throws JMSException,NamingException {
		return lookupConnectionFactoryFromJNDI(name==null?Config.parms.getString("cf"):name);
	}

	/**
	 * <b>JMS 1.1</b>
	 * @return A valid Topic object created either from JNDI lookup or directly from the given session.
	 */
	public DestinationWrapper<Queue> lookupQueue(String uri, Session session) throws JMSException, NamingException {
		if ( usingJNDI ) {
			return lookupQueueFromJNDI( uri );
		} else {
			return new DestinationWrapper<Queue>( uri, session.createQueue( uri ) );
		}
	}
	
	/**
	 * <b>JMS 1.1</b>
	 * @return A valid Topic object created either from JNDI lookup or directly from the given session.
	 */
	public DestinationWrapper<Topic> lookupTopic(String uri, Session session) throws JMSException, NamingException {
		if ( usingJNDI ) {
			return lookupTopicFromJNDI( uri );
		} else {
			return new DestinationWrapper<Topic>( uri, session.createTopic( uri ) );
		}
	}

	/**
	 * <b>JMS 1.1</b>
	 * @return A valid Destination object
	 * @throws JMSException This method only works when using JNDI, since an abstract Destination object
	 * can only be looked up and not created.
	 */
	public DestinationWrapper<Destination> lookupDestination(String uri, Session session) throws JMSException, NamingException {
		if ( usingJNDI ) {
			return lookupDestinationFromJNDI( uri );
		} else {
			throw new JMSException( "Abstract destinations cannot be created (should be using JNDI?)" );
		}
	}
	
	/**
	 * <b>JMS 2.0</b>
	 * @param uri String containing queue name
	 * @param context Active JMSContext must be supplied if not retrieving from JNDI
	 * @return A valid DestinationWrapper object containing a queue retrieved from JNDI or created from the provided JMSContext
	 * @throws NamingException
	 */
	public DestinationWrapper<Queue> lookupQueue(String uri, JMSContext context) throws JMSException, NamingException {
		if (usingJNDI || context == null) {
			return lookupQueueFromJNDI(uri); 
		} else {
			return new DestinationWrapper<Queue>(uri, context.createQueue(uri));
		}
	}

	/**
	 * <b>JMS 2.0</b>
	 * @param uri String containing topic name
	 * @param context Active JMSContext must be supplied if not retrieving from JNDI
	 * @return A valid DestinationWrapper object containing a queue retrieved from JNDI or created from the provided JMSContext
	 * @throws NamingException
	 */
	public DestinationWrapper<Topic> lookupTopic(String uri, JMSContext context) throws JMSException, NamingException {
		if (usingJNDI || context == null) {
			return lookupTopicFromJNDI(uri);
		} else {
			return new DestinationWrapper<Topic>(uri, context.createTopic(uri));			
		}
	}    
	
	
	//
	// JNDI specific methods
	//
	/**
	 * Return a JNDI Initialcontext based upon any parameters passed on the command line and then
	 * using the standard JNDI methods for commandline and properties files. 
	 */
	public synchronized InitialContext getInitialContext() throws NamingException {
	
		if (initialContext == null) {
			//Altered second parameter to object as not all entries are strings
			final Hashtable<Object, Object> env = new Hashtable<Object,Object>();

			//If we are passed a properties file, use that for initial context creation
			String ipf = Config.parms.getString("ipf").trim();
			if (ipf.length() > 0) {
				try {
					File f = new File(ipf);
					InputStream is = new FileInputStream(f);
					Properties props = new Properties();
					props.load(is);
					env.putAll(props);
				} catch (Exception e) {
					Log.logger.log(Level.SEVERE, "Problem loading JNDI properties file: " + ipf + "; Exception: " + e);
				}
			} else {
			final String username = Config.parms.getString("us");
			if (username != null && username.length() != 0) {
				Log.logger.log(Level.INFO, "getInitialContext(): authenticating as \"" + username + "\"");
				// env.put(Context.SECURITY_AUTHENTICATION, "simple"); // ???
				env.put(Context.SECURITY_PRINCIPAL, username);
				env.put(Context.SECURITY_CREDENTIALS, Config.parms.getString("pw"));
			}

			final String ii = Config.parms.getString("ii");
			if (ii != null && ii.length() != 0)
				env.put(Context.INITIAL_CONTEXT_FACTORY, ii);

			final String iu = Config.parms.getString("iu");
			if (iu != null && iu.length() != 0)	
				env.put(Context.PROVIDER_URL, iu);
			}
			boolean useSunJRE = Config.parms.getBoolean("iz");
			if (useSunJRE) {
				System.out.println("Using Suns JRE - Setting ORB initialisation string");
				env.put("java.naming.corba.orb", org.omg.CORBA.ORB.init((String[]) null, null));
				//The alternative can also be tried if the above doesnt work with JRE in use
				//The IBM JRE will automatically find the correct ORB, so do not set this param
				//env.put("com.ibm.CORBA.ORBInit", "com.ibm.ws.sib.client.ORB");
			}
			
			final String jndiPropertiesFile = Config.parms.getString("jf");
			if(jndiPropertiesFile != null && jndiPropertiesFile.length() != 0) {
				Log.logger.log( Level.INFO, "Loading JNDI properties from file {0}", jndiPropertiesFile );
				final Properties props = new Properties();
				try {
	                props.load( new BufferedInputStream( new FileInputStream( jndiPropertiesFile ) ) );
                	for (final Iterator<Map.Entry<Object, Object>> iter = props.entrySet().iterator(); iter.hasNext();) {
                		final Map.Entry<Object, Object> entry = iter.next();
						
                		String key = (String)entry.getKey();
                		String value = (String)entry.getValue();						
				
                		Log.logger.log( Level.FINE, "Adding JNDI property key: {0} value: {1} ", new Object[] { key, value } );
                		env.put(key,value);	
						
                	} // end for all props
				} catch (FileNotFoundException e) {
	                // TODO Auto-generated catch block
	                e.printStackTrace();
                } catch (IOException e) {
	                // TODO Auto-generated catch block
	                e.printStackTrace();
                }
			}
			
			initialContext = new InitialContext(env);
			
		}
		return initialContext;
	
	}
	
	/**
	 * <b>JMS 1.1</b> Look up the named ConnectionFactory object.
	 * @param uri
	 * @throws NamingException
	 */
	protected synchronized ConnectionFactory lookupConnectionFactoryFromJNDI(String uri) throws NamingException {
	
		final InitialContext jndiContext = getInitialContext();
		return (ConnectionFactory)jndiContext.lookup(uri);
	
	}

	/**
	 * <b>JMS 1.0.2</b> Look up the named QueueConnectionFactory object.
	 * @param uri
	 * @throws NamingException
	 */	
	protected synchronized QueueConnectionFactory lookupQueueConnectionFactoryFromJNDI(String uri) throws NamingException {

		final InitialContext jndiContext = getInitialContext();
		return (QueueConnectionFactory)jndiContext.lookup(uri);
	
	}

	/**
	 * <b>JMS 1.0.2</b> Look up the named TopicConnectionFactory object.
	 * @param uri
	 * @throws NamingException
	 */	
	protected synchronized TopicConnectionFactory lookupTopicConnectionFactoryFromJNDI(String uri) throws NamingException {
	
		final InitialContext jndiContext = getInitialContext();
		return (TopicConnectionFactory)jndiContext.lookup(uri);
	
	}

	public void createQueue(String name) throws Exception {
		throw new RuntimeException( "JNDI module cannot create a destination" );
	}

	public void createTopic(String name) throws Exception {
		throw new RuntimeException( "JNDI module cannot create a destination" );
	}

	public void createConnectionFactory(String name) throws Exception {
		throw new RuntimeException( "JNDI module cannot create a connection factory" );
	}
	
	public void createTopicConnectionFactory(String name) throws Exception {
		throw new RuntimeException( "JNDI module cannot create a connection factory" );
	}
	
	public void createQueueConnectionFactory(String name) throws Exception {
		throw new RuntimeException( "JNDI module cannot create a connection factory" );
	}

    /**
     ** delete a queue on the provider. differs from deleting a jms queue.
     ** unbind the associated admin object from the jndi store. 
     */
    public void deleteQueue(String name) throws Exception {
    	// No op
    }
    
    /**
     ** delete a topic on the provider. differs from deleting a jms topic.
     ** unbind the associated admin object from the jndi store. 
     */
    
    public void deleteTopic(String name) throws Exception {
    	// No op
    }
    
    /**
     ** unbind an admin object from the jndi store.
     */
    public void unbind(String lookupName) throws NamingException {
        try { 
       		getInitialContext().unbind(lookupName);
        } catch ( NameNotFoundException e ) {
        	// Swallowed
        }
    }
    
    public void initializeSetup() throws Exception {
    	// No op
    }

    public void completeSetup() throws Exception {
    	// No op
    }
    
    public boolean isQueue(Destination destination) {
    	return destination instanceof Queue;
    }

}