1: <?php
2: /**
3: * @package DataTree
4: */
5:
6: /** List every object in an array, similar to PEAR/html/menu.php. */
7: define('DATATREE_FORMAT_TREE', 1);
8:
9: /** Get a full list - an array of keys. */
10: define('DATATREE_FORMAT_FLAT', 2);
11:
12: /** The root element (top-level parent) of each DataTree group. */
13: define('DATATREE_ROOT', -1);
14:
15: /** Build a normal select query. */
16: define('DATATREE_BUILD_SELECT', 0);
17:
18: /** Build a count only query. */
19: define('DATATREE_BUILD_COUNT', 1);
20:
21: /** Build an attribute only query. */
22: define('DATATREE_BUILD_VALUES', 2);
23:
24: define('DATATREE_BUILD_VALUES_COUNT', 3);
25:
26: /**
27: * The Horde_DataTree class provides a common abstracted interface into the
28: * various backends for the Horde DataTree system.
29: *
30: * A piece of data is just a title that is saved in the page for the null
31: * driver or can be saved in a database to be accessed from everywhere. Every
32: * stored object must have a different name (inside each groupid).
33: *
34: * Required values for $params:<pre>
35: * 'group' -- Define each group of objects we want to build.</pre>
36: *
37: * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
38: *
39: * See the enclosed file COPYING for license information (LGPL). If you
40: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
41: *
42: * @author Stephane Huther <shuther1@free.fr>
43: * @author Chuck Hagenbuch <chuck@horde.org>
44: * @package DataTree
45: */
46: class Horde_DataTree {
47:
48: /**
49: * Array of all data: indexed by id. The format is:
50: * array(id => 'name' => name, 'parent' => parent).
51: *
52: * @var array
53: */
54: var $_data = array();
55:
56: /**
57: * A hash that can be used to map a full object name
58: * (parent:child:object) to that object's unique ID.
59: *
60: * @var array
61: */
62: var $_nameMap = array();
63:
64: /**
65: * Actual attribute sorting hash.
66: *
67: * @var array
68: */
69: var $_sortHash = null;
70:
71: /**
72: * Hash containing connection parameters.
73: *
74: * @var array
75: */
76: var $_params = array();
77:
78: /**
79: * Constructor.
80: *
81: * @param array $params A hash containing any additional configuration or
82: * connection parameters a subclass might need.
83: * We always need 'group', a string that defines the
84: * prefix for each set of hierarchical data.
85: */
86: function __construct($params = array())
87: {
88: $this->_params = $params;
89: }
90:
91: /**
92: * Returns a parameter of this Horde_DataTree instance.
93: *
94: * @param string $param The parameter to return.
95: *
96: * @return mixed The parameter's value or null if it doesn't exist.
97: */
98: function getParam($param)
99: {
100: return isset($this->_params[$param]) ? $this->_params[$param] : null;
101: }
102:
103: /**
104: * Removes an object.
105: *
106: * @param string $object The object to remove.
107: * @param boolean $force Force removal of every child object?
108: *
109: * @return TODO
110: */
111: function remove($object, $force = false)
112: {
113: if (is_a($object, 'Horde_DataTreeObject')) {
114: $object = $object->getName();
115: }
116:
117: if (!$this->exists($object)) {
118: return PEAR::raiseError($object . ' does not exist');
119: }
120:
121: $children = $this->getNumberOfChildren($object);
122: if ($children) {
123: /* TODO: remove children if $force == true */
124: return PEAR::raiseError(sprintf('Cannot remove, %d children exist.', count($children)));
125: }
126:
127: $id = $this->getId($object);
128: $pid = $this->getParent($object);
129: $order = $this->_data[$id]['order'];
130: unset($this->_data[$id], $this->_nameMap[$id]);
131:
132: // Shift down the order positions.
133: $this->_reorder($pid, $order);
134:
135: return $id;
136: }
137:
138: /**
139: * Removes all Horde_DataTree objects owned by a certain user.
140: *
141: * @abstract
142: *
143: * @param string $user A user name.
144: *
145: * @return TODO
146: */
147: function removeUserData($user)
148: {
149: return PEAR::raiseError('not supported');
150: }
151:
152: /**
153: * Move an object to a new parent.
154: *
155: * @param mixed $object The object to move.
156: * @param string $newparent The new parent object. Defaults to the root.
157: *
158: * @return mixed True on success, PEAR_Error on error.
159: */
160: function move($object, $newparent = null)
161: {
162: $cid = $this->getId($object);
163: if (is_a($cid, 'PEAR_Error')) {
164: return PEAR::raiseError(sprintf('Object to move does not exist: %s', $cid->getMessage()));
165: }
166:
167: if (!is_null($newparent)) {
168: $pid = $this->getId($newparent);
169: if (is_a($pid, 'PEAR_Error')) {
170: return PEAR::raiseError(sprintf('New parent does not exist: %s', $pid->getMessage()));
171: }
172: } else {
173: $pid = DATATREE_ROOT;
174: }
175:
176: $this->_data[$cid]['parent'] = $pid;
177:
178: return true;
179: }
180:
181: /**
182: * Change an object's name.
183: *
184: * @param mixed $old_object The old object.
185: * @param string $new_object_name The new object name.
186: *
187: * @return mixed True on success, PEAR_Error on error.
188: */
189: function rename($old_object, $new_object_name)
190: {
191: /* Check whether the object exists at all */
192: if (!$this->exists($old_object)) {
193: return PEAR::raiseError($old_object . ' does not exist');
194: }
195:
196: /* Check for duplicates - get parent and create new object
197: * name */
198: $parent = $this->getName($this->getParent($old_object));
199: if ($this->exists($parent . ':' . $new_object_name)) {
200: return PEAR::raiseError('Duplicate name ' . $new_object_name);
201: }
202:
203: /* Replace the old name with the new one in the cache */
204: $old_object_id = $this->getId($old_object);
205: $this->_data[$old_object_id]['name'] = $new_object_name;
206:
207: return true;
208: }
209:
210: /**
211: * Changes the order of the children of an object.
212: *
213: * @abstract
214: *
215: * @param string $parent The full id path of the parent object.
216: * @param mixed $order If an array it specifies the new positions for
217: * all child objects.
218: * If an integer and $cid is specified, the position
219: * where the child specified by $cid is inserted. If
220: * $cid is not specified, the position gets deleted,
221: * causing the following positions to shift up.
222: * @param integer $cid See $order.
223: *
224: * @return TODO
225: */
226: function reorder($parents, $order = null, $cid = null)
227: {
228: return PEAR::raiseError('not supported');
229: }
230:
231: /**
232: * Change order of children of an object.
233: *
234: * @param string $pid The parent object id string path.
235: * @param mixed $order Specific new order position or an array containing
236: * the new positions for the given parent.
237: * @param integer $cid If provided indicates insertion of a new child to
238: * the parent to avoid incrementing it when
239: * shifting up all other children's order. If not
240: * provided indicates deletion, so shift all other
241: * positions down one.
242: */
243: function _reorder($pid, $order = null, $cid = null)
244: {
245: if (!is_array($order) && !is_null($order)) {
246: // Single update (add/del).
247: if (is_null($cid)) {
248: // No id given so shuffle down.
249: foreach ($this->_data as $c_key => $c_val) {
250: if ($this->_data[$c_key]['parent'] == $pid &&
251: $this->_data[$c_key]['order'] > $order) {
252: --$this->_data[$c_key]['order'];
253: }
254: }
255: } else {
256: // We have an id so shuffle up.
257: foreach ($this->_data as $c_key => $c_val) {
258: if ($c_key != $cid &&
259: $this->_data[$c_key]['parent'] == $pid &&
260: $this->_data[$c_key]['order'] >= $order) {
261: ++$this->_data[$c_key]['order'];
262: }
263: }
264: }
265: } elseif (is_array($order) && count($order)) {
266: // Multi update.
267: foreach ($order as $order_position => $cid) {
268: $this->_data[$cid]['order'] = $order_position;
269: }
270: }
271: }
272:
273: /**
274: * Explicitly set the order for a Horde_DataTree object.
275: *
276: * @abstract
277: *
278: * @param integer $id The Horde_DataTree object id to change.
279: * @param integer $order The new order.
280: *
281: * @return TODO
282: */
283: function setOrder($id, $order)
284: {
285: return PEAR::raiseError('not supported');
286: }
287:
288: /**
289: * Dynamically determines the object class.
290: *
291: * @param array $attributes The set of attributes that contain the class
292: * information. Defaults to Horde_DataTreeObject.
293: *
294: * @return TODO
295: */
296: function _defineObjectClass($attributes)
297: {
298: $class = 'Horde_DataTreeObject';
299: if (!is_array($attributes)) {
300: return $class;
301: }
302:
303: foreach ($attributes as $attr) {
304: if ($attr['name'] == 'DataTree') {
305: switch ($attr['key']) {
306: case 'objectClass':
307: $class = $attr['value'];
308: break;
309:
310: case 'objectType':
311: $result = explode('/', $attr['value']);
312: $class = $GLOBALS['registry']->callByPackage($result[0], 'defineClass', array('type' => $result[1]));
313: break;
314: }
315: }
316: }
317:
318: return $class;
319: }
320:
321: /**
322: * Returns a Horde_DataTreeObject (or subclass) object of the data in the
323: * object defined by $object.
324: *
325: * @param string $object The object to fetch: 'parent:sub-parent:name'.
326: * @param string $class Subclass of Horde_DataTreeObject to use. Defaults to
327: * Horde_DataTreeObject. Null forces the driver to look
328: * into the attributes table to determine the
329: * subclass to use. If none is found it uses
330: * Horde_DataTreeObject.
331: *
332: * @return TODO
333: */
334: function &getObject($object, $class = 'Horde_DataTreeObject')
335: {
336: if (empty($object)) {
337: $error = PEAR::raiseError('No object requested.');
338: return $error;
339: }
340:
341: $this->_load($object);
342: if (!$this->exists($object)) {
343: $error = PEAR::raiseError($object . ' not found.');
344: return $error;
345: }
346:
347: return $this->_getObject($this->getId($object), $object, $class);
348: }
349:
350: /**
351: * Returns a Horde_DataTreeObject (or subclass) object of the data in the
352: * object with the ID $id.
353: *
354: * @param integer $id An object id.
355: * @param string $class Subclass of Horde_DataTreeObject to use. Defaults to
356: * Horde_DataTreeObject. Null forces the driver to look
357: * into the attributes table to determine the
358: * subclass to use. If none is found it uses
359: * Horde_DataTreeObject.
360: *
361: * @return TODO
362: */
363: function &getObjectById($id, $class = 'Horde_DataTreeObject')
364: {
365: if (empty($id)) {
366: $object = PEAR::raiseError('No id requested.');
367: return $object;
368: }
369:
370: $result = $this->_loadById($id);
371: if (is_a($result, 'PEAR_Error')) {
372: return $result;
373: }
374:
375: return $this->_getObject($id, $this->getName($id), $class);
376: }
377:
378: /**
379: * Helper function for getObject() and getObjectById().
380: *
381: * @access private
382: */
383: function &_getObject($id, $name, $class)
384: {
385: $use_attributes = is_null($class) || is_callable(array($class, '_fromAttributes'));
386: if ($use_attributes) {
387: $attributes = $this->getAttributes($id);
388: if (is_a($attributes, 'PEAR_Error')) {
389: return $attributes;
390: }
391:
392: if (is_null($class)) {
393: $class = $this->_defineObjectClass($attributes);
394: }
395: }
396:
397: if (!class_exists($class)) {
398: $error = PEAR::raiseError($class . ' not found.');
399: return $error;
400: }
401:
402: $dataOb = new $class($name);
403: $dataOb->setDataTree($this);
404:
405: /* If the class has a _fromAttributes method, load data from
406: * the attributes backend. */
407: if ($use_attributes) {
408: $dataOb->_fromAttributes($attributes);
409: } else {
410: /* Otherwise load it from the old data storage field. */
411: $dataOb->setData($this->getData($id));
412: }
413:
414: $dataOb->setOrder($this->getOrder($name));
415: return $dataOb;
416: }
417:
418: /**
419: * Returns an array of Horde_DataTreeObject (or subclass) objects
420: * corresponding to the objects in $ids, with the object
421: * names as the keys of the array.
422: *
423: * @param array $ids An array of object ids.
424: * @param string $class Subclass of Horde_DataTreeObject to use. Defaults to
425: * Horde_DataTreeObject. Null forces the driver to look
426: * into the attributes table to determine the
427: * subclass to use. If none is found it uses
428: * Horde_DataTreeObject.
429: *
430: * @return TODO
431: */
432: function &getObjects($ids, $class = 'Horde_DataTreeObject')
433: {
434: $result = $this->_loadById($ids);
435: if (is_a($result, 'PEAR_Error')) {
436: return $result;
437: }
438:
439: $defineClass = is_null($class);
440: $attributes = $defineClass || is_callable(array($class, '_fromAttributes'));
441:
442: if ($attributes) {
443: $data = $this->getAttributes($ids);
444: } else {
445: $data = $this->getData($ids);
446: }
447:
448: $objects = array();
449: foreach ($ids as $id) {
450: $name = $this->getName($id);
451: if (!empty($name) && !empty($data[$id])) {
452: if ($defineClass) {
453: $class = $this->_defineObjectClass($data[$id]);
454: }
455:
456: if (!class_exists($class)) {
457: return PEAR::raiseError($class . ' not found.');
458: }
459:
460: $objects[$name] = new $class($name);
461: $objects[$name]->setDataTree($this);
462: if ($attributes) {
463: $objects[$name]->_fromAttributes($data[$id]);
464: } else {
465: $objects[$name]->setData($data[$id]);
466: }
467: $objects[$name]->setOrder($this->getOrder($name));
468: }
469: }
470:
471: return $objects;
472: }
473:
474: /**
475: * Export a list of objects.
476: *
477: * @param constant $format Format of the export
478: * @param string $startleaf The name of the leaf from which we start
479: * the export tree.
480: * @param boolean $reload Re-load the requested chunk? Defaults to
481: * false (only what is currently loaded).
482: * @param string $rootname The label to use for the root element.
483: * Defaults to DATATREE_ROOT.
484: * @param integer $maxdepth The maximum number of levels to return.
485: * Defaults to DATATREE_ROOT, which is no
486: * limit.
487: * @param boolean $loadTree Load a tree starting at $root, or just the
488: * requested level and direct parents?
489: * Defaults to single level.
490: * @param string $sortby_name Attribute name to use for sorting.
491: * @param string $sortby_key Attribute key to use for sorting.
492: * @param integer $direction Sort direction:
493: * 0 - ascending
494: * 1 - descending
495: *
496: * @return mixed The tree representation of the objects, or a PEAR_Error
497: * on failure.
498: */
499: function get($format, $startleaf = DATATREE_ROOT, $reload = false,
500: $rootname = DATATREE_ROOT, $maxdepth = -1, $loadTree = false,
501: $sortby_name = null, $sortby_key = null, $direction = 0)
502: {
503: $out = array();
504:
505: /* Set sorting hash */
506: if (!is_null($sortby_name)) {
507: $this->_sortHash = Horde_DataTree::sortHash($startleaf, $sortby_name, $sortby_key, $direction);
508: }
509:
510: $this->_load($startleaf, $loadTree, $reload, $sortby_name, $sortby_key, $direction);
511:
512: switch ($format) {
513: case DATATREE_FORMAT_TREE:
514: $startid = $this->getId($startleaf, $maxdepth);
515: if (is_a($startid, 'PEAR_Error')) {
516: return $startid;
517: }
518: $this->_extractAllLevelTree($out, $startid, $maxdepth);
519: break;
520:
521: case DATATREE_FORMAT_FLAT:
522: $startid = $this->getId($startleaf);
523: if (is_a($startid, 'PEAR_Error')) {
524: return $startid;
525: }
526: $this->_extractAllLevelList($out, $startid, $maxdepth);
527: if (!empty($out[DATATREE_ROOT])) {
528: $out[DATATREE_ROOT] = $rootname;
529: }
530: break;
531:
532: default:
533: return PEAR::raiseError('Not supported');
534: }
535:
536: if (!is_null($this->_sortHash)) {
537: /* Reset sorting hash. */
538: $this->_sortHash = null;
539:
540: /* Reverse since the attribute sorting combined with tree up-ward
541: * sorting produces a reversed object order. */
542: $out = array_reverse($out, true);
543: }
544:
545: return $out;
546: }
547:
548: /**
549: * Counts objects.
550: *
551: * @param string $startleaf The name of the leaf from which we start
552: * counting.
553: *
554: * @return integer The number of the objects below $startleaf.
555: */
556: function count($startleaf = DATATREE_ROOT)
557: {
558: return $this->_count($startleaf);
559: }
560:
561: /**
562: * Create attribute sort hash
563: *
564: * @param string $root The name of the leaf from which we start
565: * the export tree.
566: * @param string $sortby_name Attribute name to use for sorting.
567: * @param string $sortby_key Attribute key to use for sorting.
568: * @param integer $direction Sort direction:
569: * 0 - ascending
570: * 1 - descending
571: *
572: * @return string The sort hash.
573: */
574: function sortHash($root, $sortby_name = null, $sortby_key = null,
575: $direction = 0)
576: {
577: return sprintf('%s-%s-%s-%s', $root, $sortby_name, $sortby_key, $direction);
578: }
579:
580: /**
581: * Export a list of objects just like get() above, but uses an
582: * object id to fetch the list of objects.
583: *
584: * @param constant $format Format of the export.
585: * @param string $startleaf The id of the leaf from which we start the
586: * export tree.
587: * @param boolean $reload Reload the requested chunk? Defaults to
588: * false (only what is currently loaded).
589: * @param string $rootname The label to use for the root element.
590: * Defaults to DATATREE_ROOT.
591: * @param integer $maxdepth The maximum number of levels to return
592: * Defaults to -1, which is no limit.
593: *
594: * @return mixed The tree representation of the objects, or a PEAR_Error
595: * on failure.
596: */
597: function getById($format, $startleaf = DATATREE_ROOT, $reload = false,
598: $rootname = DATATREE_ROOT, $maxdepth = -1)
599: {
600: $this->_loadById($startleaf);
601: $out = array();
602:
603: switch ($format) {
604: case DATATREE_FORMAT_TREE:
605: $this->_extractAllLevelTree($out, $startleaf, $maxdepth);
606: break;
607:
608: case DATATREE_FORMAT_FLAT:
609: $this->_extractAllLevelList($out, $startleaf, $maxdepth);
610: if (!empty($out[DATATREE_ROOT])) {
611: $out[DATATREE_ROOT] = $rootname;
612: }
613: break;
614:
615: default:
616: return PEAR::raiseError('Not supported');
617: }
618:
619: return $out;
620: }
621:
622: /**
623: * Returns a list of all groups (root nodes) of the data tree.
624: *
625: * @abstract
626: *
627: * @return mixed The group IDs or PEAR_Error on error.
628: */
629: function getGroups()
630: {
631: return PEAR::raiseError('not supported');
632: }
633:
634: /**
635: * Retrieve data for an object from the datatree_data field.
636: *
637: * @abstract
638: *
639: * @param integer $cid The object id to fetch, or an array of object ids.
640: *
641: * @return TODO
642: */
643: function getData($cid)
644: {
645: return PEAR::raiseError('not supported');
646: }
647:
648: /**
649: * Import a list of objects. Used by drivers to populate the internal
650: * $_data array.
651: *
652: * @param array $data The data to import.
653: * @param string $charset The charset to convert the object name from.
654: *
655: * @return TODO
656: */
657: function set($data, $charset = null)
658: {
659: $cids = array();
660: foreach ($data as $id => $cat) {
661: if (!is_null($charset)) {
662: $cat[1] = Horde_String::convertCharset($cat[1], $charset, 'UTF-8');
663: }
664: $cids[$cat[0]] = $cat[1];
665: $cparents[$cat[0]] = $cat[2];
666: $corders[$cat[0]] = $cat[3];
667: $sorders[$cat[0]] = $id;
668: }
669:
670: foreach ($cids as $id => $name) {
671: $this->_data[$id]['name'] = $name;
672: $this->_data[$id]['order'] = $corders[$id];
673: if (!is_null($this->_sortHash)) {
674: $this->_data[$id]['sorter'][$this->_sortHash] = $sorders[$id];
675: }
676: if (!empty($cparents[$id])) {
677: $parents = explode(':', substr($cparents[$id], 1));
678: $par = $parents[count($parents) - 1];
679: $this->_data[$id]['parent'] = $par;
680:
681: if (!empty($this->_nameMap[$par])) {
682: // If we've already loaded the direct parent of
683: // this object, use that to find the full name.
684: $this->_nameMap[$id] = $this->_nameMap[$par] . ':' . $name;
685: } else {
686: // Otherwise, run through parents one by one to
687: // build it up.
688: $this->_nameMap[$id] = '';
689: foreach ($parents as $parID) {
690: if (!empty($cids[$parID])) {
691: $this->_nameMap[$id] .= ':' . $cids[$parID];
692: }
693: }
694: $this->_nameMap[$id] = substr($this->_nameMap[$id], 1) . ':' . $name;
695: }
696: } else {
697: $this->_data[$id]['parent'] = DATATREE_ROOT;
698: $this->_nameMap[$id] = $name;
699: }
700: }
701:
702: return true;
703: }
704:
705: /**
706: * Extract one level of data for a parent leaf, sorted first by
707: * their order and then by name. This function is a way to get a
708: * collection of $leaf's children.
709: *
710: * @param string $leaf Name of the parent from which to start.
711: *
712: * @return array TODO
713: */
714: function _extractOneLevel($leaf = DATATREE_ROOT)
715: {
716: $out = array();
717: foreach ($this->_data as $id => $vals) {
718: if ($vals['parent'] == $leaf) {
719: $out[$id] = $vals;
720: }
721: }
722:
723: uasort($out, array($this, (is_null($this->_sortHash)) ? '_cmp' : '_cmpSorted'));
724:
725: return $out;
726: }
727:
728: /**
729: * Extract all levels of data, starting from a given parent
730: * leaf in the datatree.
731: *
732: * @access private
733: *
734: * @note If nothing is returned that means there is no child, but
735: * don't forget to add the parent if any subsequent operations are
736: * required!
737: *
738: * @param array $out This is an iterating function, so $out is
739: * passed by reference to contain the result.
740: * @param string $parent The name of the parent from which to begin.
741: * @param integer $maxdepth Max of levels of depth to check.
742: *
743: * @return TODO
744: */
745: function _extractAllLevelTree(&$out, $parent = DATATREE_ROOT,
746: $maxdepth = -1)
747: {
748: if ($maxdepth == 0) {
749: return false;
750: }
751:
752: $out[$parent] = true;
753:
754: $k = $this->_extractOneLevel($parent);
755: foreach (array_keys($k) as $object) {
756: if (!is_array($out[$parent])) {
757: $out[$parent] = array();
758: }
759: $out[$parent][$object] = true;
760: $this->_extractAllLevelTree($out[$parent], $object, $maxdepth - 1);
761: }
762: }
763:
764: /**
765: * Extract all levels of data, starting from any parent in
766: * the tree.
767: *
768: * Returned array format: array(parent => array(child => true))
769: *
770: * @access private
771: *
772: * @param array $out This is an iterating function, so $out is
773: * passed by reference to contain the result.
774: * @param string $parent The name of the parent from which to begin.
775: * @param integer $maxdepth Max number of levels of depth to check.
776: *
777: * @return TODO
778: */
779: function _extractAllLevelList(&$out, $parent = DATATREE_ROOT,
780: $maxdepth = -1)
781: {
782: if ($maxdepth == 0) {
783: return false;
784: }
785:
786: // This is redundant most of the time, so make sure we need to
787: // do it.
788: if (empty($out[$parent])) {
789: $out[$parent] = $this->getName($parent);
790: }
791:
792: foreach (array_keys($this->_extractOneLevel($parent)) as $object) {
793: $out[$object] = $this->getName($object);
794: $this->_extractAllLevelList($out, $object, $maxdepth - 1);
795: }
796: }
797:
798: /**
799: * Returns a child's direct parent ID.
800: *
801: * @param mixed $child Either the object, an array containing the
802: * path elements, or the object name for which
803: * to look up the parent's ID.
804: *
805: * @return mixed The unique ID of the parent or PEAR_Error on error.
806: */
807: function getParent($child)
808: {
809: if (is_a($child, 'Horde_DataTreeObject')) {
810: $child = $child->getName();
811: }
812: $id = $this->getId($child);
813: if (is_a($id, 'PEAR_Error')) {
814: return $id;
815: }
816: return $this->getParentById($id);
817: }
818:
819: /**
820: * Get a $child's direct parent ID.
821: *
822: * @param integer $childId Get the parent of this object.
823: *
824: * @return mixed The unique ID of the parent or PEAR_Error on error.
825: */
826: function getParentById($childId)
827: {
828: $this->_loadById($childId);
829: return isset($this->_data[$childId]) ?
830: $this->_data[$childId]['parent'] :
831: PEAR::raiseError($childId . ' not found');
832: }
833:
834: /**
835: * Get a list of parents all the way up to the root object for
836: * $child.
837: *
838: * @param mixed $child The name of the child
839: * @param boolean $getids If true, return parent IDs; otherwise, return
840: * names.
841: *
842: * @return mixed [child] [parent] in a tree format or PEAR_Error.
843: */
844: function getParents($child, $getids = false)
845: {
846: $pid = $this->getParent($child);
847: if (is_a($pid, 'PEAR_Error')) {
848: return PEAR::raiseError('Parents not found: ' . $pid->getMessage());
849: }
850: $pname = $this->getName($pid);
851: $parents = ($getids) ? array($pid => true) : array($pname => true);
852:
853: if ($pid != DATATREE_ROOT) {
854: if ($getids) {
855: $parents[$pid] = $this->getParents($pname, $getids);
856: } else {
857: $parents[$pname] = $this->getParents($pname, $getids);
858: }
859: }
860:
861: return $parents;
862: }
863:
864: /**
865: * Get a list of parents all the way up to the root object for
866: * $child.
867: *
868: * @param integer $childId The id of the child.
869: * @param array $parents The array, as we build it up.
870: *
871: * @return array A flat list of all of the parents of $child,
872: * hashed in $id => $name format.
873: */
874: function getParentList($childId, $parents = array())
875: {
876: $pid = $this->getParentById($childId);
877: if (is_a($pid, 'PEAR_Error')) {
878: return PEAR::raiseError('Parents not found: ' . $pid->getMessage());
879: }
880:
881: if ($pid != DATATREE_ROOT) {
882: $parents[$pid] = $this->getName($pid);
883: $parents = $this->getParentList($pid, $parents);
884: }
885:
886: return $parents;
887: }
888:
889: /**
890: * Get a parent ID string (id:cid format) for the specified object.
891: *
892: * @param mixed $object The object to return a parent string for.
893: *
894: * @return string|PEAR_Error The ID "path" to the parent object or
895: * PEAR_Error on failure.
896: */
897: function getParentIdString($object)
898: {
899: $ptree = $this->getParents($object, true);
900: if (is_a($ptree, 'PEAR_Error')) {
901: return $ptree;
902: }
903:
904: $pids = '';
905: while ((list($id, $parent) = each($ptree)) && is_array($parent)) {
906: $pids = ':' . $id . $pids;
907: $ptree = $parent;
908: }
909:
910: return $pids;
911: }
912:
913: /**
914: * Get the number of children an object has, only counting immediate
915: * children, not grandchildren, etc.
916: *
917: * @param mixed $parent Either the object or the name for which to count
918: * the children, defaults to the root
919: * (DATATREE_ROOT).
920: *
921: * @return integer
922: */
923: function getNumberOfChildren($parent = DATATREE_ROOT)
924: {
925: if (is_a($parent, 'Horde_DataTreeObject')) {
926: $parent = $parent->getName();
927: }
928: $this->_load($parent);
929: $out = $this->_extractOneLevel($this->getId($parent));
930:
931: return is_array($out) ? count($out) : 0;
932: }
933:
934: /**
935: * Check if an object exists or not. The root element DATATREE_ROOT always
936: * exists.
937: *
938: * @param mixed $object The name of the object.
939: *
940: * @return boolean True if the object exists, false otherwise.
941: */
942: function exists($object)
943: {
944: if (empty($object)) {
945: return false;
946: }
947:
948: if (is_a($object, 'Horde_DataTreeObject')) {
949: $object = $object->getName();
950: } elseif (is_array($object)) {
951: $object = implode(':', $object);
952: }
953:
954: if ($object == DATATREE_ROOT) {
955: return true;
956: }
957:
958: if (array_search($object, $this->_nameMap) !== false) {
959: return true;
960: }
961:
962: // Consult the backend directly.
963: return $this->_exists($object);
964: }
965:
966: /**
967: * Get the name of an object from its id.
968: *
969: * @param integer $id The id for which to look up the name.
970: *
971: * @return string TODO
972: */
973: function getName($id)
974: {
975: /* If no id or if id is a PEAR error, return null. */
976: if (empty($id) || is_a($id, 'PEAR_Error')) {
977: return null;
978: }
979:
980: /* If checking name of root, return DATATREE_ROOT. */
981: if ($id == DATATREE_ROOT) {
982: return DATATREE_ROOT;
983: }
984:
985: /* If found in the name map, return the name. */
986: if (isset($this->_nameMap[$id])) {
987: return $this->_nameMap[$id];
988: }
989:
990: /* Not found in name map, consult the backend. */
991: return $this->_getName($id);
992: }
993:
994: /**
995: * Get the id of an object from its name.
996: *
997: * @param mixed $name Either the object, an array containing the
998: * path elements, or the object name for which
999: * to look up the id.
1000: *
1001: * @return string
1002: */
1003: function getId($name)
1004: {
1005: /* Check if $name is not a string. */
1006: if (is_a($name, 'Horde_DataTreeObject')) {
1007: /* Horde_DataTreeObject, get the string name. */
1008: $name = $name->getName();
1009: } elseif (is_array($name)) {
1010: /* Path array, implode to get the string name. */
1011: $name = implode(':', $name);
1012: }
1013:
1014: /* If checking id of root, return DATATREE_ROOT. */
1015: if ($name == DATATREE_ROOT) {
1016: return DATATREE_ROOT;
1017: }
1018:
1019: /* Flip the name map to look up the id using the name as key. */
1020: if (($id = array_search($name, $this->_nameMap)) !== false) {
1021: return $id;
1022: }
1023:
1024: /* Not found in name map, consult the backend. */
1025: $id = $this->_getId($name);
1026: if (is_null($id)) {
1027: return PEAR::raiseError($name . ' does not exist');
1028: }
1029: return $id;
1030: }
1031:
1032: /**
1033: * Get the order position of an object.
1034: *
1035: * @param mixed $child Either the object or the name.
1036: *
1037: * @return mixed The object's order position or a PEAR error on failure.
1038: */
1039: function getOrder($child)
1040: {
1041: if (is_a($child, 'Horde_DataTreeObject')) {
1042: $child = $child->getName();
1043: }
1044: $id = $this->getId($child);
1045: if (is_a($id, 'PEAR_Error')) {
1046: return $id;
1047: }
1048: $this->_loadById($id);
1049:
1050: return isset($this->_data[$id]['order']) ?
1051: $this->_data[$id]['order'] :
1052: null;
1053: }
1054:
1055: /**
1056: * Replace all occurences of ':' in an object name with '.'.
1057: *
1058: * @param string $name The name of the object.
1059: *
1060: * @return string The encoded name.
1061: */
1062: function encodeName($name)
1063: {
1064: return str_replace(':', '.', $name);
1065: }
1066:
1067: /**
1068: * Get the short name of an object, returns only the last portion of the
1069: * full name. For display purposes only.
1070: *
1071: * @static
1072: *
1073: * @param string $name The name of the object.
1074: *
1075: * @return string The object's short name.
1076: */
1077: function getShortName($name)
1078: {
1079: /* If there are several components to the name, explode and get the
1080: * last one, otherwise just return the name. */
1081: if (strpos($name, ':') !== false) {
1082: $name = explode(':', $name);
1083: $name = array_pop($name);
1084: }
1085: return $name;
1086: }
1087:
1088: /**
1089: * Returns a tree sorted by the specified attribute name and/or key.
1090: *
1091: * @abstract
1092: *
1093: * @param string $root Which portion of the tree to sort.
1094: * Defaults to all of it.
1095: * @param boolean $loadTree Sort the tree starting at $root, or just the
1096: * requested level and direct parents?
1097: * Defaults to single level.
1098: * @param string $sortby_name Attribute name to use for sorting.
1099: * @param string $sortby_key Attribute key to use for sorting.
1100: * @param integer $direction Sort direction:
1101: * 0 - ascending
1102: * 1 - descending
1103: *
1104: * @return array TODO
1105: */
1106: function getSortedTree($root, $loadTree = false, $sortby_name = null,
1107: $sortby_key = null, $direction = 0)
1108: {
1109: return PEAR::raiseError('not supported');
1110: }
1111:
1112: /**
1113: * Adds an object.
1114: *
1115: * @abstract
1116: *
1117: * @param mixed $object The object to add (string or
1118: * Horde_DataTreeObject).
1119: * @param boolean $id_as_name True or false to indicate if object ID is to
1120: * be used as object name. Used in situations
1121: * where there is no available unique input for
1122: * object name.
1123: *
1124: * @return TODO
1125: */
1126: function add($object, $id_as_name = false)
1127: {
1128: return PEAR::raiseError('not supported');
1129: }
1130:
1131: /**
1132: * Add an object.
1133: *
1134: * @private
1135: *
1136: * @param string $name The short object name.
1137: * @param integer $id The new object's unique ID.
1138: * @param integer $pid The unique ID of the object's parent.
1139: * @param integer $order The ordering data for the object.
1140: *
1141: * @access protected
1142: *
1143: * @return TODO
1144: */
1145: function _add($name, $id, $pid, $order = '')
1146: {
1147: $this->_data[$id] = array('name' => $name,
1148: 'parent' => $pid,
1149: 'order' => $order);
1150: $this->_nameMap[$id] = $name;
1151:
1152: /* Shift along the order positions. */
1153: $this->_reorder($pid, $order, $id);
1154:
1155: return true;
1156: }
1157:
1158: /**
1159: * Retrieve data for an object from the horde_datatree_attributes
1160: * table.
1161: *
1162: * @abstract
1163: *
1164: * @param integer | array $cid The object id to fetch,
1165: * or an array of object ids.
1166: *
1167: * @return array A hash of attributes, or a multi-level hash
1168: * of object ids => their attributes.
1169: */
1170: function getAttributes($cid)
1171: {
1172: return PEAR::raiseError('not supported');
1173: }
1174:
1175: /**
1176: * Returns the number of objects matching a set of attribute criteria.
1177: *
1178: * @abstract
1179: *
1180: * @see buildAttributeQuery()
1181: *
1182: * @param array $criteria The array of criteria.
1183: * @param string $parent The parent node to start searching from.
1184: * @param boolean $allLevels Return all levels, or just the direct
1185: * children of $parent? Defaults to all levels.
1186: * @param string $restrict Only return attributes with the same
1187: * attribute_name or attribute_id.
1188: *
1189: * @return TODO
1190: */
1191: function countByAttributes($criteria, $parent = DATATREE_ROOT,
1192: $allLevels = true, $restrict = 'name')
1193: {
1194: return PEAR::raiseError('not supported');
1195: }
1196:
1197: /**
1198: * Returns a set of object ids based on a set of attribute criteria.
1199: *
1200: * @abstract
1201: *
1202: * @see buildAttributeQuery()
1203: *
1204: * @param array $criteria The array of criteria.
1205: * @param string $parent The parent node to start searching from.
1206: * @param boolean $allLevels Return all levels, or just the direct
1207: * children of $parent? Defaults to all levels.
1208: * @param string $restrict Only return attributes with the same
1209: * attribute_name or attribute_id.
1210: * @param integer $from The object to start to fetching
1211: * @param integer $count The number of objects to fetch
1212: * @param string $sortby_name Attribute name to use for sorting.
1213: * @param string $sortby_key Attribute key to use for sorting.
1214: * @param integer $direction Sort direction:
1215: * 0 - ascending
1216: * 1 - descending
1217: *
1218: * @return TODO
1219: */
1220: function getByAttributes($criteria, $parent = DATATREE_ROOT,
1221: $allLevels = true, $restrict = 'name', $from = 0,
1222: $count = 0, $sortby_name = null,
1223: $sortby_key = null, $direction = 0)
1224: {
1225: return PEAR::raiseError('not supported');
1226: }
1227:
1228: /**
1229: * Sorts IDs by attribute values. IDs without attributes will be added to
1230: * the end of the sorted list.
1231: *
1232: * @abstract
1233: *
1234: * @param array $unordered_ids Array of ids to sort.
1235: * @param array $sortby_name Attribute name to use for sorting.
1236: * @param array $sortby_key Attribute key to use for sorting.
1237: * @param array $direction Sort direction:
1238: * 0 - ascending
1239: * 1 - descending
1240: *
1241: * @return array Sorted ids.
1242: */
1243: function sortByAttributes($unordered_ids, $sortby_name = null,
1244: $sortby_key = null, $direction = 0)
1245: {
1246: return PEAR::raiseError('not supported');
1247: }
1248:
1249: /**
1250: * Update the data in an object. Does not change the object's
1251: * parent or name, just serialized data or attributes.
1252: *
1253: * @abstract
1254: *
1255: * @param Horde_DataTree $object A Horde_DataTree object.
1256: *
1257: * @return TODO
1258: */
1259: function updateData($object)
1260: {
1261: return PEAR::raiseError('not supported');
1262: }
1263:
1264: /**
1265: * Sort two objects by their order field, and if that is the same,
1266: * alphabetically (case insensitive) by name.
1267: *
1268: * You never call this function; it's used in uasort() calls. Do
1269: * NOT use usort(); you'll lose key => value associations.
1270: *
1271: * @private
1272: *
1273: * @param array $a The first object
1274: * @param array $b The second object
1275: *
1276: * @return integer 1 if $a should be first,
1277: * -1 if $b should be first,
1278: * 0 if they are entirely equal.
1279: */
1280: function _cmp($a, $b)
1281: {
1282: if ($a['order'] > $b['order']) {
1283: return 1;
1284: } elseif ($a['order'] < $b['order']) {
1285: return -1;
1286: } else {
1287: return strcasecmp($a['name'], $b['name']);
1288: }
1289: }
1290:
1291: /**
1292: * Sorts two objects by their sorter hash field.
1293: *
1294: * You never call this function; it's used in uasort() calls. Do NOT use
1295: * usort(); you'll lose key => value associations.
1296: *
1297: * @private
1298: *
1299: * @param array $a The first object
1300: * @param array $b The second object
1301: *
1302: * @return integer 1 if $a should be first,
1303: * -1 if $b should be first,
1304: * 0 if they are entirely equal.
1305: */
1306: function _cmpSorted($a, $b)
1307: {
1308: return intval($a['sorter'][$this->_sortHash] < $b['sorter'][$this->_sortHash]);
1309: }
1310:
1311: /**
1312: * Attempts to return a concrete Horde_DataTree instance based on $driver.
1313: *
1314: * @param mixed $driver The type of concrete Horde_DataTree subclass to return.
1315: * This is based on the storage driver ($driver). The
1316: * code is dynamically included. If $driver is an array,
1317: * then we will look in $driver[0]/lib/DataTree/ for
1318: * the subclass implementation named $driver[1].php.
1319: * @param array $params A hash containing any additional configuration or
1320: * connection parameters a subclass might need.
1321: * Here, we need 'group' = a string that defines
1322: * top-level groups of objects.
1323: *
1324: * @return Horde_DataTree The newly created concrete Horde_DataTree instance, or false
1325: * on an error.
1326: */
1327: function &factory($driver, $params = null)
1328: {
1329: $driver = Horde_String::ucfirst(basename($driver));
1330:
1331: if (is_null($params)) {
1332: $params = Horde::getDriverConfig('datatree', $driver);
1333: }
1334:
1335: if (empty($driver)) {
1336: $driver = 'Null';
1337: }
1338:
1339: $class = 'Horde_DataTree_' . $driver;
1340: if (class_exists($class)) {
1341: $dt = new $class($params);
1342: $result = $dt->_init();
1343: if (is_a($result, 'PEAR_Error')) {
1344: $dt = new Horde_DataTree_Null($params);
1345: }
1346: } else {
1347: $dt = PEAR::raiseError('Class definition of ' . $class . ' not found.');
1348: }
1349:
1350: return $dt;
1351: }
1352:
1353: /**
1354: * Attempts to return a reference to a concrete Horde_DataTree instance based on
1355: * $driver.
1356: *
1357: * It will only create a new instance if no Horde_DataTree instance with the same
1358: * parameters currently exists.
1359: *
1360: * This should be used if multiple Horde_DataTree sources (and, thus, multiple
1361: * Horde_DataTree instances) are required.
1362: *
1363: * This method must be invoked as: $var = &Horde_DataTree::singleton();
1364: *
1365: * @param mixed $driver Type of concrete Horde_DataTree subclass to return,
1366: * based on storage driver ($driver). The code is
1367: * dynamically included. If $driver is an array, then
1368: * look in $driver[0]/lib/DataTree/ for subclass
1369: * implementation named $driver[1].php.
1370: * @param array $params A hash containing any additional configuration or
1371: * connection parameters a subclass might need.
1372: *
1373: * @return Horde_DataTree The concrete Horde_DataTree reference, or false on an error.
1374: */
1375: function &singleton($driver, $params = null)
1376: {
1377: static $instances = array();
1378:
1379: if (is_null($params)) {
1380: $params = Horde::getDriverConfig('datatree', $driver);
1381: }
1382:
1383: $signature = serialize(array($driver, $params));
1384: if (!isset($instances[$signature])) {
1385: $instances[$signature] = &Horde_DataTree::factory($driver, $params);
1386: }
1387:
1388: return $instances[$signature];
1389: }
1390:
1391: }
1392: