001: /**********************************************************************************
002: * $URL: https://source.sakaiproject.org/svn/db/tags/sakai_2-4-1/db-util/storage/src/java/org/sakaiproject/util/BaseXmlFileStorage.java $
003: * $Id: BaseXmlFileStorage.java 7076 2006-03-27 21:25:47Z ggolden@umich.edu $
004: ***********************************************************************************
005: *
006: * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
007: *
008: * Licensed under the Educational Community License, Version 1.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.opensource.org/licenses/ecl1.php
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: **********************************************************************************/package org.sakaiproject.util;
021:
022: import java.util.Collections;
023: import java.util.Comparator;
024: import java.util.Enumeration;
025: import java.util.Hashtable;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Stack;
029: import java.util.Vector;
030:
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033: import org.sakaiproject.entity.api.Edit;
034: import org.sakaiproject.entity.api.Entity;
035: import org.sakaiproject.entity.api.ResourceProperties;
036: import org.sakaiproject.time.api.Time;
037: import org.w3c.dom.Document;
038: import org.w3c.dom.Element;
039: import org.w3c.dom.Node;
040: import org.w3c.dom.NodeList;
041:
042: /**
043: * <p>
044: * BaseXmlFileStorage is a class that stores Resources (of some type) in an XML file <br />
045: * backed memory store, provides locked access, and generally implements a service's <br />
046: * "storage" class. The service's storage class can extend this to provide covers to <br />
047: * turn Resource and Edit into something more type specific to the service.
048: * </p>
049: */
050: public class BaseXmlFileStorage {
051: /** Our logger. */
052: private static Log M_log = LogFactory
053: .getLog(BaseXmlFileStorage.class);
054:
055: /**
056: * Holds the container object, a table of the resources contained.
057: */
058: protected class Container {
059: /** The container Resource object. */
060: public Entity container;
061:
062: /** The table of contained entry Resources. */
063: public Hashtable contained;
064:
065: public Container(Entity c) {
066: container = c;
067: contained = new Hashtable();
068: }
069: }
070:
071: /** A full path and file name to the storage file. */
072: protected String m_fileStoragePath = null;
073:
074: /** The xml tag name for the root element holding the multiple entries. */
075: protected String m_rootTagName = null;
076:
077: /** The xml tag name for the element holding each container entry. */
078: protected String m_containerTagName = null;
079:
080: /** The xml tag name for the element holding each actual entry. */
081: protected String m_entryTagName = null;
082:
083: /** Two level store: Hashtables keyed by container ref of Container. */
084: protected Hashtable m_store = null;
085:
086: /** Store all locks (across all containers), keyed by entry Resource reference. */
087: protected Hashtable m_locks = null;
088:
089: /** The StorageUser to callback for new Resource and Edit objects. */
090: protected StorageUser m_user = null;
091:
092: /** If set, we treat reasource ids as case insensitive. */
093: protected boolean m_caseInsensitive = false;
094:
095: /**
096: * Construct.
097: *
098: * @param path
099: * The storage path.
100: * @param root
101: * The xml tag name for the root element holding the multiple entries.
102: * @param container
103: * 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).
104: * @param entry
105: * The xml tag name for the element holding each actual entry.
106: * @param user
107: * The StorageUser class to call back for creation of Resource and Edit objects.
108: */
109: public BaseXmlFileStorage(String path, String root,
110: String container, String entry, StorageUser user) {
111: m_fileStoragePath = path;
112: m_rootTagName = root;
113: m_containerTagName = container;
114: m_entryTagName = entry;
115: m_user = user;
116: }
117:
118: /**
119: * Clean up.
120: */
121: protected void finalize() {
122: m_user = null;
123: }
124:
125: /**
126: * Load the Xml Document
127: */
128: protected Document load() {
129: return Xml.readDocument(m_fileStoragePath);
130: }
131:
132: /**
133: * Open and be ready to read / write.
134: */
135: public void open() {
136: // setup for resources
137: m_store = new Hashtable();
138:
139: Container top = null;
140:
141: // put in a Top Container if we are not doing containers
142: if (m_containerTagName == null) {
143: top = new Container(null);
144: m_store.put("", top);
145: }
146:
147: // setup locks
148: m_locks = new Hashtable();
149:
150: try {
151: // read the xml
152: Document doc = load();
153: if (doc == null) {
154: M_log.warn("missing user xml file: "
155: + m_fileStoragePath);
156: return;
157: }
158:
159: // verify the root element
160: Element root = doc.getDocumentElement();
161: if (!root.getTagName().equals(m_rootTagName)) {
162: M_log.warn(".open(): root tag not: " + m_rootTagName
163: + " found: " + root.getTagName());
164: return;
165: }
166:
167: // the children
168: NodeList rootNodes = root.getChildNodes();
169: final int rootNodesLength = rootNodes.getLength();
170: for (int i = 0; i < rootNodesLength; i++) {
171: Node rootNode = rootNodes.item(i);
172: if (rootNode.getNodeType() != Node.ELEMENT_NODE)
173: continue;
174: Element rootElement = (Element) rootNode;
175:
176: // look for an entry element (entries in the root)
177: if ((m_containerTagName == null)
178: && (rootElement.getTagName()
179: .equals(m_entryTagName))) {
180: // re-create the resource and store in the top container
181: Entity entry = m_user.newResource(top.container,
182: rootElement);
183:
184: top.contained.put(caseId(entry.getId()), entry);
185: }
186:
187: // look for a container element (containers in the root, entries in the containers)
188: else if ((m_containerTagName != null)
189: && (rootElement.getTagName()
190: .equals(m_containerTagName))) {
191: // re-create the container
192: Entity containerResource = m_user
193: .newContainer(rootElement);
194:
195: // add to the store
196: Container container = new Container(
197: containerResource);
198: m_store.put(containerResource.getReference(),
199: container);
200:
201: // scan for entry children of the container
202: NodeList containerNodes = rootElement
203: .getChildNodes();
204: final int containerNodesLength = containerNodes
205: .getLength();
206: for (int j = 0; j < containerNodesLength; j++) {
207: Node containerNode = containerNodes.item(j);
208: if (containerNode.getNodeType() != Node.ELEMENT_NODE)
209: continue;
210: Element containerElement = (Element) containerNode;
211:
212: // look for an entry element (entries in the root)
213: if (containerElement.getTagName().equals(
214: m_entryTagName)) {
215: // re-create the resource
216: Entity entry = m_user.newResource(
217: container.container,
218: containerElement);
219:
220: container.contained.put(caseId(entry
221: .getId()), entry);
222: }
223: }
224: }
225: }
226: } catch (Exception e) {
227: M_log.warn(".open(): ", e);
228: }
229: }
230:
231: /**
232: * Create and return the XML Document for our storaghe
233: */
234: protected Document createDocument() {
235: // create the Dom with a root element
236: Document doc = Xml.createDocument();
237: Stack stack = new Stack();
238: Element root = doc.createElement(m_rootTagName);
239: doc.appendChild(root);
240:
241: stack.push(root);
242:
243: // if we have no containers, store all elements from the Top container under the root
244: if (m_containerTagName == null) {
245: Enumeration e = ((Container) m_store.get("")).contained
246: .elements();
247: while (e.hasMoreElements()) {
248: Entity entry = (Entity) e.nextElement();
249: entry.toXml(doc, stack);
250: }
251: }
252:
253: // otherwise, process each container
254: else {
255: Enumeration e = m_store.elements();
256: while (e.hasMoreElements()) {
257: Container c = (Container) e.nextElement();
258:
259: // skip Top
260: if (c.container == null)
261: continue;
262:
263: // store the container
264: Element containerElement = c.container
265: .toXml(doc, stack);
266:
267: // push it onto the stack, so entries are created under it
268: stack.push(containerElement);
269:
270: // store each contained under the container's element
271: Enumeration elementEnum = c.contained.elements();
272: while (elementEnum.hasMoreElements()) {
273: Entity entry = (Entity) elementEnum.nextElement();
274: entry.toXml(doc, stack);
275: }
276:
277: stack.pop();
278: }
279: }
280:
281: stack.pop();
282: return doc;
283: }
284:
285: /**
286: * flush
287: */
288: protected void flush() {
289:
290: Document doc = createDocument();
291: Xml.writeDocument(doc, m_fileStoragePath);
292: }
293:
294: /**
295: * Close.
296: */
297: public void close() {
298:
299: flush();
300: m_locks.clear();
301: m_locks = null;
302: m_store.clear();
303: m_store = null;
304: }
305:
306: /**
307: * Check if a container by this id exists.
308: *
309: * @param ref
310: * The container reference.
311: * @return true if a resource by this id exists, false if not.
312: */
313: public boolean checkContainer(String ref) {
314: Container c = ((Container) m_store.get(ref));
315: return (c != null);
316: }
317:
318: /**
319: * Get the container with this id, or null if not found.
320: *
321: * @param ref
322: * The container reference.
323: * @return The container with this id, or null if not found.
324: */
325: public Entity getContainer(String ref) {
326: if (ref == null)
327: return null;
328: Container c = ((Container) m_store.get(ref));
329: if (c == null)
330: return null;
331: return c.container;
332: }
333:
334: /**
335: * Get a list of all containers.
336: *
337: * @return A list (Resource) of all containers, or empty if none defined.
338: */
339: public List getAllContainers() {
340: List rv = new Vector();
341:
342: if (m_containerTagName == null)
343: return rv;
344: if (m_store.size() == 0)
345: return rv;
346:
347: Enumeration e = m_store.elements();
348: while (e.hasMoreElements()) {
349: Container c = (Container) e.nextElement();
350: rv.add(c.container);
351: }
352:
353: return rv;
354: }
355:
356: /**
357: * Add a new container with this id.
358: *
359: * @param ref
360: * The channel reference.
361: * @return The locked object with this id, or null if the id is in use.
362: */
363: public Edit putContainer(String ref) {
364: // if it's already defined
365: Container c = ((Container) m_store.get(ref));
366: if (c != null)
367: return null;
368:
369: // make an Edit
370: Edit edit = m_user.newContainerEdit(ref);
371:
372: synchronized (m_locks) {
373: // if it's in the locks (i.e. it's been put() but not committed
374: if (m_locks.get(edit.getReference()) != null)
375: return null;
376:
377: // store it in the locks
378: m_locks.put(edit.getReference(), edit);
379: }
380:
381: return edit;
382: }
383:
384: /**
385: * Return a lock on the container with this id, or null if a lock cannot be made.
386: *
387: * @param ref
388: * The container reference.
389: * @return The locked object with this id, or null if a lock cannot be made.
390: */
391: public Edit editContainer(String ref) {
392: Container c = (Container) m_store.get(ref);
393: if (c == null)
394: return null;
395:
396: synchronized (m_locks) {
397: // check for a lock in place
398: if (m_locks.get(c.container.getReference()) != null)
399: return null;
400:
401: // make an Edit
402: Edit edit = m_user.newContainerEdit(c.container);
403:
404: // store it in the locks
405: m_locks.put(edit.getReference(), edit);
406:
407: return edit;
408: }
409: }
410:
411: /**
412: * Commit the changes and release the locked container.
413: *
414: * @param container
415: * The container id.
416: * @param edit
417: * The entry to commit.
418: */
419: public void commitContainer(Edit edit) {
420: // make a new Entry from the Edit to update the info store
421: Entity updatedContainer = m_user.newContainer(edit);
422:
423: // update the store
424: Container c = ((Container) m_store.get(updatedContainer
425: .getReference()));
426: if (c != null) {
427: c.container = updatedContainer;
428: } else {
429: c = new Container(updatedContainer);
430: m_store.put(updatedContainer.getReference(), c);
431: }
432:
433: // release the lock
434: m_locks.remove(edit.getReference());
435: }
436:
437: /**
438: * Cancel the changes and release the locked container.
439: *
440: * @param container
441: * The container id.
442: * @param edit
443: * The entry to cancel.
444: */
445: public void cancelContainer(Edit edit) {
446: // release the lock
447: m_locks.remove(edit.getReference());
448: }
449:
450: /**
451: * Remove this container and all it contains.
452: *
453: * @param container
454: * The container id.
455: * @param edit
456: * The entry to remove.
457: */
458: public void removeContainer(Edit edit) {
459: Container c = ((Container) m_store.get(edit.getReference()));
460: if (c != null) {
461: m_store.remove(c);
462: // %%% better cleanup?
463: }
464:
465: // release the lock
466: m_locks.remove(edit.getReference());
467: }
468:
469: /**
470: * Check if a resource by this id exists.
471: *
472: * @param container
473: * The container id.
474: * @param id
475: * The id.
476: * @return true if a resource by this id exists, false if not.
477: */
478: public boolean checkResource(String container, String id) {
479: if (container == null)
480: container = "";
481: Container c = ((Container) m_store.get(container));
482: if (c == null)
483: return false;
484:
485: return c.contained.get(caseId(id)) != null;
486: }
487:
488: /**
489: * Get the entry with this id, or null if not found.
490: *
491: * @param container
492: * The container id.
493: * @param id
494: * The id.
495: * @return The entry with this id, or null if not found.
496: */
497: public Entity getResource(String container, String id) {
498: if (container == null)
499: container = "";
500: Container c = ((Container) m_store.get(container));
501: if (c == null)
502: return null;
503:
504: return (Entity) c.contained.get(caseId(id));
505: }
506:
507: /**
508: * Get all entries.
509: *
510: * @param container
511: * The container id.
512: * @return The list (Resource) of all entries.
513: */
514: public List getAllResources(String container) {
515: List rv = new Vector();
516:
517: if (container == null)
518: container = "";
519: Container c = ((Container) m_store.get(container));
520: if (c == null)
521: return rv;
522: if (c.contained.size() == 0)
523: return rv;
524:
525: rv.addAll(c.contained.values());
526: return rv;
527: }
528:
529: /**
530: * Determine if empty
531: *
532: * @return true if empty, false if not.
533: */
534: public boolean isEmpty(String container) {
535: if (container == null)
536: container = "";
537: Container c = ((Container) m_store.get(container));
538: if (c == null)
539: return true;
540: if (c.contained.size() == 0)
541: return true;
542:
543: return false;
544: }
545:
546: /**
547: * Get all entries within a range sorted by id.
548: *
549: * @param container
550: * The container id.
551: * @param first
552: * The first position.
553: * @param last
554: * The last position.
555: * @return The list (Resource) of all entries within a range sorted by id.
556: */
557: public List getAllResources(String container, int first, int last) {
558: List rv = new Vector();
559:
560: if (container == null)
561: container = "";
562: Container c = ((Container) m_store.get(container));
563: if (c == null)
564: return rv;
565: if (c.contained.size() == 0)
566: return rv;
567:
568: rv.addAll(c.contained.values());
569:
570: Collections.sort(rv);
571:
572: // subset by position
573: if (first < 1)
574: first = 1;
575: if (last >= rv.size())
576: last = rv.size();
577:
578: rv = rv.subList(first - 1, last);
579:
580: return rv;
581: }
582:
583: /**
584: * Count all entries.
585: *
586: * @param container
587: * The container id.
588: * @return The count of all entries.
589: */
590: public int countAllResources(String container) {
591: List rv = new Vector();
592:
593: if (container == null)
594: container = "";
595: Container c = ((Container) m_store.get(container));
596: if (c == null)
597: return 0;
598: return c.contained.size();
599: }
600:
601: /**
602: * Add a new entry with this id.
603: *
604: * @param container
605: * The container id.
606: * @param id
607: * The id.
608: * @param others
609: * Other fields for the newResource call
610: * @return The locked object with this id, or null if the id is in use.
611: */
612: public Edit putResource(String container, String id, Object[] others) {
613: if (container == null)
614: container = "";
615: Container c = ((Container) m_store.get(container));
616: if (c == null)
617: return null;
618:
619: // if it's already defined
620: if (c.contained.get(caseId(id)) != null)
621: return null;
622:
623: // make an Edit
624: Edit edit = m_user.newResourceEdit(c.container, id, others);
625:
626: synchronized (m_locks) {
627: // if it's in the locks (i.e. it's been put() but not committed
628: if (m_locks.get(edit.getReference()) != null)
629: return null;
630:
631: // store it in the locks
632: m_locks.put(edit.getReference(), edit);
633: }
634:
635: return edit;
636: }
637:
638: /**
639: * Return a lock on the entry with this id, or null if a lock cannot be made.
640: *
641: * @param container
642: * The container id.
643: * @param id
644: * The id.
645: * @return The locked object with this id, or null if a lock cannot be made.
646: */
647: public Edit editResource(String container, String id) {
648: if (container == null)
649: container = "";
650: Container c = ((Container) m_store.get(container));
651: if (c == null)
652: return null;
653:
654: Entity entry = (Entity) c.contained.get(caseId(id));
655: if (entry == null)
656: return null;
657:
658: synchronized (m_locks) {
659: // check for a lock in place
660: if (m_locks.get(entry.getReference()) != null)
661: return null;
662:
663: // make an Edit
664: Edit edit = m_user.newResourceEdit(c.container, entry);
665:
666: // store it in the locks
667: m_locks.put(entry.getReference(), edit);
668:
669: return edit;
670: }
671: }
672:
673: /**
674: * Commit the changes and release the lock.
675: *
676: * @param container
677: * The container id.
678: * @param edit
679: * The entry to commit.
680: */
681: public void commitResource(String container, Edit edit) {
682: if (container == null)
683: container = "";
684: Container c = ((Container) m_store.get(container));
685: if (c != null) {
686: // make a new Entry from the Edit to update the info store
687: Entity updatedEntry = m_user.newResource(c.container, edit);
688:
689: c.contained.put(caseId(updatedEntry.getId()), updatedEntry);
690: }
691:
692: // release the lock
693: m_locks.remove(edit.getReference());
694: }
695:
696: /**
697: * Cancel the changes and release the lock.
698: *
699: * @param container
700: * The container id.
701: * @param edit
702: * The entry to cancel.
703: */
704: public void cancelResource(String container, Edit edit) {
705: // release the lock
706: m_locks.remove(edit.getReference());
707: }
708:
709: /**
710: * Remove this entry.
711: *
712: * @param container
713: * The container id.
714: * @param edit
715: * The entry to remove.
716: */
717: public void removeResource(String container, Edit edit) {
718: if (container == null)
719: container = "";
720: Container c = ((Container) m_store.get(container));
721: if (c != null) {
722: // remove from the info store
723: c.contained.remove(caseId(edit.getId()));
724: }
725:
726: // release the lock
727: m_locks.remove(edit.getReference());
728: }
729:
730: /**
731: * Fix the case of resource ids to support case insensitive ids if enabled
732: *
733: * @param The
734: * id to fix.
735: * @return The id, case modified as needed.
736: */
737: protected String caseId(String id) {
738: if (m_caseInsensitive) {
739: return id.toLowerCase();
740: }
741:
742: return id;
743: }
744:
745: /**
746: * Enable / disable case insensitive ids.
747: *
748: * @param setting
749: * true to set case insensitivity, false to set case sensitivity.
750: */
751: protected void setCaseInsensitivity(boolean setting) {
752: m_caseInsensitive = setting;
753: }
754:
755: /**
756: * Get resources filtered by date and count and drafts, in descending (latest first) order
757: *
758: * @param container
759: * The container id.
760: * @param afterDate
761: * if null, no date limit, else limited to only messages after this date.
762: * @param limitedToLatest
763: * if 0, no count limit, else limited to only the latest this number of messages.
764: * @param draftsForId
765: * how to handle drafts: null means no drafts, "*" means all, otherwise drafts only if created by this userId.
766: * @param pubViewOnly
767: * if true, include only messages marked pubview, else include any.
768: * @return A list of Message objects that meet the criteria; may be empty
769: */
770: public List getResources(String container, Time afterDate,
771: int limitedToLatest, String draftsForId, boolean pubViewOnly) {
772: if (container == null)
773: container = "";
774: Container c = ((Container) m_store.get(container));
775: if (c == null)
776: return new Vector();
777: if (c.contained.size() == 0)
778: return new Vector();
779:
780: List all = new Vector();
781: all.addAll(c.contained.values());
782:
783: // sort latest date first
784: Collections.sort(all, new Comparator() {
785: public int compare(Object o1, Object o2) {
786: // if the same object
787: if (o1 == o2)
788: return 0;
789:
790: // assume they are Resource
791: Entity r1 = (Entity) o1;
792: Entity r2 = (Entity) o2;
793:
794: // get each one's date
795: Time t1 = m_user.getDate(r1);
796: Time t2 = m_user.getDate(r2);
797:
798: // compare based on date
799: int compare = t2.compareTo(t1);
800:
801: return compare;
802: }
803: });
804:
805: // early out - if no filtering needed
806: if ((limitedToLatest == 0) && (afterDate == null)
807: && ("*".equals(draftsForId)) && !pubViewOnly) {
808: return all;
809: }
810:
811: Vector selected = new Vector();
812:
813: // deal with drafts / date / pubview
814: for (Iterator i = all.iterator(); i.hasNext();) {
815: Entity r = (Entity) i.next();
816: Entity candidate = null;
817: if (m_user.isDraft(r)) {
818: // if some drafts
819: if ((draftsForId != null)
820: && (m_user.getOwnerId(r).equals(draftsForId))) {
821: candidate = r;
822: }
823: } else {
824: candidate = r;
825: }
826:
827: // deal with date if it passes the draft criteria
828: if ((candidate != null) && (afterDate != null)) {
829: if (m_user.getDate(candidate).before(afterDate)) {
830: candidate = null;
831: }
832: }
833:
834: // if we want pub view only
835: if ((candidate != null) && pubViewOnly) {
836: if (candidate.getProperties().getProperty(
837: ResourceProperties.PROP_PUBVIEW) == null) {
838: candidate = null;
839: }
840: }
841:
842: // add it if it passes all criteria
843: if (candidate != null) {
844: selected.add(candidate);
845: }
846: }
847:
848: // pick what we need
849: List rv = new Vector();
850:
851: if ((limitedToLatest > 0)
852: && (limitedToLatest < selected.size())) {
853: all = selected.subList(0, limitedToLatest);
854: } else {
855: all = selected;
856: }
857:
858: return all;
859: }
860:
861: /**
862: * Access a list of container ids that match (start with) the root.
863: *
864: * @param context
865: * The reference root to match.
866: * @return A List (String) of container id which match the root.
867: */
868: public List getContainerIdsMatching(String context) {
869: List containers = getAllContainers();
870: List rv = new Vector();
871:
872: // the id of each container will be the part that follows the root reference
873: final int pos = context.length();
874:
875: // filter
876: for (Iterator i = containers.iterator(); i.hasNext();) {
877: Entity r = (Entity) i.next();
878: String ref = r.getReference();
879: if (ref.startsWith(context)) {
880: // check the reference, return the id (what follows the root)
881: String id = ref.substring(pos);
882: rv.add(id);
883: }
884: }
885:
886: return rv;
887: }
888: }
|