/**********************************************************************************
 * $URL: https://source.sakaiproject.org/svn/db/tags/sakai_2-3-1/db-util/storage/src/java/org/sakaiproject/util/BaseXmlFileStorage.java $
 * $Id: BaseXmlFileStorage.java 7076 2006-03-27 21:25:47Z ggolden@umich.edu $
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
 * 
 * Licensed under the Educational Community License, Version 1.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at
 * 
 *      http://www.opensource.org/licenses/ecl1.php
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.util;

import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.entity.api.Edit;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.time.api.Time;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * <p>
 * BaseXmlFileStorage is a class that stores Resources (of some type) in an XML file <br />
 * backed memory store, provides locked access, and generally implements a service's <br />
 * "storage" class. The service's storage class can extend this to provide covers to <br />
 * turn Resource and Edit into something more type specific to the service.
 * </p>
 */
public class BaseXmlFileStorage
{
	/** Our logger. */
	private static Log M_log = LogFactory.getLog(BaseXmlFileStorage.class);

	/**
	 * Holds the container object, a table of the resources contained.
	 */
	protected class Container
	{
		/** The container Resource object. */
		public Entity container;

		/** The table of contained entry Resources. */
		public Hashtable contained;

		public Container(Entity c)
		{
			container = c;
			contained = new Hashtable();
		}
	}

	/** A full path and file name to the storage file. */
	protected String m_fileStoragePath = null;

	/** The xml tag name for the root element holding the multiple entries. */
	protected String m_rootTagName = null;

	/** The xml tag name for the element holding each container entry. */
	protected String m_containerTagName = null;

	/** The xml tag name for the element holding each actual entry. */
	protected String m_entryTagName = null;

	/** Two level store: Hashtables keyed by container ref of Container. */
	protected Hashtable m_store = null;

	/** Store all locks (across all containers), keyed by entry Resource reference. */
	protected Hashtable m_locks = null;

	/** The StorageUser to callback for new Resource and Edit objects. */
	protected StorageUser m_user = null;

	/** If set, we treat reasource ids as case insensitive. */
	protected boolean m_caseInsensitive = false;

	/**
	 * Construct.
	 * 
	 * @param path
	 *        The storage path.
	 * @param root
	 *        The xml tag name for the root element holding the multiple entries.
	 * @param container
	 *        The xml tag name for the element holding each container entry (may be null if there's no container structure and all entries are in the root).
	 * @param entry
	 *        The xml tag name for the element holding each actual entry.
	 * @param user
	 *        The StorageUser class to call back for creation of Resource and Edit objects.
	 */
	public BaseXmlFileStorage(String path, String root, String container, String entry, StorageUser user)
	{
		m_fileStoragePath = path;
		m_rootTagName = root;
		m_containerTagName = container;
		m_entryTagName = entry;
		m_user = user;
	}

	/**
	 * Clean up.
	 */
	protected void finalize()
	{
		m_user = null;
	}

	/**
	 * Load the Xml Document
	 */
	protected Document load()
	{
		return Xml.readDocument(m_fileStoragePath);
	}

	/**
	 * Open and be ready to read / write.
	 */
	public void open()
	{
		// setup for resources
		m_store = new Hashtable();

		Container top = null;

		// put in a Top Container if we are not doing containers
		if (m_containerTagName == null)
		{
			top = new Container(null);
			m_store.put("", top);
		}

		// setup locks
		m_locks = new Hashtable();

		try
		{
			// read the xml
			Document doc = load();
			if (doc == null)
			{
				M_log.warn("missing user xml file: " + m_fileStoragePath);
				return;
			}

			// verify the root element
			Element root = doc.getDocumentElement();
			if (!root.getTagName().equals(m_rootTagName))
			{
				M_log.warn(".open(): root tag not: " + m_rootTagName + " found: " + root.getTagName());
				return;
			}

			// the children
			NodeList rootNodes = root.getChildNodes();
			final int rootNodesLength = rootNodes.getLength();
			for (int i = 0; i < rootNodesLength; i++)
			{
				Node rootNode = rootNodes.item(i);
				if (rootNode.getNodeType() != Node.ELEMENT_NODE) continue;
				Element rootElement = (Element) rootNode;

				// look for an entry element (entries in the root)
				if ((m_containerTagName == null) && (rootElement.getTagName().equals(m_entryTagName)))
				{
					// re-create the resource and store in the top container
					Entity entry = m_user.newResource(top.container, rootElement);

					top.contained.put(caseId(entry.getId()), entry);
				}

				// look for a container element (containers in the root, entries in the containers)
				else if ((m_containerTagName != null) && (rootElement.getTagName().equals(m_containerTagName)))
				{
					// re-create the container
					Entity containerResource = m_user.newContainer(rootElement);

					// add to the store
					Container container = new Container(containerResource);
					m_store.put(containerResource.getReference(), container);

					// scan for entry children of the container
					NodeList containerNodes = rootElement.getChildNodes();
					final int containerNodesLength = containerNodes.getLength();
					for (int j = 0; j < containerNodesLength; j++)
					{
						Node containerNode = containerNodes.item(j);
						if (containerNode.getNodeType() != Node.ELEMENT_NODE) continue;
						Element containerElement = (Element) containerNode;

						// look for an entry element (entries in the root)
						if (containerElement.getTagName().equals(m_entryTagName))
						{
							// re-create the resource
							Entity entry = m_user.newResource(container.container, containerElement);

							container.contained.put(caseId(entry.getId()), entry);
						}
					}
				}
			}
		}
		catch (Exception e)
		{
			M_log.warn(".open(): ", e);
		}
	}

	/**
	 * Create and return the XML Document for our storaghe
	 */
	protected Document createDocument()
	{
		// create the Dom with a root element
		Document doc = Xml.createDocument();
		Stack stack = new Stack();
		Element root = doc.createElement(m_rootTagName);
		doc.appendChild(root);

		stack.push(root);

		// if we have no containers, store all elements from the Top container under the root
		if (m_containerTagName == null)
		{
			Enumeration e = ((Container) m_store.get("")).contained.elements();
			while (e.hasMoreElements())
			{
				Entity entry = (Entity) e.nextElement();
				entry.toXml(doc, stack);
			}
		}

		// otherwise, process each container
		else
		{
			Enumeration e = m_store.elements();
			while (e.hasMoreElements())
			{
				Container c = (Container) e.nextElement();

				// skip Top
				if (c.container == null) continue;

				// store the container
				Element containerElement = c.container.toXml(doc, stack);

				// push it onto the stack, so entries are created under it
				stack.push(containerElement);

				// store each contained under the container's element
				Enumeration elementEnum = c.contained.elements();
				while (elementEnum.hasMoreElements())
				{
					Entity entry = (Entity) elementEnum.nextElement();
					entry.toXml(doc, stack);
				}

				stack.pop();
			}
		}

		stack.pop();
		return doc;
	}

	/**
	 * flush
	 */
	protected void flush()
	{

		Document doc = createDocument();
		Xml.writeDocument(doc, m_fileStoragePath);
	}

	/**
	 * Close.
	 */
	public void close()
	{

		flush();
		m_locks.clear();
		m_locks = null;
		m_store.clear();
		m_store = null;
	}

	/**
	 * Check if a container by this id exists.
	 * 
	 * @param ref
	 *        The container reference.
	 * @return true if a resource by this id exists, false if not.
	 */
	public boolean checkContainer(String ref)
	{
		Container c = ((Container) m_store.get(ref));
		return (c != null);
	}

	/**
	 * Get the container with this id, or null if not found.
	 * 
	 * @param ref
	 *        The container reference.
	 * @return The container with this id, or null if not found.
	 */
	public Entity getContainer(String ref)
	{
		if (ref == null) return null;
		Container c = ((Container) m_store.get(ref));
		if (c == null) return null;
		return c.container;
	}

	/**
	 * Get a list of all containers.
	 * 
	 * @return A list (Resource) of all containers, or empty if none defined.
	 */
	public List getAllContainers()
	{
		List rv = new Vector();

		if (m_containerTagName == null) return rv;
		if (m_store.size() == 0) return rv;

		Enumeration e = m_store.elements();
		while (e.hasMoreElements())
		{
			Container c = (Container) e.nextElement();
			rv.add(c.container);
		}

		return rv;
	}

	/**
	 * Add a new container with this id.
	 * 
	 * @param ref
	 *        The channel reference.
	 * @return The locked object with this id, or null if the id is in use.
	 */
	public Edit putContainer(String ref)
	{
		// if it's already defined
		Container c = ((Container) m_store.get(ref));
		if (c != null) return null;

		// make an Edit
		Edit edit = m_user.newContainerEdit(ref);

		synchronized (m_locks)
		{
			// if it's in the locks (i.e. it's been put() but not committed
			if (m_locks.get(edit.getReference()) != null) return null;

			// store it in the locks
			m_locks.put(edit.getReference(), edit);
		}

		return edit;
	}

	/**
	 * Return a lock on the container with this id, or null if a lock cannot be made.
	 * 
	 * @param ref
	 *        The container reference.
	 * @return The locked object with this id, or null if a lock cannot be made.
	 */
	public Edit editContainer(String ref)
	{
		Container c = (Container) m_store.get(ref);
		if (c == null) return null;

		synchronized (m_locks)
		{
			// check for a lock in place
			if (m_locks.get(c.container.getReference()) != null) return null;

			// make an Edit
			Edit edit = m_user.newContainerEdit(c.container);

			// store it in the locks
			m_locks.put(edit.getReference(), edit);

			return edit;
		}
	}

	/**
	 * Commit the changes and release the locked container.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to commit.
	 */
	public void commitContainer(Edit edit)
	{
		// make a new Entry from the Edit to update the info store
		Entity updatedContainer = m_user.newContainer(edit);

		// update the store
		Container c = ((Container) m_store.get(updatedContainer.getReference()));
		if (c != null)
		{
			c.container = updatedContainer;
		}
		else
		{
			c = new Container(updatedContainer);
			m_store.put(updatedContainer.getReference(), c);
		}

		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Cancel the changes and release the locked container.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to cancel.
	 */
	public void cancelContainer(Edit edit)
	{
		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Remove this container and all it contains.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to remove.
	 */
	public void removeContainer(Edit edit)
	{
		Container c = ((Container) m_store.get(edit.getReference()));
		if (c != null)
		{
			m_store.remove(c);
			// %%% better cleanup?
		}

		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Check if a resource by this id exists.
	 * 
	 * @param container
	 *        The container id.
	 * @param id
	 *        The id.
	 * @return true if a resource by this id exists, false if not.
	 */
	public boolean checkResource(String container, String id)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return false;

		return c.contained.get(caseId(id)) != null;
	}

	/**
	 * Get the entry with this id, or null if not found.
	 * 
	 * @param container
	 *        The container id.
	 * @param id
	 *        The id.
	 * @return The entry with this id, or null if not found.
	 */
	public Entity getResource(String container, String id)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return null;

		return (Entity) c.contained.get(caseId(id));
	}

	/**
	 * Get all entries.
	 * 
	 * @param container
	 *        The container id.
	 * @return The list (Resource) of all entries.
	 */
	public List getAllResources(String container)
	{
		List rv = new Vector();

		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return rv;
		if (c.contained.size() == 0) return rv;

		rv.addAll(c.contained.values());
		return rv;
	}

	/**
	 * Determine if empty
	 * 
	 * @return true if empty, false if not.
	 */
	public boolean isEmpty(String container)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return true;
		if (c.contained.size() == 0) return true;

		return false;
	}

	/**
	 * Get all entries within a range sorted by id.
	 * 
	 * @param container
	 *        The container id.
	 * @param first
	 *        The first position.
	 * @param last
	 *        The last position.
	 * @return The list (Resource) of all entries within a range sorted by id.
	 */
	public List getAllResources(String container, int first, int last)
	{
		List rv = new Vector();

		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return rv;
		if (c.contained.size() == 0) return rv;

		rv.addAll(c.contained.values());

		Collections.sort(rv);

		// subset by position
		if (first < 1) first = 1;
		if (last >= rv.size()) last = rv.size();

		rv = rv.subList(first - 1, last);

		return rv;
	}

	/**
	 * Count all entries.
	 * 
	 * @param container
	 *        The container id.
	 * @return The count of all entries.
	 */
	public int countAllResources(String container)
	{
		List rv = new Vector();

		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return 0;
		return c.contained.size();
	}

	/**
	 * Add a new entry with this id.
	 * 
	 * @param container
	 *        The container id.
	 * @param id
	 *        The id.
	 * @param others
	 *        Other fields for the newResource call
	 * @return The locked object with this id, or null if the id is in use.
	 */
	public Edit putResource(String container, String id, Object[] others)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return null;

		// if it's already defined
		if (c.contained.get(caseId(id)) != null) return null;

		// make an Edit
		Edit edit = m_user.newResourceEdit(c.container, id, others);

		synchronized (m_locks)
		{
			// if it's in the locks (i.e. it's been put() but not committed
			if (m_locks.get(edit.getReference()) != null) return null;

			// store it in the locks
			m_locks.put(edit.getReference(), edit);
		}

		return edit;
	}

	/**
	 * Return a lock on the entry with this id, or null if a lock cannot be made.
	 * 
	 * @param container
	 *        The container id.
	 * @param id
	 *        The id.
	 * @return The locked object with this id, or null if a lock cannot be made.
	 */
	public Edit editResource(String container, String id)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return null;

		Entity entry = (Entity) c.contained.get(caseId(id));
		if (entry == null) return null;

		synchronized (m_locks)
		{
			// check for a lock in place
			if (m_locks.get(entry.getReference()) != null) return null;

			// make an Edit
			Edit edit = m_user.newResourceEdit(c.container, entry);

			// store it in the locks
			m_locks.put(entry.getReference(), edit);

			return edit;
		}
	}

	/**
	 * Commit the changes and release the lock.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to commit.
	 */
	public void commitResource(String container, Edit edit)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c != null)
		{
			// make a new Entry from the Edit to update the info store
			Entity updatedEntry = m_user.newResource(c.container, edit);

			c.contained.put(caseId(updatedEntry.getId()), updatedEntry);
		}

		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Cancel the changes and release the lock.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to cancel.
	 */
	public void cancelResource(String container, Edit edit)
	{
		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Remove this entry.
	 * 
	 * @param container
	 *        The container id.
	 * @param edit
	 *        The entry to remove.
	 */
	public void removeResource(String container, Edit edit)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c != null)
		{
			// remove from the info store
			c.contained.remove(caseId(edit.getId()));
		}

		// release the lock
		m_locks.remove(edit.getReference());
	}

	/**
	 * Fix the case of resource ids to support case insensitive ids if enabled
	 * 
	 * @param The
	 *        id to fix.
	 * @return The id, case modified as needed.
	 */
	protected String caseId(String id)
	{
		if (m_caseInsensitive)
		{
			return id.toLowerCase();
		}

		return id;
	}

	/**
	 * Enable / disable case insensitive ids.
	 * 
	 * @param setting
	 *        true to set case insensitivity, false to set case sensitivity.
	 */
	protected void setCaseInsensitivity(boolean setting)
	{
		m_caseInsensitive = setting;
	}

	/**
	 * Get resources filtered by date and count and drafts, in descending (latest first) order
	 * 
	 * @param container
	 *        The container id.
	 * @param afterDate
	 *        if null, no date limit, else limited to only messages after this date.
	 * @param limitedToLatest
	 *        if 0, no count limit, else limited to only the latest this number of messages.
	 * @param draftsForId
	 *        how to handle drafts: null means no drafts, "*" means all, otherwise drafts only if created by this userId.
	 * @param pubViewOnly
	 *        if true, include only messages marked pubview, else include any.
	 * @return A list of Message objects that meet the criteria; may be empty
	 */
	public List getResources(String container, Time afterDate, int limitedToLatest, String draftsForId, boolean pubViewOnly)
	{
		if (container == null) container = "";
		Container c = ((Container) m_store.get(container));
		if (c == null) return new Vector();
		if (c.contained.size() == 0) return new Vector();

		List all = new Vector();
		all.addAll(c.contained.values());

		// sort latest date first
		Collections.sort(all, new Comparator()
		{
			public int compare(Object o1, Object o2)
			{
				// if the same object
				if (o1 == o2) return 0;

				// assume they are Resource
				Entity r1 = (Entity) o1;
				Entity r2 = (Entity) o2;

				// get each one's date
				Time t1 = m_user.getDate(r1);
				Time t2 = m_user.getDate(r2);

				// compare based on date
				int compare = t2.compareTo(t1);

				return compare;
			}
		});

		// early out - if no filtering needed
		if ((limitedToLatest == 0) && (afterDate == null) && ("*".equals(draftsForId)) && !pubViewOnly)
		{
			return all;
		}

		Vector selected = new Vector();

		// deal with drafts / date / pubview
		for (Iterator i = all.iterator(); i.hasNext();)
		{
			Entity r = (Entity) i.next();
			Entity candidate = null;
			if (m_user.isDraft(r))
			{
				// if some drafts
				if ((draftsForId != null) && (m_user.getOwnerId(r).equals(draftsForId)))
				{
					candidate = r;
				}
			}
			else
			{
				candidate = r;
			}

			// deal with date if it passes the draft criteria
			if ((candidate != null) && (afterDate != null))
			{
				if (m_user.getDate(candidate).before(afterDate))
				{
					candidate = null;
				}
			}

			// if we want pub view only
			if ((candidate != null) && pubViewOnly)
			{
				if (candidate.getProperties().getProperty(ResourceProperties.PROP_PUBVIEW) == null)
				{
					candidate = null;
				}
			}

			// add it if it passes all criteria
			if (candidate != null)
			{
				selected.add(candidate);
			}
		}

		// pick what we need
		List rv = new Vector();

		if ((limitedToLatest > 0) && (limitedToLatest < selected.size()))
		{
			all = selected.subList(0, limitedToLatest);
		}
		else
		{
			all = selected;
		}

		return all;
	}

	/**
	 * Access a list of container ids that match (start with) the root.
	 * 
	 * @param context
	 *        The reference root to match.
	 * @return A List (String) of container id which match the root.
	 */
	public List getContainerIdsMatching(String context)
	{
		List containers = getAllContainers();
		List rv = new Vector();

		// the id of each container will be the part that follows the root reference
		final int pos = context.length();

		// filter
		for (Iterator i = containers.iterator(); i.hasNext();)
		{
			Entity r = (Entity) i.next();
			String ref = r.getReference();
			if (ref.startsWith(context))
			{
				// check the reference, return the id (what follows the root)
				String id = ref.substring(pos);
				rv.add(id);
			}
		}

		return rv;
	}
}
