// ResourceStoreImpl.java
// $Id: ResourceStoreImpl.java,v 1.14 2003/06/06 14:23:14 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.AttributeHolder;
import org.w3c.tools.resources.InvalidResourceException;
import org.w3c.tools.resources.Resource;

import org.w3c.tools.resources.serialization.Serializer;
import org.w3c.tools.resources.serialization.SerializationException;
import org.w3c.util.EmptyEnumeration;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.io.Writer;

/**
 * A generic resource store that keeps resource in a file using
 * the Serializer interface.
 */

public class ResourceStoreImpl implements ResourceStore {
    
    static final int writerSize = 65536;

    class ResourceIndex {
	
	boolean  modified    = false;
	String   identifier  = null;
	Resource resource    = null;
	boolean  initialized = false;

	synchronized void markModified() {
	    modified = true;
	}

	synchronized boolean isModified() {
	    return modified;
	}

	synchronized Resource loadResource(Hashtable defs) {
	    if (initialized) {
		return resource;
	    } else {
		resource.initialize(defs);
		initialized = true;
		return resource;
	    }
	}

	synchronized Resource getResource() {
	    return resource;
	}

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

	synchronized String getIdentifier() {
	    return identifier;
	}

	synchronized void setIdentifier(String identifier) {
	    this.identifier = identifier;
	}

	ResourceIndex(Resource resource, boolean initialized) {
	    this.resource    = resource;
	    this.identifier  = resource.unsafeGetIdentifier();
	    this.modified    = false;
	    this.initialized = initialized;
	}
	
    }

    /**
     * The store format version number.
     */
    private static final int VERSION = 2;
    /**
     * Our Resource Serializer.
     */
    protected Serializer serializer = null;
    /**
     * Our underlying associated file.
     */
    File repository = null ;
    /**
     * Our resource store manager.
     */
    protected ResourceStoreManager manager = null ;
    /**
     * Our token within the resource store manager.
     */
    Object token = null;
    /**
     * The resources we know about: maps identifier to resource objects.
     */
    Hashtable resources = null ;
    /**
     * Has this repository been modified.
     */
    boolean modified = false ;

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

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

    // be smart here
    // FIXME removed the synchronized to avoid a deadlock
    protected void markModified() {
	if (!modified) {
	    synchronized (this) {
		modified = true;
	    }
	}
    }

    /**
     * Get the version of that resource store.
     * Version numbers are used to distinguish between pickling format. 
     * A resource store implementator has the duty of bumping the returned
     * number whenever it's archiving format changes.
     * Resource stores that relies on some external archiving mechanisms
     * (such as a database), may return a constant.
     * @return An integer version number.
     */

    public int getVersion() {
	return VERSION;
    }

    /**
     * Get the identifier for that store.
     * @return A uniq store identifier, as a String.
     */

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

    /**
     * Restore the resource whose name is given.
     * This method doesn't assume that the resource will actually be restored,
     * it can be kept in a cache by the ResourceStore object, and the cached 
     * version of the resource may be returned.
     * @param identifier The identifier of the resource to restore.
     * @param defs Default attribute values. If the resource needs to be
     *     restored from its pickled version, this Hashtable provides
     *     a set of default values for some of the attributes.
     * @return A Resource instance, or <strong>null</strong>.
     * @exception InvalidResourceException If the resource could not
     * be restored from the store.
     */

    public Resource loadResource(String identifier, Hashtable defs)
	throws InvalidResourceException
    {
	loadResources();
	markUsed();
	ResourceIndex index = (ResourceIndex) resources.get(identifier) ;
	if ( index == null )
	    return null;
	if ( defs == null )
	    defs = new Hashtable(3);
	defs.put("store-entry", index);
	return index.loadResource(defs);
    }

    /**
     * Get this resource, but only if already loaded.
     * The resource store may (recommended) maintain a cache of the resource
     * it loads from its store. If the resource having this identifier 
     * has already been loaded, return it, otherwise, return
     * <strong>null</strong>.
     * @param identifier The resource identifier.
     * @return A Resource instance, or <strong>null</strong>.
     */

    public Resource lookupResource(String identifier) {
	loadResources();
	markUsed();
	ResourceIndex index = (ResourceIndex) resources.get(identifier);
	return (((index == null) || (! index.initialized)) 
		? null : index.getResource() );
    }

    /**
     * Stabilize the given resource.
     * @param resource The resource to save.
     */

    public void saveResource(Resource resource) {
	loadResources();
	ResourceIndex index = (ResourceIndex) resource.getStoreEntry();
	if ( index == null )
	    throw new UnknownResourceException(resource);
	if (index.isModified())
	    save() ;
	markUsed() ;
    }

    /**
     * Add this resource to this resource store.
     * @param resource The resource to be added.
     */

    public synchronized void addResource(Resource resource) {
	loadResources();
	ResourceIndex index = new ResourceIndex(resource, true);
	index.markModified();
	resource.setValue("store-entry", index);
	resources.put(index.getIdentifier(), index);
	markModified();
	markUsed();
    }

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

    public synchronized void removeResource(String identifier) {
	ResourceIndex index = (ResourceIndex) resources.get(identifier);
	if ( index != null ) {
	    index.unloadResource();
	    resources.remove(identifier);
	    markModified();
	    markUsed();
	}
    }

    /**
     * Rename a given resource.
     * @param oldid The olde resource identifier.
     * @param newid The new resource identifier.
     */

    public synchronized void renameResource(String oldid, String newid) {
	ResourceIndex index = (ResourceIndex) resources.get(oldid);
	if (index != null) {
	    resources.remove(oldid);
	    index.setIdentifier(newid);
	    resources.put(newid, index);
	    index.markModified();
	    markModified();
	}
    }

    /**
     * Mark this resource as modified.
     * @param resource The resource that has changed (and will have to be
     * pickled some time latter).
     */

    public void markModified(Resource resource) {
	ResourceIndex index = (ResourceIndex) resource.getStoreEntry();
	if ( index != null ) {
	    index.markModified();
	    markModified() ;
	    markUsed();
	}
    }

    /**
     * Can this resource store be unloaded now ?
     * This method gets called by the ResourceStoreManager before calling
     * the <code>shutdown</code> method, when possible. An implementation
     * of that method is responsible for checking the <code>acceptUnload
     * </code> method of all its loaded resource before returning 
     * <strong>true</strong>, meaning that the resource store can be unloaded.
     * @return A boolean <strong>true</strong> if the resource store can be
     * unloaded.
     */

    public synchronized boolean acceptUnload() {
	boolean accept = true;
	if ((manager != null) &&
	    (manager.getStoreSizeLimit() > 0) &&
	    resources.size() > manager.getStoreSizeLimit()) {
	    accept = false;
	} else {
	    Enumeration e      = resources.elements();
	    while ( e.hasMoreElements() ) {
		ResourceIndex entry    = (ResourceIndex) e.nextElement();
		Resource      resource = entry.getResource();
		synchronized (entry) {
		    if (! resource.acceptUnload() )
			accept = false;
		}
	    }
	}
	if ( ! accept ) {
	    try {
		if ( modified ) 
		    internalSave(false);
	    } catch (IOException ex) {
		ex.printStackTrace();
		warning("internalSave failed at acceptUnload");
	    }
	}
	return accept;
    }

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

    protected synchronized void internalSave(boolean unload) 
	throws IOException
    {
	//nothing to save
	if (resources == null)
	    return;
	//1st, build the resource array
	Enumeration enum = resources.elements();
	Vector      vres = new Vector(11);
	while (enum.hasMoreElements()) {
	    Resource res = ((ResourceIndex)enum.nextElement()).getResource();
	    vres.addElement(res);
	}
	Resource resourcearray[] = new Resource[vres.size()];
	vres.copyInto(resourcearray);

	//try to save in a temporary file
	File tmp = new File(repository.getParent(),
			    repository.getName()+".tmp") ;
	FileOutputStream fos = new FileOutputStream(tmp);
	OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
	Writer writer = new BufferedWriter( osw, writerSize);

	serializer.writeResources(resourcearray, writer);

	//success!!
	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() ;
	modified = false;
	//unload if needed
	if (unload) {
	    for (int i = 0 ; i < resourcearray.length ; i++)
		resourcearray[i].notifyUnload();
	    resources = null;
	}
    }

    /**
     * Shutdown this store.
     */

    public void shutdown() {
	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 this store.
     */

    public void save() {
	if ( modified ) {
	    try {
		internalSave(false) ;
	    } catch (IOException ex) {
		warning("Save failed ["+ex.getMessage()+"]");
	    }
	}
    }

    /**
     * Enumerate all the resources saved in this store.
     * @return An enumeration of Strings, giving the identifier for all 
     *     the resources that this store knows about.
     */

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

    /**
     * Check for the existence of a resource in this store.
     * @param identifier The identifier of the resource to check.
     * @return A boolean <strong>true</strong> if the resource exists
     *    in this store, <strong>false</strong> otherwise.
     */

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

    protected void loadResources() {
	if (resources == null) {
	    synchronized (this) {
		if (resources == null) {
		    try {
			resources = new Hashtable(11);
			if (repository.exists()) {
			    Reader reader = 
				new BufferedReader(new FileReader(repository));
			    Resource resourceArray[] = 
				serializer.readResources(reader);
			    for (int i = 0 ; i < resourceArray.length ; i++) {
				ResourceIndex entry = 
				    new ResourceIndex(resourceArray[i], false);
				resources.put(entry.getIdentifier(), entry);
			    }
			}
		    } catch (IOException ioex) {
			ioex.printStackTrace();
			warning("Unable to load resources");
		    } catch (SerializationException sex) {
			warning(sex.getMessage());
			sex.printStackTrace();
		    } catch (Exception ex) {
			String err = "Error in " + repository.getName() +
			    " in dir " + repository.getParent() + ":" +
			    ex.getMessage();
			warning(err);
		    }
		}
	    }
	}
    }

    /**
     * This resource store is being built, initialize it with the given arg.
     * @param manager The ResourceStoreManager instance that asks yourself
     * to initialize.
     * @param token The resource store manager key to that resource store, 
     * this token should be used when calling methods from the manager that
     * are to act on yourself.
     * @param repository A file, giving the location of the associated 
     *    repository.
     */
    public void initialize(ResourceStoreManager manager,
			   Object token,
			   File repository,
			   Serializer serializer) 
    {
	this.manager    = manager;
	this.token      = token;
	this.repository = repository;
	this.serializer = serializer;
	this.resources  = null;
	markUsed();
    }

}
