/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.jndi;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.*;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;

import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.i18n.LogWriterI18n;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.datasource.AbstractDataSource;
import org.apache.geode.internal.datasource.ClientConnectionFactoryWrapper;
import org.apache.geode.internal.datasource.DataSourceCreateException;
import org.apache.geode.internal.datasource.DataSourceFactory;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.jta.TransactionManagerImpl;
import org.apache.geode.internal.jta.TransactionUtils;
import org.apache.geode.internal.jta.UserTransactionImpl;

/**
 * <p>
 * Utility class for binding DataSources and Transactional resources to JNDI Tree. If there is a
 * pre-existing JNDI tree in the system, the GemFire JNDI tree is not not generated.
 * </p>
 * <p>
 * Datasources are bound to jndi tree after getting initialised. The initialisation parameters are
 * read from cache.xml.
 * </p>
 * <p>
 * If there is a pre-existing TransactionManager/UserTransaction (possible in presence of
 * application server scenerio), the GemFire TransactionManager and UserTransaction will not be
 * initialised. In that case, application server TransactionManager/UserTransaction will handle the
 * transactional activity. But even in this case the datasource element will be bound to the
 * available JNDI tree. The transactional datasource (XADataSource) will make use of available
 * TransactionManager.
 * </p>
 *
 */
public class JNDIInvoker {

  // private static boolean DEBUG = false;
  /**
   * JNDI Context, this may refer to GemFire JNDI Context or external Context, in case the external
   * JNDI tree exists.
   */
  private static Context ctx;
  /**
   * transactionManager TransactionManager, this refers to GemFire TransactionManager only.
   */
  private static TransactionManager transactionManager;
  // most of the following came from the javadocs at:
  // http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/transaction/jta/JtaTransactionManager.html
  private static String[][] knownJNDIManagers = {{"java:/TransactionManager", "JBoss"},
      {"java:comp/TransactionManager", "Cosminexus"}, // and many others
      {"java:appserver/TransactionManager", "GlassFish"}, {"java:pm/TransactionManager", "SunONE"},
      {"java:comp/UserTransaction", "Orion, JTOM, BEA WebLogic"},
      // not sure about the following but leaving it for backwards compat
      {"javax.transaction.TransactionManager", "BEA WebLogic"}};
  /** ************************************************* */
  /**
   * WebSphere 5.1 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_1 =
      "com.ibm.ws.Transaction.TransactionManagerFactory";
  /**
   * WebSphere 5.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_5_0 =
      "com.ibm.ejs.jts.jta.TransactionManagerFactory";
  /**
   * WebSphere 4.0 TransactionManagerFactory
   */
  private static final String WS_FACTORY_CLASS_4 = "com.ibm.ejs.jts.jta.JTSXA";
  /**
   * List of DataSource bound to the context, used for cleaning gracefully closing datasource and
   * associated threads.
   */
  private static List dataSourceList = new ArrayList();

  /**
   * If this system property is set to true, GemFire will not try to lookup for an existing JTA
   * transaction manager bound to JNDI context or try to bind itself as a JTA transaction manager.
   * Also region operations will <b>not</b> participate in an ongoing JTA transaction.
   */
  private static Boolean IGNORE_JTA =
      Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "ignoreJTA");

  /**
   * Bind the transaction resources. Bind UserTransaction and TransactionManager.
   * <p>
   * If there is pre-existing JNDI tree in the system and TransactionManager / UserTransaction is
   * already bound, GemFire will make use of these resources, but if TransactionManager /
   * UserTransaction is not available, the GemFire TransactionManager / UserTransaction will be
   * bound to the JNDI tree.
   * </p>
   *
   */
  public static void mapTransactions(DistributedSystem distSystem) {
    try {
      TransactionUtils.setLogWriter(distSystem.getLogWriter().convertToLogWriterI18n());
      cleanup();
      if (IGNORE_JTA) {
        return;
      }
      ctx = new InitialContext();
      doTransactionLookup();
    } catch (NamingException ne) {
      LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
      if (ne instanceof NoInitialContextException) {
        String exception =
            "JNDIInvoker::mapTransactions:: No application server context found, Starting GemFire JNDI Context Context ";
        if (writer.finerEnabled())
          writer.finer(exception);
        try {
          initializeGemFireContext();
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled())
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound TransactionManager to Context GemFire JNDI Tree");
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled())
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound Transaction to Context GemFire JNDI Tree");
        } catch (NamingException ne1) {
          if (writer.infoEnabled())
            writer.info(
                LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSNAMINGEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_GEMFIRE_JNDI_TREE);
        } catch (SystemException se1) {
          if (writer.infoEnabled())
            writer.info(
                LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSSYSTEMEXCEPTION_WHILE_BINDING_USERTRANSACTION_TO_GEMFIRE_JNDI_TREE);
        }
      } else if (ne instanceof NameNotFoundException) {
        String exception =
            "JNDIInvoker::mapTransactions:: No TransactionManager associated to Application server context, trying to bind GemFire TransactionManager";
        if (writer.finerEnabled())
          writer.finer(exception);
        try {
          transactionManager = TransactionManagerImpl.getTransactionManager();
          ctx.rebind("java:/TransactionManager", transactionManager);
          if (writer.fineEnabled())
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound TransactionManager to Application Server Context");
          UserTransactionImpl utx = new UserTransactionImpl();
          ctx.rebind("java:/UserTransaction", utx);
          if (writer.fineEnabled())
            writer.fine(
                "JNDIInvoker::mapTransactions::Bound UserTransaction to Application Server Context");
        } catch (NamingException ne1) {
          if (writer.infoEnabled())
            writer.info(
                LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSNAMINGEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_APPLICATION_SERVER_JNDI_TREE);
        } catch (SystemException se1) {
          if (writer.infoEnabled())
            writer.info(
                LocalizedStrings.JNDIInvoker_JNDIINVOKERMAPTRANSACTIONSSYSTEMEXCEPTION_WHILE_BINDING_TRANSACTIONMANAGERUSERTRANSACTION_TO_APPLICATION_SERVER_JNDI_TREE);
        }
      }
    }
  }

  /*
   * Cleans all the DataSource ans its associated threads gracefully, before start of the GemFire
   * system. Also kills all of the threads associated with the TransactionManager before making it
   * null.
   */
  private static void cleanup() {
    if (transactionManager instanceof TransactionManagerImpl) {
      TransactionManagerImpl.refresh();
      // unbind and set TransactionManager to null, so as to
      // initialize TXManager correctly if the cache is being restarted
      transactionManager = null;
      try {
        if (ctx != null) {
          ctx.unbind("java:/TransactionManager");
        }
      } catch (NamingException e) {
        // ok to ignore, rebind will be tried later
      }
    }
    int len = dataSourceList.size();
    for (int i = 0; i < len; i++) {
      if (dataSourceList.get(i) instanceof AbstractDataSource)
        ((AbstractDataSource) dataSourceList.get(i)).clearUp();
      else if (dataSourceList.get(i) instanceof ClientConnectionFactoryWrapper) {
        ((ClientConnectionFactoryWrapper) dataSourceList.get(i)).clearUp();
      }
    }
    dataSourceList.clear();
    IGNORE_JTA = Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "ignoreJTA");
  }

  /*
   * Helps in locating TransactionManager in presence of application server scenario. Stores the
   * value of TransactionManager for reference in GemFire system.
   */
  private static void doTransactionLookup() throws NamingException {
    Object jndiObject = null;
    LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
    for (int i = 0; i < knownJNDIManagers.length; i++) {
      try {
        jndiObject = ctx.lookup(knownJNDIManagers[i][0]);
      } catch (NamingException e) {
        String exception = "JNDIInvoker::doTransactionLookup::Couldn't lookup ["
            + knownJNDIManagers[i][0] + " (" + knownJNDIManagers[i][1] + ")]";
        if (writer.finerEnabled())
          writer.finer(exception);
      }
      if (jndiObject instanceof TransactionManager) {
        transactionManager = (TransactionManager) jndiObject;
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager for "
            + knownJNDIManagers[i][1];
        if (writer.fineEnabled())
          writer.fine(exception);
        return;
      } else {
        String exception = "JNDIInvoker::doTransactionLookup::Found TransactionManager of class "
            + (jndiObject == null ? "null" : jndiObject.getClass())
            + " but is not of type javax.transaction.TransactionManager";
        if (writer.fineEnabled())
          writer.fine(exception);
      }
    }
    Class clazz;
    try {
      if (writer.finerEnabled())
        writer.finer(
            "JNDIInvoker::doTransactionLookup::Trying WebSphere 5.1: " + WS_FACTORY_CLASS_5_1);
      clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_1);
      if (writer.fineEnabled())
        writer
            .fine("JNDIInvoker::doTransactionLookup::Found WebSphere 5.1: " + WS_FACTORY_CLASS_5_1);
    } catch (ClassNotFoundException ex) {
      try {
        if (writer.finerEnabled())
          writer.finer(
              "JNDIInvoker::doTransactionLookup::Trying WebSphere 5.0: " + WS_FACTORY_CLASS_5_0);
        clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_5_0);
        if (writer.fineEnabled())
          writer.fine(
              "JNDIInvoker::doTransactionLookup::Found WebSphere 5.0: " + WS_FACTORY_CLASS_5_0);
      } catch (ClassNotFoundException ex2) {
        try {
          clazz = ClassPathLoader.getLatest().forName(WS_FACTORY_CLASS_4);
          String exception =
              "JNDIInvoker::doTransactionLookup::Found WebSphere 4: " + WS_FACTORY_CLASS_4;
          if (writer.fineEnabled())
            writer.fine(exception, ex);
        } catch (ClassNotFoundException ex3) {
          if (writer.finerEnabled())
            writer.finer(
                "JNDIInvoker::doTransactionLookup::Couldn't find any WebSphere TransactionManager factory class, neither for WebSphere version 5.1 nor 5.0 nor 4");
          throw new NoInitialContextException();
        }
      }
    }
    try {
      Method method = clazz.getMethod("getTransactionManager", (Class[]) null);
      transactionManager = (TransactionManager) method.invoke(null, (Object[]) null);
    } catch (Exception ex) {
      writer.warning(
          LocalizedStrings.JNDIInvoker_JNDIINVOKER_DOTRANSACTIONLOOKUP_FOUND_WEBSPHERE_TRANSACTIONMANAGER_FACTORY_CLASS_0_BUT_COULDNT_INVOKE_ITS_STATIC_GETTRANSACTIONMANAGER_METHOD,
          clazz.getName(), ex);
      throw new NameNotFoundException(
          LocalizedStrings.JNDIInvoker_JNDIINVOKER_DOTRANSACTIONLOOKUP_FOUND_WEBSPHERE_TRANSACTIONMANAGER_FACTORY_CLASS_0_BUT_COULDNT_INVOKE_ITS_STATIC_GETTRANSACTIONMANAGER_METHOD
              .toLocalizedString(new Object[] {clazz.getName()}));
    }
  }

  /**
   * Initialises the GemFire context. This is called when no external JNDI Context is found.
   *
   * @throws NamingException
   */
  private static void initializeGemFireContext() throws NamingException {
    Hashtable table = new Hashtable();
    table.put(Context.INITIAL_CONTEXT_FACTORY,
        "org.apache.geode.internal.jndi.InitialContextFactoryImpl");
    ctx = new InitialContext(table);
  }

  /**
   * Binds a single Datasource to the existing JNDI tree. The JNDI tree may be The Datasource
   * properties are contained in the map. The Datasource implementation class is populated based on
   * properties in the map.
   *
   * @param map contains Datasource configuration properties.
   */
  public static void mapDatasource(Map map, List props) {
    String value = (String) map.get("type");
    String jndiName = "";
    LogWriterI18n writer = TransactionUtils.getLogWriterI18n();
    Object ds = null;
    try {
      jndiName = (String) map.get("jndi-name");
      if (value.equals("PooledDataSource")) {
        ds = DataSourceFactory.getPooledDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        dataSourceList.add(ds);
        if (writer.fineEnabled())
          writer.fine("Bound java:/" + jndiName + " to Context");
      } else if (value.equals("XAPooledDataSource")) {
        ds = DataSourceFactory.getTranxDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        dataSourceList.add(ds);
        if (writer.fineEnabled())
          writer.fine("Bound java:/" + jndiName + " to Context");
      } else if (value.equals("SimpleDataSource")) {
        ds = DataSourceFactory.getSimpleDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds);
        dataSourceList.add(ds);
        if (writer.fineEnabled())
          writer.fine("Bound java:/" + jndiName + " to Context");
      } else if (value.equals("ManagedDataSource")) {
        ClientConnectionFactoryWrapper ds1 = DataSourceFactory.getManagedDataSource(map, props);
        ctx.rebind("java:/" + jndiName, ds1.getClientConnFactory());
        dataSourceList.add(ds1);
        if (writer.fineEnabled())
          writer.fine("Bound java:/" + jndiName + " to Context");
      } else {
        String exception = "JNDIInvoker::mapDataSource::No correct type of DataSource";
        if (writer.fineEnabled())
          writer.fine(exception);
        throw new DataSourceCreateException(exception);
      }
      ds = null;
    } catch (NamingException ne) {
      if (writer.infoEnabled())
        writer.info(
            LocalizedStrings.JNDIInvoker_JNDIINVOKER_MAPDATASOURCE_0_WHILE_BINDING_1_TO_JNDI_CONTEXT,
            new Object[] {"NamingException", jndiName});
    } catch (DataSourceCreateException dsce) {
      if (writer.infoEnabled())
        writer.info(
            LocalizedStrings.JNDIInvoker_JNDIINVOKER_MAPDATASOURCE_0_WHILE_BINDING_1_TO_JNDI_CONTEXT,
            new Object[] {"DataSourceCreateException", jndiName});
    }
  }

  /**
   * @return Context the existing JNDI Context. If there is no pre-esisting JNDI Context, the
   *         GemFire JNDI Context is returned.
   */
  public static Context getJNDIContext() {
    return ctx;
  }

  /**
   * returns the GemFire TransactionManager.
   *
   * @return TransactionManager
   */
  public static TransactionManager getTransactionManager() {
    return transactionManager;
  }

  public static int getNoOfAvailableDataSources() {
    return dataSourceList.size();
  }
}
