// SimpleResourceStore.java
// $Id: SimpleResourceStore.java,v 1.4 1997/07/30 14:02:02 ylafon Exp $  
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.upgrade.from1to2;

import java.util.* ;
import java.io.* ;

class ResourceIndex {
    String identifier = null ;
    int    fileptr    = -1 ;
    int    bytesize   = -1 ;
    byte   bytes[]    = null;

    int getIdentifierUTFLength() {
	int strlen = identifier.length() ;
	int utflen = 0 ;

	for (int i = 0 ; i < strlen ; i++) {
	    int c = identifier.charAt(i);
	    if ((c >= 0x0001) && (c <= 0x007F)) {
		utflen++;
	    } else if (c > 0x07FF) {
		utflen += 3;
	    } else {
		utflen += 2;
	    }
	}
	return utflen + 2 ;
    }

    void setBytes(byte bits[]) {
	this.bytes    = bits;
	this.fileptr  = -1;
	this.bytesize = bits.length;
    }

    ResourceIndex(SimpleResourceStore store, ResourceIndex old) {
	this.identifier = old.identifier ;
	this.fileptr    = -1 ;
	this.bytesize   = -1 ;
    }

    ResourceIndex(SimpleResourceStore store) {
	this.identifier = null ;
	this.fileptr    = -1 ;
	this.bytesize   = -1 ;
    }

}

/**
 * A very naive resource store.
 * This resource store keeps all the resources in a file. It loads all 
 * resources (on a per-demand basis), and never unloads them.
 */

public class SimpleResourceStore {
    /**
     * The resources we know about: maps identifier to resource objects.
     */
    Hashtable resources = null ;
    /**
     * Our underlying associated file.
     */
    File repository = null ;
    /**
     * Has this repository been modified.
     */
    boolean modified = false ;
    /**
     * Do we have a pending save operation ?
     */
    boolean save_pending = false ;

    /**
     * Get our file, positionned at the given position.
     * We won't keep our repository file always open (too much of these
     * resource stroe would burn our available file descriptors).
     * @param at The position at wich you want the stream.
     */

    protected RandomAccessFile getFileAt(int pos)
	throws IOException
    {
	RandomAccessFile fd = new RandomAccessFile(repository, "rw") ;
	fd.seek(pos) ;
	return fd ;
    }

    /**
     * Restore our whole index from our repository.
     */

    protected synchronized void loadIndex(Hashtable target) 
	throws IOException
    {
	// If no index file exists, we are creating it:
	InputStream fin = null ;
	try {
	    fin = new FileInputStream(repository) ;
	} catch (FileNotFoundException ex) {
	    // This resource store is being created, mark modified
	    modified = true;
	    return ;
	}
	// Otherwise load it:
	DataInputStream in  = (new DataInputStream
			       (new BufferedInputStream(fin)));
	int count = in.readInt() ;
	for (int i = 0 ; i < count ; i++) {
	    ResourceIndex index = new ResourceIndex(this) ;
	    index.fileptr    = in.readInt();
	    index.bytesize   = in.readInt() ;
	    index.identifier = in.readUTF() ;
	    target.put(index.identifier, index) ;
	}
	fin.close() ;
    }

    /**
     * Get the bytes that are the pickled version of the given resource.
     * This opens the file for each resource, it could of course
     * be optimized for the cases were all the resources are to be loaded
     * at once.
     * @param identifier The resource identifier.
     * @return The length, in the instance buffer, of the bytes that makes
     *    this resource.
     */

    protected synchronized
    byte[] getResourceBytes(ResourceIndex index, byte into[]) 
	throws IOException
    {
	if ((into == null) || (index.bytesize > into.length))
	    into = new byte[index.bytesize] ;
	RandomAccessFile fd = getFileAt(index.fileptr) ;
	fd.read(into, 0, index.bytesize) ;
	fd.close() ;
	return into ;
    }

    /**
     * Internal save: save the repository back to disk.
     * @param unload Should we unload any existing resources ?
     */

    public synchronized void save()
	throws IOException
    {
	Enumeration           enum       = resources.elements() ;
	ByteArrayOutputStream out        = new ByteArrayOutputStream() ;
	DataOutputStream      dout       = new DataOutputStream(out) ;

	// Emit the store manager prlog (by hand this time):
	dout.writeInt(w3c.tools.store.ResourceStoreManager.STOREFILE_IDENTIFIER);
	dout.writeUTF("w3c.jigsaw.resources.SimpleResourceStore");
	dout.writeInt(1);
	dout.close();
	byte prolog[] = out.toByteArray();
	// Stage 1: pickle all resources to the byte stream:
	out  = new ByteArrayOutputStream();
	dout = new DataOutputStream(out);
	while ( enum.hasMoreElements() ) {
	    // Even if might already have an index entry, we recreate a new
	    // one since we are not sure the save operation will succeed
	    ResourceIndex index = (ResourceIndex) enum.nextElement() ;
	    index.fileptr  = dout.size() + prolog.length;
	    dout.write(index.bytes, 0, index.bytes.length);
	}
	// Stage 2: compute the index size:
	int         nwritten = resources.size() ;
	int         isize    = 4 ;
	Enumeration index    = resources.elements() ;
	while (index.hasMoreElements()) {
	    ResourceIndex entry = (ResourceIndex) index.nextElement() ;
	    isize += (8 + entry.getIdentifierUTFLength());
	}
	// Stage 3: create a suitable temporary file, and associated streams
	File             tmp  = new File(repository.getParent()
					 , repository.getName()+".tmp") ;
	DataOutputStream fout = (new DataOutputStream
				 (new BufferedOutputStream
				  (new FileOutputStream (tmp)))) ;
	// Stage 4: emit the index part of the archive
	fout.write(prolog, 0, prolog.length);
	index = resources.elements() ;
	fout.writeInt(nwritten) ;
	while (index.hasMoreElements()) {
	    ResourceIndex entry = (ResourceIndex) index.nextElement() ;
	    entry.fileptr += isize ;
	    fout.writeInt(entry.fileptr) ;
	    fout.writeInt(entry.bytes.length) ;
	    fout.writeUTF(entry.identifier) ;
	}
	// Stage 5: emit the bits that make the pickled version of resources
 	byte bits[] = out.toByteArray() ;
	fout.write(bits, 0, bits.length) ;
	fout.close() ;
	// Stage 6: Success ! 
	// - Rename the temp file to the repository file.
	// - Exchange our index hashtable for the new one.
	String name = repository.getName() ;
	String dir  = repository.getParent() ;
	File   bak  = new File(dir, name+".bak");
	File   tild = new File(dir, name+".bak~") ;
	
	// 1st move: delete the ~ file if any:
	if ( tild.exists() )
	    tild.delete() ;
	// 2nd move rename bak to ~ (bak no longer exists)
	if ( bak.exists() && ! bak.renameTo(tild))
	    bak.delete() ;
	// 3nd move: rename the current repository to bak
	if ( repository.exists() && ! repository.renameTo(bak) ) {
	    tild.renameTo(bak) ;
	    System.out.println("unable to rename "+repository+" to "+bak) ;
	    return ;
	} 
	// 4th move: rename the tmp file to the repository
	if ( ! tmp.renameTo(repository) ) {
	    bak.renameTo(repository) ;
	    tild.renameTo(bak);
	    System.out.println("unable to rename "+tmp+" to "+repository) ;
	    return ;
	}
	// cleanup (erase the ~ file)
	tild.delete() ;
    }

    public boolean upgrade(String identifier) 
	throws IOException, ResourceUpgraderException
    {
	// Lookup the index entry for that resource:
	ResourceIndex index = (ResourceIndex) resources.get(identifier);
	if ( index == null )
	    return false;
	// Get the old bits, find an appropriate upgrader:
	byte             oldbits[] = getResourceBytes(index, null);
	ResourceUpgrader upgrader = ResourceUpgrader.getUpgrader(oldbits);
	if ( upgrader != null ) {
	    byte newbits[] = upgrader.upgrade(oldbits, 0, index.bytesize);
	    // Hack the index entry:
	    if ( newbits != null ) {
		index.setBytes(newbits);
	    } else {
		System.out.println(identifier+": in store "+repository
				   + ", upgrade failed, using upgrader "
				   + upgrader.getClass().getName());
		return false;
	    }
	} else {
	    index.setBytes(oldbits);
	}
	return true;
    }

    /**
     * Enumerate all the resources identifier in this repository.
     */

    public Enumeration enumerateResourceIdentifiers() {
	return resources.keys() ;
    }

    /**
     * Print a simple resource store.
     */

    public String toString() {
	return repository.getAbsolutePath() ;
    }

    /**
     * Initialize this simple store with the given file.
     * @param manager The resource store manager that loaded use.
     * @param file The repository file.
     */

    public void initialize(File repository) 
	throws IOException
    {
	try {
	    this.repository = repository ;
	    this.resources  = new Hashtable() ;
	    loadIndex(resources) ;
	} catch (IOException ex) {
	    ex.printStackTrace();
	}
    }

}
