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