// SimpleResourceStore.java
// $Id: SimpleResourceStore.java,v 1.3 1998/06/30 15:37:09 bmahe 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 (class name)");
	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() ;
  }
}
