
/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 *
 */

package org.jboss.tm;



import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.management.ObjectName;
import javax.transaction.xa.Xid;
import org.jboss.system.ServiceMBeanSupport;
import javax.transaction.SystemException;
import org.jboss.util.id.GUID;


/**
 * XidFactory.java
 *
 *
 * Created: Sat Jun 15 19:01:18 2002
 *
 * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a>
 *
 * Modified: Sun Jan 05 10:45:00 2003
 *
 * @author <a href="mailto:jamie_r_burns@hotmail.com">Jamie Burns</a>
 *
 * If we stop/start JBoss but leave MS SQLServer running, MS SQLServer
 * fails when it encounters an Xid with a global transaction id that it
 * has seen before. XidFactory has been changed so that each instance of
 * XidFactory has a unique baseGlobalId each time JBoss starts.
 *
 * baseGlobalId format is now
 *
 *     hostName/guid/
 *
 * Each Xid will have a global transaction id in the following format
 *
 *    baseGlobalId/globalIdNumber
 *
 * The <code>guid</code> is a JBoss {@link org.jboss.util.id.GUID GUID}.
 * 
 * This implementation reserves 14 characters of Xid.MAXGTRIDSIZE for the 
 * <code>globalIdNumber</code>. The guid and the slahes used in the
 * <code>baseGlobalId</code> format use around 44 characters of the
 * Xid.MAXGTRIDSIZE. This leaves around 6 characters for the hostname. It is
 * likely that hostname in the <code>baseGlobalId</code> will be truncated.
 *
 * A <code>baseGlobalId</code> that is provided through the setter method will
 * be truncated if it is longer than 
 *
 *     Xid.MAXGTRIDSIZE - JBossXidFactory.MAX_GLOBAL_ID_NUMBER_SIZE
 *
 * @version
 *
 * @jmx.mbean name="jboss.tm:service=XidFactory" 
 *            display-name="XId Factory"
 *            description="A factory for supplying xids. Configuration options include the base global id string, the global id number, and whether the xid is of the maximum size."
 * @jboss.service servicefile="transaction"
 * @jboss.xmbean 
 */

public class JBossXidFactory
   extends ServiceMBeanSupport
   implements XidFactory
{

   /**
    * Maximum number of characters reserved for the globalIdNumber in the
    * globally unique transaction identifier.
    */
   private static final int MAX_GLOBAL_ID_NUMBER_SIZE = 14;
   
   /**
    * Maximum number of characters reserved for the baseGlobalId in the globally
    * unique transaction identifier.
    */
   private static final int MAX_BASE_GLOBAL_ID_SIZE = Xid.MAXGTRIDSIZE - MAX_GLOBAL_ID_NUMBER_SIZE;
   
   private ObjectName txLoggerName;

   private TxLogger txLogger;

   private boolean initialized = false;

   /**
    *  The default value of baseGlobalId is the host name of this host, 
    *  followed by a slash.
    *
    *  This is used for building globally unique transaction
    *  identifiers.  It would be safer to use the IP address, but a
    *  host name is better for humans to read and will do for now.  *
    *  This must be set individually if multiple jboss instances are *
    *  running on the same machine.  The baseGlobalId is used only for
    *  transactions originating on this jboss instance.  for
    *  transactions imported from elsewhere by the XAWork/XATerminator
    *  interfaces, the source global id is used for all branches on
    *  this jboss instance.  The baseGlobalId is also used as the base
    *  of ALL branchids generated on this machine, whether or not the
    *  global id is from this machine.
    */
   private String baseGlobalId;

   /**
    *  The next transaction id to use on this host.
    */
   private long globalIdNumber = 0;

   private long lastSavedId;

   private long idLogStep = 100;

   /**
    * The variable <code>pad</code> says whether the byte[] should be their
    * maximum 64 byte length or the minimum.  
    * The max length is required for Oracle..
    *
    */
   private boolean pad = false;

   /**
    * The variable <code>noBranchQualifier</code> base global id for
    * this instance padded if necessary
    *
    */
   private byte[] noBranchQualifier;

   public JBossXidFactory() 
   {
   }

   protected void startService() throws Exception
   {
      if (baseGlobalId == null)
      {
         StringBuffer tempBaseGlobalId = new StringBuffer(Xid.MAXGTRIDSIZE);

         tempBaseGlobalId.append("/");
         tempBaseGlobalId.append(new GUID().toString());
         tempBaseGlobalId.append("/");

         String hostName;

         try {
            hostName = InetAddress.getLocalHost().getHostName();
         } catch (UnknownHostException e) {
            hostName = "localhost";
         }

         // Ensure room for MAX_GLOBAL_ID_NUMBER_SIZE digits of serial no.
         // Truncate the hostname if there is not enough space.

         int spaceForHostName = MAX_BASE_GLOBAL_ID_SIZE - tempBaseGlobalId.length();

         if(hostName.length() > spaceForHostName)
             tempBaseGlobalId.insert(0, hostName.substring(0, spaceForHostName - 1));
         else
             tempBaseGlobalId.insert(0, hostName);

         baseGlobalId = tempBaseGlobalId.toString();
         
         setPad(pad);//initialize noBranchQualifier
      }
      if (txLogger == null)
      {
         if (txLoggerName != null)
         {
            txLogger = (TxLogger)getServer().invoke(txLoggerName,
                                                    "getInstance",
                                                    new Object[] {},
                                                    new String[] {});
         }
      }
      if (!initialized)
      {
         if (txLogger != null)
         {
            long lastId = txLogger.recoverIdCounter();
            if (lastId == -1)
            {
               globalIdNumber = 0;
            }
            else
            {
               globalIdNumber = lastId + idLogStep;
            }
         
            txLogger.saveIdCounter(globalIdNumber);
         }
         else
         {
            //standalone non persistent client operation.
            globalIdNumber = 0;
            idLogStep = -1;
         }
         lastSavedId = globalIdNumber;
         initialized = true;
      }
   }


   
   
   /**
    * mbean get-set pair for field txLoggerName
    * Get the value of txLoggerName
    * @return value of txLoggerName
    *
    * @jmx.managed-attribute description="The txLogger that is used to persist the global id counter"
    *      value="jboss.tm:service=SerializingTxLogger"
    */
   public ObjectName getTxLoggerName()
   {
      return txLoggerName;
   }

   /**
    * mbean get-set pair for field txLoggerName
    * Set the value of txLoggerName
    * @param value of txLoggerName
    *
    * @jmx.managed-attribute
    */
   public void setTxLoggerName(ObjectName txLoggerName)
   {
      this.txLoggerName = txLoggerName;
   }
   
   /**
    * The <code>setTxLogger</code> method is a convenience method for
    * testing without an mbean server.
    *
    * @param txLogger a <code>TxLogger</code> value
    */
   public void setTxLogger(TxLogger txLogger)
   {
      this.txLogger = txLogger;
   }
   
   /**
    * mbean get-set pair for field BaseGlobalId
    * Get the value of BaseGlobalId
    * @return value of BaseGlobalId
    *
    * @jmx.managed-attribute description="The base string for the Xid global id.  The default is the local host name, or 'localhost' if this cannot be determined.  This must be set explicitly if you are running more than one jboss instance on a server and they need to communicate transactions."
    */
   public String getBaseGlobalId()
   {
      return baseGlobalId;
   }
   
   
   /**
    * Set the value of BaseGlobalId. The baseGlobalId is truncated to
    * <code>MAX_BASE_GLOBAL_ID_SIZE</code> characters if its length is
    * greater than <code>MAX_BASE_GLOBAL_ID_SIZE</code> characters.
    *
    * @param BaseGlobalId  Value to assign to BaseGlobalId
    *
    *
    * @jmx.managed-attribute
    */
   public void setBaseGlobalId(final String baseGlobalId)
   {
      if(baseGlobalId.length() > MAX_BASE_GLOBAL_ID_SIZE)
          this.baseGlobalId = baseGlobalId.substring(0, MAX_BASE_GLOBAL_ID_SIZE));
      else
          this.baseGlobalId = baseGlobalId;
        
      setPad(pad);//reinitialize noBranchQualifier
   }
   

   
   
   /**
    * mbean get-set pair for field globalIdNumber
    * Get the value of globalIdNumber
    * @return value of globalIdNumber
    *
    *
    * @jmx.managed-attribute description="The current global id number."
    */
   public synchronized long getGlobalIdNumber()
   {
      return globalIdNumber;
   }
   
   
   /**
    * mbean get-set pair for field idLogStep
    * Get the value of idLogStep
    * @return value of idLogStep
    *
    * @return a <code>long</code> value
    * @jmx.managed-attribute description="The number of ids to generate before logging the last one.  This is the maximum gap in global id numbers due to a crash."
    */
   public long getIdLogStep()
   {
      return idLogStep;
   }

   /**
    * Describe <code>setIdLogStep</code> method here.
    *
    * @param idLogStep a <code>long</code> value
    * @jmx.managed-attribute
    */
   public void setIdLogStep(long idLogStep)
   {
      this.idLogStep = idLogStep;
   }
   
   /**
    * mbean get-set pair for field pad
    * Get the value of pad
    * @return value of pad
    *
    * @jmx.managed-attribute description="Whether to pad xid to the full length.  The Oracle drivers require pad true, all others allow pad false." 
    *                        value="false"
    */
   public boolean isPad()
   {
      return pad;
   }
   
   
   /**
    * Set the value of pad
    * @param pad  Value to assign to pad
    *
    * @jmx.managed-attribute
    */
   public void setPad(boolean pad)
   {
      this.pad = pad;
      if (baseGlobalId != null)
      {
         noBranchQualifier = doPad(baseGlobalId.getBytes());
      }
   }
   
   
   
   
   /**
    * mbean get-set pair for field instance
    * Get the value of instance
    * @return value of instance
    *
    * @jmx.managed-attribute description="Internal method for retrieving the XidFactory instance from the mbean, to avoid calls through the mbean server." access="read-only"
    */
   public XidFactory getInstance()
   {
      return this;
   }
   
   
   /**
    * Describe <code>newXid</code> method here.
    *
    * @return a <code>Xid</code> value
    * @jmx.managed-operation description="Returns a new xid with a new global id." impact="ACTION"
    */
   public Xid newXid() throws SystemException
   {
      byte[] globalId = (baseGlobalId + Long.toString(getNextId())).getBytes();
      return new XidImpl(doPad(globalId), noBranchQualifier);
   }

   /**
    * Describe <code>newBranch</code> method here.
    *
    * @param xid The supplied Xid provides the global id for the new branch.
    * @param branchIdNum The long value to be used to identify the branch.
    * @return a <code>Xid</code> value
    * @jmx.managed-operation description="Constructs an Xid with the same global id as the one supplied, and using the branch number supplied in the branch id" impact="ACTION"
    * @jmx.managed-parameter name="xid" type="javax.transaction.xa.Xid" description="Global (for this tm) xid that needs another branch"

    * @jmx.managed-parameter name="branchIdNum" type="long" description="Supplied branch id number for the new branch"

    */
   public Xid newBranch(Xid xid, long branchIdNum)
   {
      byte[] branchId = (baseGlobalId + Long.toString(branchIdNum)).getBytes();
      return new XidImpl(xid, doPad(branchId));
   }

   /**
    * The <code>originatedHere</code> method tel you whether the global id part
    * of this xid was generated by "this" xid factory.  "This" presumably means 
    * ip address and jndi port or similar info.  The point is to decide whether the
    * tx manager using this xid factory should do the recovery for this transaction.
    *
    * @param xid a <code>Xid</code> value
    * @return a <code>boolean</code> value
    * @jmx.managed-operation description="Determines if the supplied xid was generated by the current xid factory configuration in some running of JBoss" impact="INFO"
    * @jmx.managed-parameter name="xid" type="javax.transaction.xa.Xid" description="Xid that we want to know if it is ours."
    */
   public boolean originatedHere(Xid xid)
   {
      if (xid == null) 
      {
         throw new IllegalArgumentException("originatedHere needs a non-null xid");
      } // end of if ()
      byte[] globalId = xid.getGlobalTransactionId();
      try 
      {
         String globalIdString = new String(globalId);
         //starts with the same base global id string
         if (!globalIdString.startsWith(baseGlobalId)) 
         {
            return false;
         } // end of if ()
         //The rest of it needs to be a number.
         new Long(globalIdString.substring(baseGlobalId.length()));
         return true;
      }
      catch (Exception e)
      {
         return false;
      } // end of try-catch
      
   }


   /**
    * Describe <code>toString</code> method here.
    *
    * @param xid The Xid to be represented as a string.
    * @return a <code>String</code> value
    * @jmx.managed-operation description="provides a string representation of any xid.  I'm not really sure why we don't just use toString()." impact="INFO"
    * @jmx.managed-parameter name="xid" type="javax.transaction.xa.Xid" description="Xid to be stringified."
    */
   public String toString(Xid xid)
   {
      if (xid instanceof XidImpl) 
      {
         return XidImpl.toString((XidImpl)xid);
      } // end of if ()
      else
      {
         return xid.toString();
      } // end of else
   }


   private synchronized long getNextId() throws SystemException
   {
      globalIdNumber++;
      if (txLogger != null && globalIdNumber == lastSavedId + idLogStep)
      {
         try
         {
            txLogger.saveIdCounter(globalIdNumber);
         }
         catch (IOException ioe)
         {
            throw new SystemException("could not write global id counter log:" + ioe);
         }
         lastSavedId = globalIdNumber;
      }
      return globalIdNumber;
   }

   
   private byte[] doPad(byte[] globalId)
   {
      if (pad) 
      {
         byte[] result = new byte[Xid.MAXGTRIDSIZE];
         System.arraycopy(globalId, 0, result, 0, globalId.length);
         globalId = result;
      } // end of if ()
      return globalId;
   }



}// XidFactory
