// SimpleResourceStore.java
// $Id: SimpleResourceStore.java,v 1.4 1999/07/08 12:57:46 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html
package org.w3c.tools.resources.store ;

import org.w3c.tools.resources.*;

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

class ResourceIndex {
    SimpleResourceStore store     = null ;
    String identifier = null ;
    int    fileptr    = -1 ;
    int    bytesize   = -1 ;
    Resource resource = null ;
    boolean modified = false ;

    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 ;
    }

    synchronized void unloadResource() {
	if ( resource == null )
	    return ;
	// notify the resource of unload:
	resource.notifyUnload() ;
	resource = null;
    }

    synchronized Resource isLoaded() {
	return resource;
    }

    synchronized Resource loadResource(Hashtable defs)
	throws IOException
	{
	    // Fast case:
	    if ( resource != null ) 
		return resource ;
	    // Slow case:
	    DataInputStream in = (new DataInputStream
				  (new ByteArrayInputStream
				   (store.getResourceBytes(this, null)))) ;
	    resource = (Resource) AttributeHolder.unpickle(in, defs) ;
	    return resource ;
	}

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

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

}

/**
 * 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 implements ResourceStore {
    /**
     * Dump exceptions when unloading a resource fails.
     */
    private static final boolean debugunload = true;

    private static final boolean debug = false;
    /**
     * The store format version number.
     */
    private static final int VERSION = 1;

    /**
     * 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 ;
    /**
     * Our resource store manager.
     */
    protected ResourceStoreManager manager = null ;
    /**
     * Our token within the resource store manager.
     */
    Object token = null;

    /**
     * Mark the store as having been used recently.
     */

    protected final void markUsed() {
	if ( manager != null )
	    manager.markUsed(token) ;
    }

    /**
     * Get the identifier for that store.
     * We use the repository as an identifier, which is safe here.
     * @return The absolute path of the repository.
     */

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

    /**
     * Emit the given string as a warning, to whoever it is appropriate.
     * @param msg The warning message.
     */

    protected void warning(String msg) {
	System.out.println("[" + getClass().getName() 
			   + "@" + repository
			   + "]: " + msg) ;
    }
     
    /**
     * 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)));
	    try {
		ResourceStoreManager.unpickleProlog(in, this);
	    } catch (NotAStoreException ex) {
		// That's really bad, let's just restart the stream, and see:
		System.err.println("*** Prolog doesn't match !!");
		in = (new DataInputStream(new BufferedInputStream(fin)));
	    } catch (InvalidStoreClassException ex) {
		// That's due to some changes between 1.0alpha4 and alpha5:
		String compname = "org.w3c.tools.resources.store."+
		    "SimpleResourceStore";
		if ( ! ex.getSaverClassName().equals(compname) ) {
		    System.err.println("*** Prolog doesn't match (classname)");
		    in = (new DataInputStream(new BufferedInputStream(fin)));
		}
	    } catch (StoreClassVersionException ex) {
		// We really don't know how to handle this, again restart:
		System.err.println("*** Prolog doesn't match (versions)");
		in = (new DataInputStream(new BufferedInputStream(fin)));
	    }
	    // Read in the store:
	    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() ;
		index.modified   = false ;
		index.resource   = null ;
		target.put(index.identifier, index) ;
	    }
	    fin.close() ;
	}

    /**
     * Debug - print this resource store index.
     */

    protected void printIndex() {
	Enumeration enum = resources.elements() ;
	while (enum.hasMoreElements() ) {
	    ResourceIndex entry = (ResourceIndex) enum.nextElement() ;
	    System.out.println(entry.identifier
			       + " " + entry.bytesize 
			       + "@" +entry.fileptr);
	}
    }

    /**
     * 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 ;
    }

    /**
     * Pickle the given resource into the given output stream.
     * @param resource The resource to pickle.
     * @param out The data output stream to pickle to.
     * @return The size of the pickled version of the resource.
     */
    
    protected int pickleResource(Resource resource, DataOutputStream out) 
	throws IOException
    {
	int start = out.size() ;
	resource.pickle(out) ;
	return out.size() - start ;
    }

    /**
     * Get the store format version number.
     * @return An ineteger version number, guaranteed to change whenever the
     * format used by the store changes.
     */

    public int getVersion() {
	return VERSION;
    }

    /**
     * Load a resource, or get one from the cache.
     * @param identifier The resource identifier.
     * @return A Resource instance, or <strong>null</strong> if the resource
     * doesn't exist in that store.
     * @exception InvalidResourceException If the resource couldn't be 
     * restored from its pickled format.
     */
    
    public Resource loadResource(String identifier, Hashtable defs)
	throws InvalidResourceException
    {
	if (debug) {
	    System.out.println("[STORE] load resource : "+identifier+" in "+
			       repository.getAbsolutePath());
	}
	ResourceIndex index = (ResourceIndex) resources.get(identifier) ;
	if ( index == null )
	    return null ;
	// Load it, but don't bother locking the store
	if ( defs == null )
	    defs = new Hashtable(3);
	defs.put("store-entry", index);
	try {
	    return index.loadResource(defs);
	} catch (HolderInitException ex) {
	    if ( debugunload ) {
		System.err.println("* "+getClass().getName()+".debugunload:");
		ex.printStackTrace();
	    } 
	    UnknownResource unknown = new UnknownResource();
	    unknown.initialize(defs);
	    return unknown;
	    // Throw an invalid resource exception:
	    // throw new InvalidResourceException(identifier, ex.getMessage());
	} catch (IOException ex) {
	    if ( debugunload ) {
		System.err.println("* "+getClass().getName()+".debugunload:");
		ex.printStackTrace();
	    }
	    throw new InvalidResourceException(identifier, ex.getMessage());
	}
    }

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

    protected synchronized void internalSave(boolean unload) 
	throws IOException
    {
	Enumeration           enum       = resources.elements() ;
	Hashtable             nresources = new Hashtable(resources.size()+1) ;
	ByteArrayOutputStream out        = new ByteArrayOutputStream() ;
	DataOutputStream      dout       = new DataOutputStream(out) ;
	byte                  bits[]     = null ;
	
	// Stage 0: pickle the store prolog:
	ResourceStoreManager.pickleProlog(dout, this);
	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() ;
	    ResourceIndex entry = new ResourceIndex(this, index) ;
	    if ((index.resource == null) || ( ! index.modified)) {
		bits = getResourceBytes(index, bits) ;
		entry.resource   = index.resource ;
		entry.fileptr    = dout.size() + prolog.length;
		entry.bytesize   = index.bytesize ;
		dout.write(bits, 0, entry.bytesize) ;
	    } else {
		entry.resource   = index.resource ;
		entry.fileptr    = dout.size() + prolog.length;
		entry.bytesize   = pickleResource(index.resource, dout) ;
	    }
	    if ( unload && (index.resource != null)) {
		index.unloadResource() ;
		index.resource = null ;
		entry.resource = null ;
	    }
	    if ( entry.resource != null )
		entry.resource.setValue("store-entry", entry);
	    nresources.put(entry.identifier, entry) ;
	}    
	// Stage 2: compute the index size:
	int         nwritten = nresources.size() ;
	int         isize    = 4 ;
	Enumeration nindex   = nresources.elements() ;
	while (nindex.hasMoreElements()) {
	    ResourceIndex entry = (ResourceIndex) nindex.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);
	nindex = nresources.elements() ;
	fout.writeInt(nwritten) ;
	while (nindex.hasMoreElements()) {
	    ResourceIndex entry = (ResourceIndex) nindex.nextElement() ;
	    entry.fileptr += isize ;
	    fout.writeInt(entry.fileptr) ;
	    fout.writeInt(entry.bytesize) ;
	    fout.writeUTF(entry.identifier) ;
	}
	// Stage 5: emit the bits that make the pickled version of resources
	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() ) { 
	    if ( ! repository.renameTo(bak) ) {
		warning("unable to rename "+repository+" to "+bak) ;
		tild.renameTo(bak) ;
	    }
	    repository.delete();
	}
	// 4th move: rename the tmp file to the repository
	if ( ! tmp.renameTo(repository) ) {
	    bak.renameTo(repository) ;
	    tild.renameTo(bak);
	    warning("unable to rename "+tmp+" to "+repository) ;
	}
	// cleanup (erase the ~ file)
	tild.delete() ;
	save_pending = false ;
	modified     = false ;
	resources    = nresources ;
    }

    /**
     * Lookup this resource.
     * @param identifier The resource identifier.
     * @return A Resource instance, or <strong>null</strong> if either the
     *    resource doesn't exist, or it isn't loaded yet.
     */
    
    public Resource lookupResource (String identifier) {
	ResourceIndex index = (ResourceIndex) resources.get(identifier) ;
	markUsed() ;
	return ((index == null) ? null : index.resource ) ;
    }
    
    /**
     * Save a given resource.
     * If the resource has been modified, we save <em>all</em> our resources.
     * @param resource The resource to be save right now.
     */

    public void saveResource(Resource resource) {
	if (debug) {
	    System.out.println("[STORE] save resource : "
			       +resource.getIdentifier()+
			       " in "+repository.getAbsolutePath()
			       +" ? "+modified);
	}
	ResourceIndex index = (ResourceIndex) resource.getStoreEntry();
	if ( index == null )
	    throw new UnknownResourceException(resource);
	if (index.modified)
	    save() ;
	markUsed() ;
    }


    /**
     * Add a new resource to the resource store.
     * @param resource The resource to add.
     */

    public synchronized void addResource(Resource resource) {
	if (debug) {
	    System.out.println("[STORE] add resource "+
			       resource.getIdentifier()+
			       " to "+repository.getAbsolutePath());
	}    ResourceIndex index = new ResourceIndex(this) ;
	index.fileptr = -1 ;
	index.bytesize = -1 ;
	index.identifier = resource.getIdentifier() ;
	index.modified = true ;
	index.resource = resource ;
	resource.setValue("store-entry", index);
	resources.put(resource.getIdentifier(), index) ;
	modified = true ;
	markUsed() ;
    }

    /**
     * Remove a resource from this resource store.
     * @param identifier The identifier of the resource to be removed.
     */

    public synchronized void removeResource(String identifier) {
	if (debug) {
	    System.out.println("[STORE] remove resource "+identifier+" from "+
			       repository.getAbsolutePath());
	}
	ResourceIndex index = (ResourceIndex) resources.get(identifier) ;
	if ( index != null ) {
	    index.unloadResource() ;
	    resources.remove(identifier) ;
	    modified = true ;
	    markUsed() ;
	}
	return ;
    }

    /**
     * Rename a resource in the store.
     * @param oldid The old resource identifier.
     * @param newid The new resource identifier.
     */

    public synchronized void renameResource(String oldid, String newid) {
	if (debug) {
	    System.out.println("[STORE] rename resource "+oldid+
			       " to "+newid+" in "+
			       repository.getAbsolutePath());
	}    
	ResourceIndex index = (ResourceIndex) resources.get(oldid);
	if (index != null) {
	    resources.remove(oldid);
	    index.identifier = newid;
	    resources.put(newid, index);
	    index.modified = true;
	    modified       = true;
	}
    }

    /**
     * Mark the given resource as being modified.
     * @param identifier The identifier of the modified resource.
     */

    public synchronized void markModified(Resource resource) {
	if (debug) {
	    System.out.println("[STORE] mark resource "+
			       resource.getIdentifier()+
			       " modified in "+repository.getAbsolutePath());
	}    
	ResourceIndex index = (ResourceIndex) resource.getStoreEntry();
	if ( index != null ) {
	    index.modified = true ;
	    modified       = true ;
	    markUsed();
	}
    }

    /**
     * Can the resource store manager unload that store from memory.
     * We check all the loaded resource, to see if they are willing to be
     * unloaded; we do unload straight all the resources that accept (to
     * reduce memory usage), and return <strong>false</strong> only if one
     * of our resource refuse to be unloaded.
     */

    public synchronized boolean acceptUnload() {
	Enumeration e      = resources.elements();
	boolean     accept = true;
	while ( e.hasMoreElements() ) {
	    ResourceIndex entry    = (ResourceIndex) e.nextElement();
	    Resource      resource = entry.isLoaded();
	    if (resource != null) {
		synchronized(entry) {
		    if ( resource.acceptUnload() ) {
			if ( ! entry.modified )
			    entry.unloadResource();
		    } else {
			accept = false;
		    }
		}
	    }
	}
	if ( ! accept ) {
	    // Let's at least do an internal save, so next time, we'll unload
	    // some resources:
	    try {
		if ( modified ) 
		    internalSave(false);
	    } catch (IOException ex) {
		ex.printStackTrace();
		warning("internalSave failed at acceptUnload");
	    }
	}
	return accept;
    }
    
    /**
     * Shutdown this store.
     */

    public synchronized void shutdown() {
	if (debug) {
	    System.out.println("[STORE] store shutdowned : "+getIdentifier());
	    System.out.println("\tand save ? "+modified);
	}
	if ( modified ) {
	    try {
		internalSave(true) ;
	    } catch (IOException ex) {
		ex.printStackTrace();
		warning("internalSave failed at shutdown.") ;
	    }
	} else {
	    // Just notify the unload of loaded resources:
	    Enumeration entries = resources.elements() ;
	    while ( entries.hasMoreElements() ) {
		ResourceIndex index = (ResourceIndex) entries.nextElement() ;
		index.unloadResource() ;
	    }
	}
	// Clean-up all references we have to external objects:
	resources  = null ;
	manager    = null ;
    }

    /**
     * Save all our modified resources back to our file.
     */

    public synchronized void save() {
	if (debug) {
	    System.out.println("[STORE] save store "+
			       repository.getAbsolutePath()+
			       " ? "+modified);
	}
	if ( modified ) {
	    try {
		internalSave(false) ;
	    } catch (IOException ex) {
		warning("Save failed ["+ex.getMessage()+"]");
	    }
	}
    }
    
    /**
     * Enumerate all the resources identifier in this repository.
     */

    public Enumeration enumerateResourceIdentifiers() {
	if (debug) {
	    System.out.println("[STORE] enumerateResourceIdentifiers "
			       +"for store : " + repository.getAbsolutePath());
	}
	markUsed() ;
	return resources.keys() ;
    }
    
    /**
     * Does this store defines this resource.
     * @param identifier The resource we are looking for.
     * @return A boolean <strong>true</strong> if found.
     */

    public boolean hasResource(String identifier) {
	markUsed() ;
	return resources.get(identifier) != null ;
    }

    /**
     * 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 token Our identification token from the above resource store
     * manager (this is an opaque object).
     * @param file The repository file.
     */

    public void initialize(ResourceStoreManager manager
			   , Object token
			   , File repository) {
	try {
	    this.manager    = manager ;
	    this.token      = token;
	    this.repository = repository ;
	    this.resources  = new Hashtable(11) ;
	    loadIndex(resources) ;
	    markUsed();
	} catch (IOException ex) {
	    warning("unable to load index.");
	}
    }

    public static void main(String args[])
	throws Exception
    {
	File                 repository = new File(args[0]) ;
	SimpleResourceStore store      = new SimpleResourceStore() ;
	store.initialize(null, null, repository) ;
	// List all available resources:
	Enumeration enum = store.enumerateResourceIdentifiers() ;
	while (enum.hasMoreElements()) {
	    String   id       = (String) enum.nextElement() ;
	    try {
		Resource resource = store.loadResource(id, null);
		System.out.println(id+":") ;
		resource.print(System.out) ;
	    } catch (Exception ex) {
		System.out.println("*** unable to load \""+id+"\"");
		ex.printStackTrace();
	    }
	}
	// Shutdown the store:
	store.shutdown() ;
    }
}
