/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-util/src/main/java/org/sakaiproject/util/BaseXmlFileStorage.java $
* $Id: BaseXmlFileStorage.java 80991 2010-08-09 15:46:37Z david.horwitz@uct.ac.za $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2008 Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.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/ECL-2.0
*
* 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>
* @deprecated This was used in earlier versions of Sakai and isn't supported any longer.
*/
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;
}
/**
* Load the Xml Document
*/
protected Document load()
{
return StorageUtils.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 = StorageUtils.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();
StorageUtils.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)
{
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
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;
}
}