/*
 * IronJacamar, a Java EE Connector Architecture implementation
 * Copyright 2015, Red Hat Inc, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the Eclipse Public License 1.0 as
 * published by the Free Software Foundation.
 *
 * This software 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 Eclipse
 * Public License for more details.
 *
 * You should have received a copy of the Eclipse Public License 
 * along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.ironjacamar.core.tx.narayana;

import org.ironjacamar.core.CoreLogger;
import org.ironjacamar.core.recovery.ValidatingManagedConnectionFactoryRecoveryPlugin;
import org.ironjacamar.core.spi.recovery.RecoveryPlugin;
import org.ironjacamar.core.spi.security.SubjectFactory;
import org.ironjacamar.core.spi.transaction.TransactionIntegration;
import org.ironjacamar.core.spi.transaction.XAResourceStatistics;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashSet;
import java.util.Set;

import javax.resource.ResourceException;
import javax.resource.spi.ManagedConnection;
import javax.resource.spi.ManagedConnectionFactory;
import javax.resource.spi.ValidatingManagedConnectionFactory;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.transaction.xa.XAResource;

import org.jboss.logging.Logger;

/**
 * An XAResourceRecovery implementation.
 *
 * @author <a href="[email protected]">Stefano Maestri</a>
 * @author <a href="[email protected]">Jesper Pedersen</a>
 */
public class XAResourceRecoveryImpl implements org.ironjacamar.core.spi.transaction.recovery.XAResourceRecovery,
                                               org.jboss.tm.XAResourceRecovery
{
   /** Log instance */
   private static CoreLogger log = Logger.getMessageLogger(CoreLogger.class, XAResourceRecoveryImpl.class.getName());

   private final TransactionIntegration ti;

   private final ManagedConnectionFactory mcf;

   private final Boolean padXid;

   private final Boolean isSameRMOverrideValue;

   private final Boolean wrapXAResource;

   private final String recoverSecurityDomain;

   private final SubjectFactory subjectFactory;

   private final RecoveryPlugin plugin;

   private final XAResourceStatistics xastat;

   private ManagedConnection recoverMC;

   private String jndiName;

   /**
    * Create a new XAResourceRecoveryImpl.
    *
    * @param ti ti
    * @param mcf mcf
    * @param padXid padXid
    * @param isSameRMOverrideValue isSameRMOverrideValue
    * @param wrapXAResource wrapXAResource
    * @param recoverSecurityDomain recoverSecurityDomain
    * @param subjectFactory subjectFactory
    * @param plugin recovery plugin
    * @param xastat The XAResource statistics implementation
    */
   public XAResourceRecoveryImpl(TransactionIntegration ti,
                                 ManagedConnectionFactory mcf,
                                 Boolean padXid, Boolean isSameRMOverrideValue, Boolean wrapXAResource,
                                 String recoverSecurityDomain,
                                 SubjectFactory subjectFactory,
                                 RecoveryPlugin plugin,
                                 XAResourceStatistics xastat)
   {
      if (ti == null)
         throw new IllegalArgumentException("TransactionIntegration is null");

      if (mcf == null)
         throw new IllegalArgumentException("MCF is null");

      if (plugin == null)
         throw new IllegalArgumentException("Plugin is null");

      this.ti = ti;
      this.mcf = mcf;
      this.padXid = padXid;
      this.isSameRMOverrideValue = isSameRMOverrideValue;
      this.wrapXAResource = wrapXAResource;
      this.recoverSecurityDomain = recoverSecurityDomain;
      this.subjectFactory = subjectFactory;

      if (plugin instanceof ValidatingManagedConnectionFactoryRecoveryPlugin &&
          mcf instanceof ValidatingManagedConnectionFactory)
      {
         this.plugin = null;
      }
      else
      {
         this.plugin = plugin;
      }

      this.xastat = xastat;

      this.recoverMC = null;
      this.jndiName = null;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void initialize() throws Exception
   {
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void shutdown() throws Exception
   {
      if (recoverMC != null)
         close(recoverMC);
   }

   /**
    * Set the jndiName.
    *
    * @param jndiName The jndiName to set.
    */
   public void setJndiName(String jndiName)
   {
      this.jndiName = jndiName;
   }

   /**
    * Provides XAResource(s) to the transaction system for recovery purposes.
    *
    * @return An array of XAResource objects for use in transaction recovery
    * In most cases the implementation will need to return only a single XAResource in the array.
    * For more sophisticated cases, such as where multiple different connection types are supported,
    * it may be necessary to return more than one.
    *
    * The Resource should be instantiated in such a way as to carry the necessary permissions to
    * allow transaction recovery. For some deployments it may therefore be necessary or desirable to
    * provide resource(s) based on e.g. database connection parameters such as username other than those
    * used for the regular application connections to the same resource manager.
    */
   @Override
   public XAResource[] getXAResources()
   {
      try
      {
         Subject subject = getSubject();

         // Check if we got a valid Subject instance; requirement for recovery
         if (subject != null)
         {
            ManagedConnection mc = open(subject);
            XAResource xaResource = null;

            Object connection = null;
            try
            {
               connection = openConnection(mc, subject);
               xaResource = mc.getXAResource();
            }
            catch (ResourceException reconnect)
            {
               closeConnection(connection);
               connection = null;
               close(mc);
               mc = open(subject);
               xaResource = mc.getXAResource();
            }
            finally
            {
               boolean forceDestroy = closeConnection(connection);
               connection = null;

               if (forceDestroy)
               {
                  close(mc);
                  mc = open(subject);
                  xaResource = mc.getXAResource();
               }
            }
            
            if (wrapXAResource && !(xaResource instanceof org.ironjacamar.core.spi.transaction.xa.XAResourceWrapper))
            {
               String eisProductName = null;
               String eisProductVersion = null;

               try
               {
                  if (mc.getMetaData() != null)
                  {
                     eisProductName = mc.getMetaData().getEISProductName();
                     eisProductVersion = mc.getMetaData().getEISProductVersion();
                  }
               }
               catch (ResourceException re)
               {
                  // Ignore
               }

               if (eisProductName == null)
                  eisProductName = jndiName;

               if (eisProductVersion == null)
                  eisProductVersion = jndiName;

               xaResource = ti.createXAResourceWrapper(xaResource,
                                                       padXid,
                                                       isSameRMOverrideValue,
                                                       eisProductName,
                                                       eisProductVersion,
                                                       jndiName, false,
                                                       xastat);
            }

            log.debugf("Recovery XAResource=%s for %s", xaResource, jndiName);

            return new XAResource[]{xaResource};
         }
         else
         {
            log.nullSubjectCrashRecovery(jndiName);
         }
      }
      catch (ResourceException re)
      {
         log.exceptionDuringCrashRecovery(jndiName, re.getMessage(), re);
      }

      return new XAResource[0];
   }

   /**
    * This method provide the Subject used for the XA Resource Recovery
    * integration with the XAResourceRecoveryRegistry.
    *
    * This isn't done through the SecurityAssociation functionality of JBossSX
    * as the Subject returned here should only be used for recovery.
    *
    * @return The recovery subject; <code>null</code> if no Subject could be created
    */
   private Subject getSubject()
   {
      return AccessController.doPrivileged(new PrivilegedAction<Subject>()
      {
         /**
          * run method
          */
         public Subject run()
         {
            try
            {
               String domain = recoverSecurityDomain;

               if (domain != null && subjectFactory != null)
               {
                  Subject subject = SecurityActions.createSubject(subjectFactory, domain);
                     
                  Set<PasswordCredential> pcs = SecurityActions.getPasswordCredentials(subject);
                  if (!pcs.isEmpty())
                  {
                     for (PasswordCredential pc : pcs)
                     {
                        pc.setManagedConnectionFactory(mcf);
                     }
                  }

                  log.debugf("Recovery Subject=%s", subject);

                  return subject;
               }
               else
               {
                  log.noCrashRecoverySecurityDomain(jndiName);
               }
            }
            catch (Throwable t)
            {
               log.exceptionDuringCrashRecoverySubject(jndiName, t.getMessage(), t);
            }

            return null;
         }
      });
   }

   /**
    * Open a managed connection
    * @param s The subject
    * @return The managed connection
    * @exception ResourceException Thrown in case of an error
    */
   @SuppressWarnings("unchecked")
   private ManagedConnection open(Subject s) throws ResourceException
   {
      log.debugf("Open managed connection (%s)", s);

      if (recoverMC == null)
         recoverMC = mcf.createManagedConnection(s, null);

      if (plugin == null)
      {
         try
         {
            ValidatingManagedConnectionFactory vmcf = (ValidatingManagedConnectionFactory)mcf;

            Set connectionSet = new HashSet(1);
            connectionSet.add(recoverMC);

            Set invalid = vmcf.getInvalidConnections(connectionSet);

            if (invalid != null && !invalid.isEmpty())
            {
               log.debugf("Invalid managed connection: %s", recoverMC);

               close(recoverMC);
               recoverMC = mcf.createManagedConnection(s, null);
            }
         }
         catch (ResourceException re)
         {
            log.debugf("Exception during invalid check", re);

            close(recoverMC);
            recoverMC = mcf.createManagedConnection(s, null);
         }
      }

      return recoverMC;
   }

   /**
    * Close a managed connection
    * @param mc The managed connection
    */
   private void close(ManagedConnection mc)
   {
      log.debugf("Closing managed connection for recovery (%s)", mc);

      if (mc != null)
      {
         try
         {
            mc.cleanup();
         }
         catch (ResourceException ire)
         {
            log.debugf("Error during recovery cleanup", ire);
         }
      }

      if (mc != null)
      {
         try
         {
            mc.destroy();
         }
         catch (ResourceException ire)
         {
            log.debugf("Error during recovery destroy", ire);
         }
      }

      // The managed connection for recovery is now gone
      recoverMC = null;
   }

   /**
    * Open a connection
    * @param mc The managed connection
    * @param s The subject
    * @return The connection handle
    * @exception ResourceException Thrown in case of an error
    */
   private Object openConnection(ManagedConnection mc, Subject s) throws ResourceException
   {
      if (plugin == null)
         return null;

      log.debugf("Open connection (%s, %s)", mc, s);

      return mc.getConnection(s, null);
   }

   /**
    * Close a connection
    * @param c The connection
    * @return Should the managed connection be forced closed
    */
   private boolean closeConnection(Object c)
   {
      if (plugin == null)
         return false;

      log.debugf("Closing connection for recovery check (%s)", c);

      boolean forceClose = false;

      if (c != null)
      {
         try
         {
            forceClose = !plugin.isValid(c);
         }
         catch (ResourceException re)
         {
            log.debugf("Error during recovery plugin isValid()", re);
            forceClose = true;
         }

         try
         {
            plugin.close(c);
         }
         catch (ResourceException re)
         {
            log.debugf("Error during recovery plugin close()", re);
            forceClose = true;
         }
      }

      log.debugf("Force close=%s", forceClose);

      return forceClose;
   }
}