/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors 
 * as indicated by the @author tags. 
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors. 
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
 * MA  02110-1301, USA.
 * 
 * (C) 2005-2006,
 * @author JBoss Inc.
 */
/*
 * Copyright (C) 1998, 1999, 2000, 2001,
 *
 * Arjuna Solutions Limited,
 * Newcastle upon Tyne,
 * Tyne and Wear,
 * UK.
 *
 * $Id: HashedStore.java 2342 2006-03-30 13:06:17Z  $
 */

package com.arjuna.ats.internal.arjuna.objectstore;

import com.arjuna.ats.arjuna.common.*;
import com.arjuna.ats.arjuna.ArjunaNames;
import com.arjuna.ats.arjuna.coordinator.*;
import com.arjuna.common.util.propertyservice.PropertyManager;
import com.arjuna.ats.arjuna.objectstore.ObjectStore;
import com.arjuna.ats.arjuna.objectstore.ObjectStoreType;
import com.arjuna.ats.arjuna.gandiva.ClassName;
import com.arjuna.ats.arjuna.gandiva.ObjectName;
import com.arjuna.ats.arjuna.state.*;

import com.arjuna.ats.arjuna.logging.tsLogger;
import com.arjuna.ats.arjuna.logging.FacilityCode;

import com.arjuna.common.util.logging.*;

import java.io.*;
import java.io.File;

import com.arjuna.ats.arjuna.exceptions.ObjectStoreException;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.lang.NumberFormatException;

/*
 * Should be derived from FragmentedStore, but we currently
 * don't have such an implementation in Java.
 */

/**
 * The basic shadowing store implementations store the object states in
 * a separate file within the same directory in the object store, determined
 * by the object's type. However, as the number of file entries within the
 * directory increases, so does the search time for finding a specific file.
 * The HashStore implementation hashes object states over many different
 * sub-directories to attempt to keep the number of files in a given
 * directory low, thus improving performance as the number of object states
 * grows.
 *
 * @author Mark Little (mark@arjuna.com)
 * @version $Id: HashedStore.java 2342 2006-03-30 13:06:17Z  $
 * @since JTS 2.0.
 *
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_1 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_1] - HashedStore.create caught: {0}
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_2 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_2] - invalid number of hash directories: {0}. Will use default.
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_3 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_3] - invalid number of hash directories: {0}
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_4 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_4] -  caught exception: {0}
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_5 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_5] - HashedStore.allObjUids - could not pack Uid.
 * @message com.arjuna.ats.internal.arjuna.objectstore.HashedStore_6 [com.arjuna.ats.internal.arjuna.objectstore.HashedStore_6] - HashedStore.allObjUids - could not pack end of list Uid.
 */

public class HashedStore extends ShadowNoFileLockStore
{

    public int typeIs ()
    {
	return ObjectStoreType.HASHED;
    }

    public ClassName className ()
    {
	return ArjunaNames.Implementation_ObjectStore_HashedStore();
    }

    public static ClassName name ()
    {
	return ArjunaNames.Implementation_ObjectStore_HashedStore();
    }

    /*
     * Have to return as a ShadowingStore because of
     * inheritence.
     */

    public static ShadowingStore create ()
    {
	return new HashedStore("");
    }

    public static ShadowingStore create (Object[] param)
    {
	if (param == null)
	    return null;

	String location = (String) param[0];
	Integer shareStatus = (Integer) param[1];
	int ss = ObjectStore.OS_UNSHARED;

	if (shareStatus != null)
	{
	    try
	    {
		if (shareStatus.intValue() == ObjectStore.OS_SHARED)
		    ss = ObjectStore.OS_SHARED;
	    }
	    catch (Exception e)
	    {
		if (tsLogger.arjLoggerI18N.isWarnEnabled())
		{
		    tsLogger.arjLoggerI18N.warn("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_1",
						new Object[]{e});
		}
	    }
	}

	return new HashedStore(location, ss);
    }

    public static ShadowingStore create (ObjectName param)
    {
	if (param == null)
	    return null;
	else
	    return new HashedStore(param);
    }

    /**
     * Given a type name initialise <code>state</code> to contains all of the
     * Uids of objects of that type
     */

    public boolean allObjUids (String tName, InputObjectState state, int match) throws ObjectStoreException
    {
	if (tsLogger.arjLogger.debugAllowed())
	{
	    tsLogger.arjLogger.debug(DebugLevel.FUNCTIONS, VisibilityLevel.VIS_PUBLIC,
				     FacilityCode.FAC_OBJECT_STORE, 
				     "HashedStore.allObjUids("+tName+", "+state+", "+match+")");
	}

	/*
	 * Directory ALWAYS has a trailing '/'
	 */

	String directory = locateStore(getStoreName());
	OutputObjectState store = new OutputObjectState();

	/* Does typename start with a '/' if so skip over */

	if ((tName != null) && (tName.charAt(0) == File.separatorChar))
	    directory = directory + tName.substring(1, tName.length());
	else
	    directory = directory + tName;

	if (!directory.endsWith(File.separator))
	    directory = directory + File.separator;

	File f = new File(directory);
	String[] entry = f.list();

	if ((entry != null) && (entry.length > 0))
	{
	    for (int i = 0; i < entry.length; i++)
	    {
		if ( Character.isDigit(entry[i].charAt(1)) || entry[i].startsWith(HASH_SEPARATOR) )
		{
		    File dir = new File(directory + entry[i]);

		    if (dir.isDirectory())
		    {
			String[] dirEnt = dir.list();
    
			for (int j = 0; j < dirEnt.length; j++)
			{
			    try
			    {
				Uid aUid = new Uid(dirEnt[j], true);

				if (!aUid.valid() || (aUid.equals(Uid.nullUid())))
				{
				    String revealed = revealedId(dirEnt[j]);

				    // don't want to give the same id twice.

				    if (present(revealed, dirEnt))
					aUid = null;
				    else
					aUid = new Uid(revealed);
				}

				if ((aUid.notEquals(Uid.nullUid())) && ((match == ObjectStore.OS_UNKNOWN) ||
				    (isType(aUid, tName, match))))
				{
                    if(scanZeroLengthFiles || new File(dir, dirEnt[j]).length() > 0) {
    				    aUid.pack(store);
                    }
				}
			    }
			    catch (NumberFormatException e)
			    {
				/*
				 * Not a number at start of file.
				 */
			    }
			    catch (IOException e)
			    {
				throw new ObjectStoreException(tsLogger.log_mesg.getString("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_5"));
			    }
			}
		    }
		}
		else
		{
		    // ignore
		}
	    }
	}

	/* terminate list */

	try
	{
	    Uid.nullUid().pack(store);
	}
	catch (IOException e)
	{
	    throw new ObjectStoreException(tsLogger.log_mesg.getString("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_6"));
	}

	state.setBuffer(store.buffer());

	store = null;

	return true;
    }

    /*
     * Protected constructors and destructor
     */

    protected HashedStore ()
    {
	this(ObjectStore.OS_UNSHARED);
    }

    protected HashedStore (int shareStatus)
    {
	super(shareStatus);

	if (tsLogger.arjLogger.debugAllowed())
	{
	    tsLogger.arjLogger.debug(DebugLevel.FUNCTIONS, VisibilityLevel.VIS_PROTECTED,
				     FacilityCode.FAC_OBJECT_STORE, "HashedStore.HashedStore( "+shareStatus+" )");
	}
    }

    protected HashedStore (String locationOfStore)
    {
	this(locationOfStore, ObjectStore.OS_UNSHARED);
    }

    protected HashedStore (String locationOfStore, int shareStatus)
    {
	super(shareStatus);

	if (tsLogger.arjLogger.debugAllowed())
	{
	    tsLogger.arjLogger.debug(DebugLevel.FUNCTIONS, VisibilityLevel.VIS_PROTECTED,
				     FacilityCode.FAC_OBJECT_STORE, "HashedStore.HashedStore("+locationOfStore+")");
	}
	
	try
	{
	    setupStore(locationOfStore);
	}
	catch (ObjectStoreException e)
	{
	    tsLogger.arjLogger.warn(e.getMessage());

	    throw new com.arjuna.ats.arjuna.exceptions.FatalError(e.toString());
	}
    }

    protected HashedStore (ObjectName objName)
    {
	super(objName);
	
	if (tsLogger.arjLogger.debugAllowed())
	{
	    tsLogger.arjLogger.debug(DebugLevel.FUNCTIONS, VisibilityLevel.VIS_PROTECTED,
				     FacilityCode.FAC_OBJECT_STORE, "HashedStore.HashedStore("+objName+")");
	}
	
	try
	{
	    setupStore("");
	}
	catch (ObjectStoreException e)
	{
	    tsLogger.arjLogger.warn(e.getMessage());

	    throw new com.arjuna.ats.arjuna.exceptions.FatalError(e.toString());
	}
    }

    protected String truncate (String value)
    {
	int lastIndex = value.lastIndexOf(HashedStore.HASH_SEPARATOR);
	String toReturn = value;

	if (lastIndex != -1)
	{
	    int nextIndex = value.lastIndexOf(HashedStore.HASH_SEPARATOR, lastIndex - 1);
	    
	    if (nextIndex != -1)
	    {
		char[] bitInbetween = new char[lastIndex - nextIndex - 1];
		boolean isDigit = true;
		
		value.getChars(nextIndex + 1, lastIndex, bitInbetween, 0);
		
		for (int i = 0; (i < bitInbetween.length) && isDigit; i++)
		{
		    if (!Character.isDigit(bitInbetween[i]))
		    {
			isDigit = false;
		    }
		}

		if (isDigit)
		    toReturn = value.substring(lastIndex + 1);
	    }
	}

	return toReturn;
    }
    
    /**
     * @return the file name for the state of the object
     * identified by the Uid and TypeName. 
     */
    
    protected String genPathName (Uid objUid, String tName, int otype) throws ObjectStoreException
    {
	if (tsLogger.arjLogger.debugAllowed())
	{
	    tsLogger.arjLogger.debug(DebugLevel.FUNCTIONS, VisibilityLevel.VIS_PROTECTED,
				     FacilityCode.FAC_OBJECT_STORE, 
				     "HashedStore.genPathName("+objUid+", "+tName+", "+ObjectStore.stateTypeString(otype)+")");
	}

	String storeName = locateStore(getStoreName());
	String fname = null;
	String cPtr = null;
	int uidHash = objUid.hashCode();
	String os = objUid.fileStringForm();
	String hashDir = HashedStore.HASH_SEPARATOR + uidHash % HashedStore.NUMBEROFDIRECTORIES + HashedStore.HASH_SEPARATOR + File.separator;  // make sure hash value is unique in the string

	if ((tName == null) || (tName.length() == 0))
	    cPtr = "";
	else
	{
	    cPtr = tName;

	    /*
	     * Convert Unix separators to 'other', i.e., Windows!
	     */

	    if (FileSystemStore.rewriteSeparator && (cPtr.indexOf(FileSystemStore.unixSeparator) != -1))
	    {
		cPtr = cPtr.replace(FileSystemStore.unixSeparator, File.separatorChar);
	    }
	}

	/*
	 * storeName always ends in '/' so we can remove any
	 * at the start of the type name.
	 */

	if (cPtr.charAt(0) == File.separatorChar)
	    cPtr = cPtr.substring(1, cPtr.length());

	if (cPtr.charAt(cPtr.length() -1) != File.separatorChar)
	    fname = storeName + cPtr + File.separator + hashDir + os;
	else
	    fname = storeName + cPtr + hashDir + os;

	/*
	 * Make sure we don't end in a '/'.
	 */

	if (fname.charAt(fname.length() -1) == File.separatorChar)
	    fname = fname.substring(0, fname.length() -2);

	// mark the shadow copy distinctly
	if (otype == ObjectStore.OS_SHADOW)
	    fname = fname + SHADOWCHAR;

	return fname;
    }

    public static final char SHADOWCHAR = '!';

    private final boolean present (String id, String[] list)
    {
	for (int i = 0; i < list.length; i++)
	{
	    if (list[i].equals(id))
		return true;
	}
	
	return false;
    }
    
    private static final int DEFAULT_NUMBER_DIRECTORIES = 255;
    private static final String HASH_SEPARATOR = "#";
    
    private static int NUMBEROFDIRECTORIES = DEFAULT_NUMBER_DIRECTORIES;

    static
    {
	String numberOfDirs = arjPropertyManager.propertyManager.getProperty(com.arjuna.ats.arjuna.common.Environment.HASHED_DIRECTORIES);

	if (numberOfDirs != null)
	{
	    try
	    {
		Integer i = new Integer(numberOfDirs);
		
		NUMBEROFDIRECTORIES = i.intValue();

		if (NUMBEROFDIRECTORIES <= 0)
		{
		    if (tsLogger.arjLoggerI18N.isWarnEnabled())
		    {
			tsLogger.arjLoggerI18N.warn("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_2",
						    new Object[]{numberOfDirs});
		    }
		    
		    NUMBEROFDIRECTORIES = DEFAULT_NUMBER_DIRECTORIES;
		}
	    }
	    catch (NumberFormatException e)
	    {
		if (tsLogger.arjLoggerI18N.isWarnEnabled())
		{
		    tsLogger.arjLoggerI18N.warn("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_3",
						new Object[]{numberOfDirs});
		}
		
		throw new com.arjuna.ats.arjuna.exceptions.FatalError("Invalid hash directory number: "+numberOfDirs);
	    }
	    catch (Exception e)
	    {
		if (tsLogger.arjLoggerI18N.isWarnEnabled())
		{
		    tsLogger.arjLoggerI18N.warn("com.arjuna.ats.internal.arjuna.objectstore.HashedStore_4",
						new Object[]{e});
		}
		
		throw new com.arjuna.ats.arjuna.exceptions.FatalError(e.toString());
	    }
	}
    }

}

