package org.mockejb;


import javax.jms.*;

import javax.naming.*;

import org.mockejb.interceptor.*;
import org.mockejb.jms.*;

/**
 * Provides methods to "deploy" EJBs. Most deploy  
 * methods simply create Home proxy and bind it to the JNDI. 
 * Since MockEjb is not a container in a true sense, the purpose of this class
 * is to provide an abstraction that emulates EJB deployment.
 * 
 * @author  Alexander Ananiev
 */
public class MockContainer {
    
    
    private Context context;
    
    private EntityDatabase entityDatabase;
    
    /**
     * We store the security context (principal) on a thread. The field is static to make sure that 
     * there is only one per thread
     */
    private static ThreadLocal threadContext = new ThreadLocal(); 
    
    /**
     * Returns the user that was passed to the 
     * MockContainer.login method. Note that the user 
     * is stored as a ThreadLocal variable, so it is available to all classes. 
     * Returns the anonymous user if 
     * login was not called. 
     *  
     * @return the current user of the MockContainer
     *
     */
    public static MockUser getUser() {
        
        // check if we have the principal on the thread
        MockUser user = (MockUser) threadContext.get();
        
        if ( user == null )
            user = MockUser.ANONYMOUS_USER;
        
        return user;
        
    }
    
    /**
     * Simulates the login operation of the container. This method does
     * not perform any security checks.
     * It simply stores the given MockUser object for further use by EJBContext's security-related methods.
     * 
     * @param user user of the MockContainer
     */
    public void login( MockUser user ){
        threadContext.set( user );
    }
    
    

    /**
     * Creates a new instance of the MockContainer for 
     * the given context, deletes all aspects from the AspectSystem and adds the system interceptors, such as 
     * ExceptionHandler.
     * Clears the EntityDatabase as well.
     * Creates a default (anonymous) user which is used for all EJB operations unless "login"
     * is called explicitly.  
     * @param context JNDI context to use for all "bind" operations
     */
    public MockContainer( final Context context ){
        this.context = context;
        threadContext.set( MockUser.ANONYMOUS_USER );
        setupDefaultInterceptors();
    }
    
    /**
     * Add the interceptors that should always be present to 
     * the AspectSystem.
     */
    protected void setupDefaultInterceptors(){
        
        // create the entity cache
        loadEntityDatabase();

        AspectSystem aspectSystem = AspectSystemFactory.getAspectSystem();
        aspectSystem.clear();
        
        aspectSystem.addFirst( new BMPFinderHandler( entityDatabase ) );
        aspectSystem.addFirst( new CMPFindByPrimaryKeyHandler( entityDatabase ) );
        
        // All beans should have the exception handler
        aspectSystem.addFirst( new EjbExceptionHandler());
       
    }

    
    /**
     * Deploys session bean specified by the given descriptor.
     * <code>MockContainer</code> creates the proxy implementing 
     * session bean home interface and binds it to the JNDI context
     * using <code>rebind()</code> method. 
     * Clients can subsequently lookup the home and invoke <code>create()</code>.  
     * @param descriptor descriptor of the session bean to deploy 
     * As of MockEJB 0.6, this method does not return MockEjbObject since  
     * the direct use of MockEjbObject is deprecated. AspectSystem should be used instead.
     */
    public void deploy( SessionBeanDescriptor descriptor ) throws NamingException {
        
        SessionBeanHome home = new SessionBeanHome( descriptor );

        context.rebind( descriptor.getJndiName(), home.createProxy() );
        
    }
    
    /**
     * Deploys entity bean specified by the given descriptor.
     * 
     */ 
    public void deploy( EntityBeanDescriptor descriptor ) throws NamingException {
        
        EntityBeanHome home = new EntityBeanHome( descriptor, entityDatabase );

        context.rebind( descriptor.getJndiName(), home.createProxy() );
        
    }
    

    /**
     * If "isAlreadyBound" is "false" in deployment descriptor, 
     * creates mock connection factory and destination and bind them to JNDI.
     * Otherwise, assumes that the connection factory and destination were created
     * previously and available from JNDI. The default is "false". 
     * Creates MDB and sets it to listen on the destination. 
     * Handles both queues and topics, depending on "isTopic" setting of the 
     * deployment descriptor.
     * Note that mock JMS implementation is synchronous, in other words the message sent 
     * to the destination is delivered to MDB right away.  
     * @param descriptor deployment descriptor of the MDB specifying JNDI names of
     * connection factory and destinations as well as the bean implementation object. 
     * 
     * @throws NamingException in case of problems binding to JNDI or retrieving objects from JNDI
     * @throws JMSException in case of problems with mock connection factory or destination.  
     */
    public void deploy( MDBDescriptor descriptor ) throws NamingException, JMSException {

        
        // Create connection factory and destination using JMS 1.0 way
        MessageConsumer consumer;
        Connection connection;
        
        if ( descriptor.isTopic() ){
            
            TopicConnectionFactory topicConnectionFactory = 
                (TopicConnectionFactory) createJMSObject( 
                    descriptor.getConnectionFactoryJndiName(), descriptor.isAlreadyBound(), 
                    new TopicConnectionFactoryImpl() ); 
            
            Topic topic = (Topic) createJMSObject( 
                    descriptor.getDestinationJndiName(), descriptor.isAlreadyBound(),
                    new MockTopic( descriptor.getDestinationJndiName() ) ); 
            
            TopicConnection topicConnection =  topicConnectionFactory.createTopicConnection();
            connection = topicConnection;
            // TODO: implement transactions and acknowledgements
            TopicSession topicSession = topicConnection.createTopicSession(false, 
                    Session.AUTO_ACKNOWLEDGE);
            consumer = topicSession.createSubscriber(topic);
            
        }
        else {

            QueueConnectionFactory queueConnectionFactory = 
                (QueueConnectionFactory) createJMSObject( 
                    descriptor.getConnectionFactoryJndiName(), descriptor.isAlreadyBound(), 
                    new QueueConnectionFactoryImpl() ); 
            
            Queue queue = (Queue) createJMSObject( 
                    descriptor.getDestinationJndiName(), descriptor.isAlreadyBound(),
                    new MockQueue( descriptor.getDestinationJndiName() ) ); 
           
            QueueConnection queueConnection =  queueConnectionFactory.createQueueConnection();
            connection = queueConnection;
            QueueSession queueSession = queueConnection.createQueueSession(false, 
                    Session.AUTO_ACKNOWLEDGE);
            consumer = queueSession.createReceiver( queue );
        }
        
        // Same routine as for session bean
        MDBHome home = new MDBHome( descriptor );
        MDBHomeIface mdbHome = (MDBHomeIface)home.createProxy();
        
        MessageListener messageListener = mdbHome.create();
        
        consumer.setMessageListener( messageListener );
        connection.start();
        
    }
    
    /**
     * Returns an instance of the EntityDatabase that will be used by the 
     * system interceptors (Finder Handlers) used by this instance of the container.
     * 
     * @return an instance of the entity database
     */
    public EntityDatabase getEntityDatabase(){
        
        return entityDatabase;
        
    }
    
    /**
     * Helper method to create connection factory or destination based on the settings
     * 
     * @param jndiName
     * @param isAlreadyBound if true, get one from JNDI
     * @param defaultImpl
     * @return created object
     */
    private Object createJMSObject( String jndiName, boolean isAlreadyBound, 
            Object defaultImpl ) throws NamingException {
        
        Object obj = defaultImpl;
        if ( isAlreadyBound )
            obj = context.lookup( jndiName );
        else
            context.rebind( jndiName, defaultImpl );
        
        return obj;
        
    }
    
    /**
     * Creates message-driven bean. This method emulates <code>create()</code>
     * method of the Session bean home interface. 
     * Since MDBs don't have home interface, <code>MockContainer</code> provides this 
     * service for MDB clients. 
     * @param ejbObject MockEjbObject of the message bean created by <code>deployMessageBean()</code> 
     * @return implementation of the MessageListener interface. The interface is 
     * implemented by a proxy provided by MockEjbObject.
     * 
     * @deprecated use deploy instead 
     */
    public MessageListener createMessageBean( MockEjbObject ejbObject ) {
        
        // we use local home to avoid remote exception
        MDBHomeIface home = (MDBHomeIface) (ejbObject.getHomeImpl());
        
        return home.create();
        
    }
    
    
    /**
     * Tests if the given throwable is the system exception in terms of the container.
     * Currently we consider all runtime exceptions and transaction-related exceptions
     * system exceptions.
     * <br>Note that the spec is vague on 
     * what is system and non-system exception, so this method might change in the future.
     * 
     * @param throwable exception in question
     * @return true if the given throwable is "system" exception
     */
    // TODO: provide the facility for setting an array of exception types.
    public static boolean isSystemException( Throwable throwable ){
    
        return ( throwable instanceof RuntimeException ||
            throwable instanceof java.rmi.RemoteException ||
            throwable instanceof javax.transaction.SystemException ||
            throwable instanceof javax.transaction.NotSupportedException ||             
            throwable instanceof javax.transaction.InvalidTransactionException ||             
            throwable instanceof java.lang.reflect.InvocationTargetException);       
    }
    
    
    protected void loadEntityDatabase() {
        
        String entityDatabaseClassName = System.getProperty( "mockejb.entity.database" ); 
        
        if ( entityDatabaseClassName != null ) {
            try {           

                Class entityDatabaseClass = 
                    Class.forName( entityDatabaseClassName, true, this.getClass().getClassLoader());
                entityDatabase = (EntityDatabase) entityDatabaseClass.newInstance();
                
            }
            catch ( ClassNotFoundException cnfe ) {
                throw new MockEjbSystemException( cnfe );
            }
            catch ( InstantiationException ie ) {
                throw new MockEjbSystemException( ie );
            }
            catch ( IllegalAccessException iae ) {
                throw new MockEjbSystemException( iae );
            }
        }
        else
            entityDatabase = new EntityDatabaseImpl();
        
    }

}