1: <?php
2: /**
3: * A cache backend for Kolab storage data handlers.
4: *
5: * PHP version 5
6: *
7: * @category Kolab
8: * @package Kolab_Storage
9: * @author Thomas Jarosch <thomas.jarosch@intra2net.com>
10: * @author Gunnar Wrobel <wrobel@pardus.de>
11: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
12: * @link http://pear.horde.org/index.php?package=Kolab_Storage
13: */
14:
15: /**
16: * A cache backend for Kolab storage data handlers.
17: *
18: * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
19: *
20: * See the enclosed file COPYING for license information (LGPL). If you
21: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
22: *
23: * @category Kolab
24: * @package Kolab_Storage
25: * @author Thomas Jarosch <thomas.jarosch@intra2net.com>
26: * @author Gunnar Wrobel <wrobel@pardus.de>
27: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
28: * @link http://pear.horde.org/index.php?package=Kolab_Storage
29: */
30: class Horde_Kolab_Storage_Cache_Data
31: {
32: /** Key for the backend ID to object ID mapping. */
33: const B2O = 'M';
34:
35: /** Key for the object ID to backend ID mapping. */
36: const O2B = 'B';
37:
38: /** Key for the objects. */
39: const OBJECTS = 'O';
40:
41: /** Key for recording duplicate objects. */
42: const DUPLICATES = 'U';
43:
44: /** Key for recording error objects. */
45: const ERRORS = 'E';
46:
47: /** Key for the stamp. */
48: const STAMP = 'P';
49:
50: /** Key for the data format version. */
51: const DATA_VERSION = 'D';
52:
53: /** Key for the last time the data was synchronized. */
54: const SYNC = 'S';
55:
56: /** Key for the cache format version. */
57: const VERSION = 'V';
58:
59: /** Key for the data set parameters associated with this cache. */
60: const ID = 'I';
61:
62: /** Holds the version number of the cache format. */
63: const FORMAT_VERSION = '1';
64:
65: /** Holds query results. */
66: const QUERIES = 'Q';
67:
68: /**
69: * The core cache driver.
70: *
71: * @var Horde_Kolab_Storage_Cache
72: */
73: private $_cache;
74:
75: /**
76: * Data parameters that will be recorded in the cache.
77: *
78: * @var array
79: */
80: private $_parameters;
81:
82: /**
83: * Data ID.
84: *
85: * @var string
86: */
87: private $_data_id;
88:
89: /**
90: * The cache data.
91: *
92: * @var array
93: */
94: private $_data = false;
95:
96: /**
97: * Constructor.
98: *
99: * @param Horde_Kolab_Storage_Cache $cache The core cache driver.
100:
101: * @param array $parameters Data set parameters that
102: * are only recorded and have
103: * no further impact.
104: */
105: public function __construct(Horde_Kolab_Storage_Cache $cache,
106: $parameters = null)
107: {
108: $this->_cache = $cache;
109: $this->_parameters = $parameters;
110: }
111:
112: /**
113: * The ID for the data cache.
114: *
115: * @param string $data_id The unique ID for the data used when caching it.
116: *
117: * @return NULL
118: */
119: public function setDataId($data_id)
120: {
121: $this->_data_id = $data_id;
122: }
123:
124: /**
125: * Return the ID for the data cache.
126: *
127: * @return string The unique ID for the data used when caching it.
128: */
129: public function getDataId()
130: {
131: if ($this->_data_id === null) {
132: throw new Horde_Kolab_Storage_Exception(
133: 'You must set the ID of the data cache!'
134: );
135: }
136: return $this->_data_id;
137: }
138:
139: /**
140: * Retrieve the cached list data.
141: *
142: * @return mixed The data of the object.
143: */
144: private function _load()
145: {
146: if ($this->_data === false) {
147: $this->_data = unserialize($this->_cache->loadData($this->getDataId()));
148: if (!is_array($this->_data)
149: || !isset($this->_data[self::SYNC])
150: || !isset($this->_data[self::VERSION])
151: || $this->_data[self::VERSION] != self::FORMAT_VERSION) {
152: $this->_data = array();
153: }
154: }
155: }
156:
157: /**
158: * Cache the data.
159: *
160: * @return NULL
161: */
162: public function save()
163: {
164: $this->_cache->storeData($this->getDataId(), serialize($this->_data));
165: }
166:
167: /**
168: * Check if the cache has been initialized.
169: *
170: * @return boolean True if cache data is available.
171: */
172: public function isInitialized()
173: {
174: $this->_load();
175: return !empty($this->_data);
176: }
177:
178: /**
179: * Retrieve the object list from the cache.
180: *
181: * @return array The list of objects.
182: */
183: public function getObjects()
184: {
185: return $this->_fetchCacheEntry(self::OBJECTS);
186: }
187:
188: /**
189: * Retrieve the specified object from the cache.
190: *
191: * @param string $obid The object ID to fetch.
192: *
193: * @return array The list of objects.
194: */
195: public function getObjectByBackendId($obid)
196: {
197: $obids = $this->getBackendToObject();
198: if (isset($obids[$obid])) {
199: $objects = $this->getObjects();
200: return $objects[$obids[$obid]];
201: } else {
202: throw new Horde_Kolab_Storage_Exception(
203: sprintf ('No such object %s!', $obid)
204: );
205: }
206: }
207:
208: /**
209: * Return the object ID to backend ID mapping.
210: *
211: * @return array The mapping.
212: */
213: public function getObjectToBackend()
214: {
215: return $this->_fetchCacheEntry(self::O2B);
216: }
217:
218: /**
219: * Return the backend ID to object ID mapping.
220: *
221: * @return array The mapping.
222: */
223: public function getBackendToObject()
224: {
225: return $this->_fetchCacheEntry(self::B2O);
226: }
227:
228: /**
229: * Retrieve the last stamp.
230: *
231: * @return Horde_Kolab_Storage_Folder_Stamp The last recorded stamp.
232: */
233: public function getStamp()
234: {
235: $this->_checkInit(self::STAMP);
236: return $this->_data[self::STAMP];
237: }
238:
239: /**
240: * Retrieve the data version.
241: *
242: * @return string The version of the stored data.
243: */
244: public function getVersion()
245: {
246: $this->_checkInit(self::DATA_VERSION);
247: return $this->_data[self::DATA_VERSION];
248: }
249:
250: /**
251: * Retrieve the list of object duplicates.
252: *
253: * @since Horde_Kolab_Storage 1.1.0
254: *
255: * @return array The list of duplicates.
256: */
257: public function getDuplicates()
258: {
259: return $this->_fetchCacheEntry(self::DUPLICATES);
260: }
261:
262: /**
263: * Retrieve the list of object errors.
264: *
265: * @since Horde_Kolab_Storage 1.1.0
266: *
267: * @return array The list of errors.
268: */
269: public function getErrors()
270: {
271: return $this->_fetchCacheEntry(self::ERRORS);
272: }
273:
274: /**
275: * Retrieve an attachment.
276: *
277: * @param string $obid Object backend id.
278: * @param string $attachment_id Attachment ID.
279: *
280: * @return resource A stream opened to the attachement data.
281: */
282: public function getAttachment($obid, $attachment_id)
283: {
284: return $this->_cache->loadAttachment(
285: $this->getDataId(), $obid, $attachment_id
286: );
287: }
288:
289: /**
290: * Retrieve an attachment by name.
291: *
292: * @param string $obid Object backend id.
293: * @param string $attachment_id Attachment ID.
294: *
295: * @return array An array of attachment resources.
296: */
297: public function getAttachmentByName($obid, $name)
298: {
299: $object = $this->getObjectByBackendId($obid);
300: if (!isset($object['_attachments']['name'][$name])) {
301: throw new Horde_Kolab_Storage_Exception(
302: sprintf(
303: 'No attachment named "%s" for object id %s!',
304: $name,
305: $obid
306: )
307: );
308: }
309: $result = array();
310: foreach ($object['_attachments']['name'][$name] as $attachment_id) {
311: $result[$attachment_id] = $this->_cache->loadAttachment(
312: $this->getDataId(), $obid, $attachment_id
313: );
314: }
315: return $result;
316: }
317:
318: /**
319: * Retrieve an attachment by name.
320: *
321: * @param string $obid Object backend id.
322: * @param string $attachment_id Attachment ID.
323: *
324: * @return array An array of attachment resources.
325: */
326: public function getAttachmentByType($obid, $type)
327: {
328: $object = $this->getObjectByBackendId($obid);
329: if (!isset($object['_attachments']['type'][$type])) {
330: throw new Horde_Kolab_Storage_Exception(
331: sprintf(
332: 'No attachment with type "%s" for object id %s!',
333: $type,
334: $obid
335: )
336: );
337: }
338: $result = array();
339: foreach ($object['_attachments']['type'][$type] as $attachment_id) {
340: $result[$attachment_id] = $this->_cache->loadAttachment(
341: $this->getDataId(), $obid, $attachment_id
342: );
343: }
344: return $result;
345: }
346:
347: /**
348: * Return the timestamp of the last synchronization.
349: *
350: * @since Horde_Kolab_Storage 1.1.0
351: *
352: * @return int Timestamp of the last sync.
353: */
354: public function getLastSync()
355: {
356: $this->_load();
357: return isset($this->_data[self::SYNC]) ? $this->_data[self::SYNC] : false;
358: }
359:
360: /**
361: * Is the specified query data available in the cache?
362: *
363: * @since Horde_Kolab_Storage 1.1.0
364: *
365: * @param string $key The query key.
366: *
367: * @return boolean True in case cached data is available.
368: */
369: public function hasQuery($key)
370: {
371: $this->_load();
372: return isset($this->_data[self::QUERIES][$key]);
373: }
374:
375: /**
376: * Return query information.
377: *
378: * @since Horde_Kolab_Storage 1.1.0
379: *
380: * @param string $key The query key.
381: *
382: * @return mixed The query data.
383: */
384: public function getQuery($key)
385: {
386: if ($this->hasQuery($key)) {
387: return $this->_data[self::QUERIES][$key];
388: } else {
389: throw new Horde_Kolab_Storage_Exception(
390: sprintf('Missing query cache data (Key: %s). Synchronize first!', $key)
391: );
392: }
393: }
394:
395: /**
396: * Set query information.
397: *
398: * @since Horde_Kolab_Storage 1.1.0
399: *
400: * @param string $key The query key.
401: * @param mixed $data The query data.
402: *
403: * @return NULL
404: */
405: public function setQuery($key, $data)
406: {
407: $this->_load();
408: $this->_data[self::QUERIES][$key] = $data;
409: }
410:
411: /**
412: * Fetch the specified cache entry in case it is present. Returns an empty
413: * array otherwise.
414: *
415: * @param string $key The key in the cached data array.
416: *
417: * @return array The cache entry.
418: */
419: private function _fetchCacheEntry($key)
420: {
421: $this->_checkInit($key);
422: if (isset($this->_data[$key])) {
423: return $this->_data[$key];
424: } else {
425: return array();
426: }
427: }
428:
429: /**
430: * Verify that the data cache is initialized.
431: *
432: * @param string $key The key in the cached data array.
433: *
434: * @return NULL
435: *
436: * @throws Horde_Kolab_Storage_Exception In case the cache has not been
437: * initialized.
438: */
439: private function _checkInit($key)
440: {
441: if (!$this->isInitialized()) {
442: throw new Horde_Kolab_Storage_Exception(
443: sprintf('Missing cache data (Key: %s). Synchronize first!', $key)
444: );
445: }
446: }
447:
448: /**
449: * Map backend IDs to object ids.
450: *
451: * @since Horde_Kolab_Storage 1.1.0
452: *
453: * @param array $backend_ids The list of backend IDs
454: *
455: * @return array A list that associates object IDs (values) to backend IDs
456: * (keys).
457: */
458: public function backendMap($backend_ids)
459: {
460: if (empty($backend_ids)) {
461: return array();
462: }
463: $map = array();
464: foreach ($backend_ids as $item) {
465: $map[$item] = $this->_data[self::B2O][$item];
466: }
467: return $map;
468: }
469:
470: /**
471: * Store the objects list in the cache.
472: *
473: * @param array $object The object data to store.
474: * @param Horde_Kolab_Storage_Folder_Stamp $stamp The current stamp.
475: * @param string $version The format version of
476: * the provided data.
477: * @param array $delete Backend IDs that were removed.
478: *
479: * @return NULL
480: */
481: public function store(array $objects,
482: Horde_Kolab_Storage_Folder_Stamp $stamp,
483: $version,
484: array $delete = array())
485: {
486: $this->_load();
487: if (!empty($delete)) {
488: foreach ($delete as $obid => $object_id) {
489: $object = $this->_data[self::OBJECTS][$object_id];
490: if (isset($object['_attachments'])) {
491: foreach ($object['_attachments']['id'] as $id) {
492: $this->_cache->deleteAttachment(
493: $this->getDataId(), $obid, $id
494: );
495: }
496: }
497: unset($this->_data[self::O2B][$object_id]);
498: unset($this->_data[self::OBJECTS][$object_id]);
499: unset($this->_data[self::B2O][$obid]);
500: }
501: }
502: foreach ($objects as $obid => $object) {
503: if (!empty($object) && isset($object['uid'])) {
504: if (isset($this->_data[self::O2B][$object['uid']])) {
505: if (!isset($this->_data[self::DUPLICATES][$object['uid']])) {
506: $this->_data[self::DUPLICATES][$object['uid']][] = $this->_data[self::O2B][$object['uid']];
507: }
508: $this->_data[self::DUPLICATES][$object['uid']][] = $obid;
509: }
510: $this->_data[self::B2O][$obid] = $object['uid'];
511: $this->_data[self::O2B][$object['uid']] = $obid;
512: if (isset($object['_attachments'])) {
513: $attachments = array();
514: foreach ($object['_attachments'] as $id => $attachment) {
515: $attachments['id'][] = $id;
516: if (isset($attachment['name'])) {
517: $attachments['name'][$attachment['name']][] = $id;
518: }
519: if (isset($attachment['type'])) {
520: $attachments['type'][$attachment['type']][] = $id;
521: }
522: $this->_cache->storeAttachment($this->getDataId(), $obid, $id, $attachment['content']);
523: }
524: $object['_attachments'] = $attachments;
525: }
526: $this->_data[self::OBJECTS][$object['uid']] = $object;
527: } else {
528: $this->_data[self::B2O][$obid] = false;
529: $this->_data[self::ERRORS][] = $obid;
530: }
531: }
532: $this->_data[self::QUERIES] = array();
533: $this->_data[self::STAMP] = serialize($stamp);
534: $this->_data[self::DATA_VERSION] = $version;
535: $this->_data[self::VERSION] = self::FORMAT_VERSION;
536: $this->_data[self::ID] = serialize($this->_parameters);
537: $this->_data[self::SYNC] = time();
538: }
539:
540: /**
541: * Initialize the cache structure.
542: *
543: * @return NULL
544: */
545: public function reset()
546: {
547: $this->_data = array();
548: }
549: }
550: