// FileCache.java
//
//    A cache storing data in files.
//
//    senger@ebi.ac.uk
//    November 2003
//

// TBD: this may go later to the general tools
package org.biomoby.client;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;


/**
 * A simple cache implementation to save and later return (any) data,
 * or to find that the given data are not available in the cache. It
 * uses files to store data. <p>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: FileCache.java,v 1.5 2005/04/07 16:37:02 kawas Exp $
 */
public class FileCache
    implements SimpleFileCache {

    protected static String CACHE_DIR = "cache";
    protected static String fileSeparator;

    protected String rootDirName;
    protected String rootURLName;

    // a directory name (without a full path, usually without any path
    // at all) where the cached objects will be stored (as files) -
    // this directory is always added to the path given in the
    // constructor (relative to the given 'rootDirname')
    protected static String startingDir;

    // index of cached files (TBD: put it also in a file?)
    Hashtable index = new Hashtable();

    /**************************************************************************
     * To be used by the inheriting classes.
     **************************************************************************/
    protected FileCache() {
	fileSeparator = System.getProperty ("file.separator");
	startingDir = CACHE_DIR;
    }

    /**************************************************************************
     * Constructor specifying where to create files with the cached
     * objects. The cache files will be stored in
     *
     * <pre>
     * &lt;rootDirName&gt;/cache/
     * </pre>
     *
     * The all not yet existing directories (for example the last
     * 'cache' directory) will be created for you.
     * <p>
     *
     * 'rootURLName' is used by method {@link #getURL} to return back
     * cached data. It serves as a prefix (perhaps including the
     * protocol, host and port) to returned URLs. In other words:
     * 'rootDirName' tells where to create physical files, and
     * 'rootULName' tells how to find these file using HTTP or other
     * protocols.
     **************************************************************************/
    public FileCache (String rootDirName, String rootURLName) {
	this();
	this.rootDirName = rootDirName;
	this.rootURLName = rootURLName;
    }

    static final char   CLEAR_CHAR = '_';
    /**************************************************************************
     * It creates an 'id' in the form:
     *    rootName / semanticType / { prop1_prop2_... } (time).syntaxType
     *
     * The part between { and } (inclusive) will be replaced by a
     * unique shorter string in order to create a reasonably long real
     * file name.
     **************************************************************************/
    public String createId (String rootName,
			    String semanticType, String syntaxType,
			    long lastModified,
			    Properties props) {
	StringBuffer buf = new StringBuffer ();
	buf.append (clean (rootName));
	buf.append (fileSeparator);
	buf.append (clean (semanticType));
	buf.append (fileSeparator);
	String[] ps = null;
	synchronized (props) {
	    ps = new String [props.size()];
	    int i = -1;
	    for (Iterator it = props.entrySet().iterator(); it.hasNext(); )
		ps[++i] = it.next().toString();
	}
	Arrays.sort (ps);
	buf.append ("{");
	for (int i = 0; i < ps.length; i++) {
 	    buf.append (CLEAR_CHAR);
// 	    buf.append (clean (ps[i].toString()));
	    buf.append (ps[i].toString());
	}
	buf.append ("}");
	buf.append ("(" + lastModified + ")");
	buf.append (".");
	buf.append (clean (syntaxType));
	return new String (buf);
    }

    /**************************************************************************
     * It expects the 'id' in the form as created by 'createId'.
     **************************************************************************/
    public boolean existsInCache (String id) {
	return new File (getFullFilename (id)).exists();
    }

    /**************************************************************************
     * This class does not implement this method. Use {@link
     * #getFilename} and read the returned file yourself.
     **************************************************************************/
    public Object getContents (String id)
	throws IOException {
	return null;
    }

    /**************************************************************************
     * This class does not implement this method. Use {@link
     * #setContents(String,byte[])} to store data in a file.
     **************************************************************************/
    public void setContents (String id, Object data)
	throws IOException {
    }

    /**************************************************************************
     *
     **************************************************************************/
    public String getFilename (String id)
	throws IOException {
	String filename = getFullFilename (id);
	File parent = new File (filename).getParentFile();
	parent.mkdirs();
 	if (parent.exists())
	    return filename;
	throw new IOException ("Cannot create all needed directories: '" + parent.toString() + "'.");
    }

    /**************************************************************************
     *
     **************************************************************************/
    public void setContents (String id, byte[] data)
	throws IOException {
	String filename = getFilename (id);
	File outputFile = new File (filename);
	BufferedOutputStream fileout = new BufferedOutputStream
	    (new FileOutputStream (outputFile));
	fileout.write (data);
	fileout.close();
    }

    /**************************************************************************
     * Return a pointer to cached object identified by its 'id'.
     *
     * If a 'rootURLName' (given in the constructor) is not null it
     * returns:
     *<pre>
     *    &lt;rootURLName&gt;/&lt;name&gt;
     *</pre>
     * where &lt;name&gt; is a name relative to the beginning of the cache
     * (meaning that 'rootDirName' is not included here). If, however,
     * the 'rootURLName ' is null it returns:
     *<pre>
     *    file:&lt;name&gt;
     *</pre>
     * where the &lt;name&gt; is the same as above.
     **************************************************************************/
    public String getURL (String id) {
	if (rootURLName == null)
	    return "file:" + getRelativeFilename (id);
	else
	    return rootURLName + "/" + getRelativeFilename (id);
    }

    /**************************************************************************
     *
     **************************************************************************/
    public void removeFromCache (String id)
	throws IOException {
	// TBD: not yet implemented
    }

    /**************************************************************************
     *
     **************************************************************************/
    public void removeOlderThen (long millis)
	throws IOException {
	// TBD: not yet implemented
    }

    /**************************************************************************
     * Replace non digit/letter characters in 'toBeCleaned' by their
     * numeric value. If there are more such numeric values side by
     * side, put a dot between them. Return the cleaned string.
     **************************************************************************/
    protected String clean (String toBeCleaned) {

	char[] chars = toBeCleaned.toCharArray();
	int len = chars.length;
	int i = -1;
	while (++i < len) {
	    char c = chars[i];
	    if (!Character.isLetterOrDigit (c) && c != '_')
		break;
	}
	if (i < len) {
	    StringBuffer buf = new StringBuffer (len*2);
	    for (int j = 0 ; j < i ; j++) {
		buf.append (chars[j]);
	    }
	    boolean lastOneWasDigitalized = false;
	    while (i < len) {
		char c = chars[i];
		if (Character.isLetterOrDigit (c) || c == '_') {
		    buf.append (c);
		    lastOneWasDigitalized = false;
		} else {
		    if (lastOneWasDigitalized)
			buf.append ('.');
		    buf.append ((int)c);
		    lastOneWasDigitalized = true;
		}
		i++;
	    }
	    return new String (buf);
	}
	return toBeCleaned;
    }

    /**************************************************************************
     * Returns a full file name representing an object identified by
     * its 'id' (but the file still does not need to exist). The path
     * begins in the root dir (as given in the constructor).
     **************************************************************************/
    protected String getFullFilename (String id) {
	return rootDirName + getRelativeFilename (id);
    }

    /**************************************************************************
     * Returns a name of a file (which does not need to exist yet,
     * however) for a cached object identified by the 'id'. The
     * returned name starts with '/' but it is relative to the root
     * dir (as set in a constructor).
     **************************************************************************/
    protected String getRelativeFilename (String id) {
	String fileName = (String)index.get (id);
	if (fileName == null) {
	    int posFrom = id.indexOf ("{");
	    int posTo = id.indexOf ("}");
	    StringBuffer buf = new StringBuffer();
	    buf.append (fileSeparator);
	    buf.append (startingDir);
	    buf.append (fileSeparator);
	    buf.append (id.substring (0, posFrom));
	    buf.append (clean (new java.rmi.server.UID().toString()));
	    buf.append (id.substring (posTo+1));
	    fileName = new String (buf);
	    index.put (id, fileName);
	}
	return fileName;
    }
}
