5a83742767a118b41b886e3555fc660925a6c4da
[mailer.git] / inc / classes / main / database / classes / class_LocalFileDatabase.php
1 <?php
2 /**
3  * Database backend class for storing objects in locally created files.
4  *
5  * This class serializes objects and saves them to local files.
6  *
7  * @author      Roland Haeder <roland __NOSPAM__ [at] __REMOVE_ME__ mxchange [dot] org>
8  * @version     0.1
9  */
10 class LocalFileDatabase extends BaseDatabaseFrontend implements DatabaseFrontendInterface {
11         /**
12          * Save path for "file database"
13          */
14         private $savePath = "";
15
16         /**
17          * The file's extension
18          */
19         private $fileExtension = "serialized";
20
21         /**
22          * The IO handler for file handling which should be FileIOHandler.
23          */
24         private $ioInstance = null;
25
26         /**
27          * The last read file's name
28          */
29         private $lastFile = "";
30
31         /**
32          * The last read file's content including header information
33          */
34         private $lastContents = array();
35
36         /**
37          * The private constructor. Do never instance from outside!
38          * You need to set a local file path. The class will then validate it.
39          *
40          * @return      void
41          */
42         private function __construct() {
43                 // Call parent constructor
44                 parent::constructor(__CLASS__);
45
46                 // Set description
47                 $this->setPartDescr("Dateidatenbankschicht");
48
49                 // Create unique ID
50                 $this->createUniqueID();
51
52                 // Clean up a little
53                 $this->removeSystemArray();
54         }
55
56         /**
57          * Create an object of LocalFileDatabase and set the save path for local files.
58          * This method also validates the given file path.
59          *
60          * @param               $savePath                                       The local file path string
61          * @param               $ioInstance                             The input/output handler. This
62          *                                                                      should be FileIOHandler
63          * @return      $dbInstance                             An instance of LocalFileDatabase
64          * @throws      SavePathIsEmptyException                If the given save path is an
65          *                                                                      empty string
66          * @throws      SavePathIsNoDirectoryException  If the save path is no
67          *                                                                              path (e.g. a file)
68          * @throws      SavePathReadProtectedException  If the save path is read-
69          *                                                                              protected
70          * @throws      SavePathWriteProtectedException If the save path is write-
71          *                                                                              protected
72          */
73         public final static function createLocalFileDatabase ($savePath, FileIOHandler $ioInstance) {
74                 // Get an instance
75                 $dbInstance = new LocalFileDatabase();
76
77                 if (empty($savePath)) {
78                         // Empty string
79                         throw new SavePathIsEmptyException($dbInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
80                 } elseif (!is_dir($savePath)) {
81                         // Is not a dir
82                         throw new SavePathIsNoDirectoryException($savePath, self::EXCEPTION_INVALID_PATH_NAME);
83                 } elseif (!is_readable($savePath)) {
84                         // Path not readable
85                         throw new SavePathReadProtectedException($savePath, self::EXCEPTION_READ_PROTECED_PATH);
86                 } elseif (!is_writeable($savePath)) {
87                         // Path not writeable
88                         throw new SavePathWriteProtectedException($savePath, self::EXCEPTION_WRITE_PROTECED_PATH);
89                 }
90
91                 // Debug output
92                 if (defined('DEBUG_DATABASE')) $dbInstance->getDebugInstance()->output(sprintf("[%s:] Es werden lokale Dateien zum Speichern von Objekten verwendet.<br />\n",
93                         $dbInstance->__toString()
94                 ));
95
96                 // Set save path and IO instance
97                 $dbInstance->setSavePath($savePath);
98                 $dbInstance->setIOInstance($ioInstance);
99
100                 // Return database instance
101                 return $dbInstance;
102         }
103
104         /**
105          * Setter for save path
106          *
107          * @param               $savePath               The local save path where we shall put our serialized classes
108          * @return      void
109          */
110         public final function setSavePath ($savePath) {
111                 // Secure string
112                 $savePath = (string) $savePath;
113
114                 // Debug message
115                 if ((defined('DEBUG_DATABASE')) || (defined('DEBUG_ALL'))) $this->getDebugInstance()->output(sprintf("[%s:] Lokaler Speicherpfad <strong>%s</strong> wird verwendet.<br />\n",
116                         $this->__toString(),
117                         $savePath
118                 ));
119
120                 // Set save path
121                 $this->savePath = $savePath;
122         }
123
124         /**
125          * Getter for save path
126          *
127          * @return      $savePath               The local save path where we shall put our serialized classes
128          */
129         public final function getSavePath () {
130                 return $this->savePath;
131         }
132
133         /**
134          * Getter for file extension
135          *
136          * @return      $fileExtension          The file extension for all file names
137          */
138         public final function getFileExtension () {
139                 return $this->fileExtension;
140         }
141
142         /**
143          * Saves a given object to the local file system by serializing and
144          * transparently compressing it
145          *
146          * @param               $object                         The object we shall save to the local file system
147          * @return      void
148          * @throws      NullPointerException    If the object instance is null
149          * @throws      NoObjectException               If the parameter $object is not
150          *                                                              an object
151          */
152         public final function saveObject ($object) {
153                 // Some tests on the parameter...
154                 if (is_null($object)) {
155                         // Is null, throw exception
156                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
157                 } elseif (!is_object($object)) {
158                         // Is not an object, throw exception
159                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
160                 } elseif (!method_exists($object, '__toString')) {
161                         // A highly required method was not found... :-(
162                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
163                 }
164
165                 // Debug message
166                 if ((defined('DEBUG_DATABASE')) || (defined('DEBUG_ALL'))) $this->getDebugInstance()->output(sprintf("[%s:] Das Objekt <strong>%s</strong> soll in eine lokale Datei gespeichert werden.<br />\n",
167                         $this->__toString(),
168                         $object->__toString()
169                 ));
170
171                 // Get a string containing the serialized object. We cannot exchange
172                 // $this and $object here because $object does not need to worry
173                 // about it's limitations... ;-)
174                 $serialized = $this->serializeObject($object);
175
176                 // Debug message
177                 if ((defined('DEBUG_DATABASE')) || (defined('DEBUG_ALL'))) $this->getDebugInstance()->output(sprintf("[%s:] Das Objekt <strong>%s</strong> ist nach der Serialisierung <strong>%s</strong> Byte gross.<br />\n",
178                         $this->__toString(),
179                         $object->__toString(),
180                         strlen($serialized)
181                 ));
182
183                 // Get a path name plus file name and append the extension
184                 $fqfn = $this->getSavePath() . $object->getPathFileNameFromObject() . "." . $this->getFileExtension();
185
186                 // Save the file to disc we don't care here if the path is there,
187                 // this must be done in later methods.
188                 $this->getIOInstance()->saveFile($fqfn, array($this->getCompressorChannel()->getCompressorExtension(), $serialized));
189         }
190
191         /**
192          * Get a serialized string from the given object
193          *
194          * @param               $object         The object we want to serialize and transparently
195          *                                              compress
196          * @return      $serialized     A string containing the serialzed/compressed object
197          * @see         ObjectLimits    An object holding limition information
198          * @see         SerializationContainer  A special container class for e.g.
199          *                                                              attributes from limited objects
200          */
201         private function serializeObject ($object) {
202                 // If there is no limiter instance we serialize the whole object
203                 // otherwise only in the limiter object (ObjectLimits) specified
204                 // attributes summarized in a special container class
205                 if ($this->getLimitInstance() === null) {
206                         // Serialize the whole object. This tribble call is the reason
207                         // why we need a fall-back implementation in CompressorChannel
208                         // of the methods compressStream() and decompressStream().
209                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($object));
210                 } else {
211                         // Serialize only given attributes in a special container
212                         $container = SerializationContainer::createSerializationContainer($this->getLimitInstance(), $object);
213
214                         // Serialize the container
215                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($container));
216                 }
217
218                 // Return the serialized object string
219                 return $serialized;
220         }
221
222         /**
223          * Analyses if a unique ID has already been used or not by search in the
224          * local database folder.
225          *
226          * @param               $uniqueID               A unique ID number which shall be checked
227          *                                              before it will be used
228          * @param               $inConstructor  If we got called in a de/con-structor or
229          *                                              from somewhere else
230          * @return      $isUnused               true    = The unique ID was not found in the database,
231          *                                              false = It is already in use by an other object
232          * @throws      NoArrayCreatedException If explode() fails to create an array
233          * @throws      InvalidArrayCountException      If the array contains less or
234          *                                                                      more than two elements
235          */
236         public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
237                 // Currently not used... ;-)
238                 $isUsed = false;
239
240                 // Split the unique ID up in path and file name
241                 $pathFile = explode("@", $uniqueID);
242
243                 // Are there two elements? Index 0 is the path, 1 the file name + global extension
244                 if (!is_array($pathFile)) {
245                         // No array found
246                         if ($inConstructor) {
247                                 return false;
248                         } else {
249                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
250                         }
251                 } elseif (count($pathFile) != 2) {
252                         // Invalid ID returned!
253                         if ($inConstructor) {
254                                 return false;
255                         } else {
256                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
257                         }
258                 }
259
260                 // Create full path name
261                 $pathName = $this->getSavePath() . $pathFile[0];
262
263                 // Check if the file is there with a file handler
264                 if ($inConstructor) {
265                         // No exceptions in constructors and destructors!
266                         $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
267
268                         // Has an object being created?
269                         if (!is_object($dirInstance)) return false;
270                 } else {
271                         // Outside a constructor
272                         try {
273                                 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
274                         } catch (PathIsNoDirectoryException $e) {
275                                 // Okay, path not found
276                                 return false;
277                         }
278                 }
279
280                 // Initialize the search loop
281                 $isValid = false;
282                 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", ".."))) {
283                         // Generate FQFN for testing
284                         $fqfn = sprintf("%s/%s", $pathName, $dataFile);
285                         $this->setLastFile($fqfn);
286
287                         // Get instance for file handler
288                         $inputHandler = $this->getIOInstance();
289
290                         // Try to read from it. This makes it sure that the file is
291                         // readable and a valid database file
292                         $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
293
294                         // Extract filename (= unique ID) from it
295                         $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
296
297                         // Is this the required unique ID?
298                         if ($ID == $pathFile[1]) {
299                                 // Okay, already in use!
300                                 $isUsed = true;
301                         }
302                 }
303
304                 // Close the directory handler
305                 $dirInstance->closeDirectory();
306
307                 // Now the same for the file...
308                 return $isUsed;
309         }
310
311         /**
312          * Getter for the file IO instance
313          *
314          * @return      $ioInstance     An instance for IO operations
315          * @see         FileIOHandler   The concrete handler for IO operations
316          */
317         public final function getIOInstance () {
318                 return $this->ioInstance;
319         }
320
321         /**
322          * Setter for the file IO instance
323          *
324          * @param               $ioInstance     An instance for IO operations (should be
325          *                                              FileIOHandler)
326          * @return      void
327          */
328         public final function setIOInstance (FileIOHandler $ioInstance) {
329                 $this->ioInstance = $ioInstance;
330         }
331
332         /**
333          * Setter for the last read file
334          *
335          * @param               $fqfn   The FQFN of the last read file
336          * @return      void
337          */
338         private function setLastFile ($fqfn) {
339                 // Cast string
340                 $fqfn = (string) $fqfn;
341                 $this->lastFile = $fqfn;
342         }
343
344         /**
345          * Getter for last read file
346          *
347          * @return      $lastFile               The last read file's name with full path
348          */
349         public final function getLastFile () {
350                 return $this->lastFile;
351         }
352
353         /**
354          * Setter for contents of the last read file
355          *
356          * @param               $contents               An array with header and data elements
357          * @return      void
358          */
359         private function setLastFileContents ($contents) {
360                 // Cast array
361                 $contents = (array) $contents;
362                 $this->lastContents = $contents;
363         }
364
365         /**
366          * Getter for last read file's content as an array
367          *
368          * @return      $lastContent    The array with elements 'header' and 'data'.
369          */
370         public final function getLastContents () {
371                 return $this->lastContents;
372         }
373
374         /**
375          * Get cached (last fetched) data from the local file database
376          *
377          * @param               $uniqueID               The ID number for looking up the data
378          * @return      $object         The restored object from the maybe compressed
379          *                                              serialized data
380          * @throws      MismatchingCompressorsException If the compressor from
381          *                                                                              the loaded file
382          *                                                                              mismatches with the 
383          *                                                                              current used one.
384          * @throws      NullPointerException                    If the restored object
385          *                                                                              is null
386          * @throws      NoObjectException                               If the restored "object"
387          *                                                                              is not an object instance
388          * @throws      MissingMethodException                  If the required method
389          *                                                                              toString() is missing
390          */
391         public function getObjectFromCachedData ($uniqueID) {
392                 // Get instance for file handler
393                 $inputHandler = $this->getIOInstance();
394
395                 // Get last file's name and contents
396                 $fqfn = $this->repairFQFN($this->getLastFile(), $uniqueID);
397                 $contents = $this->repairContents($this->getLastContents(), $fqfn);
398
399                 // Let's decompress it. First we need the instance
400                 $compressInstance = $this->getCompressorChannel();
401
402                 // Is the compressor's extension the same as the one from the data?
403                 if ($compressInstance->getCompressorExtension() != $contents['header'][0]) {
404                         /**
405                          * @todo        For now we abort here but later we need to make this a little more dynamic.
406                          */
407                         throw new MismatchingCompressorsException(array($this, $contents['header'][0], $fqfn, $compressInstance->getCompressorExtension()), self::EXCEPTION_MISMATCHING_COMPRESSORS);
408                 }
409
410                 // Decompress the data now
411                 $serialized = $compressInstance->getCompressor()->decompressStream($contents['data']);
412
413                 // And unserialize it...
414                 $object = unserialize($serialized);
415
416                 // This must become a valid object, so let's check it...
417                 if (is_null($object)) {
418                         // Is null, throw exception
419                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
420                 } elseif (!is_object($object)) {
421                         // Is not an object, throw exception
422                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
423                 } elseif (!method_exists($object, '__toString')) {
424                         // A highly required method was not found... :-(
425                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
426                 }
427
428                 // And return the object
429                 return $object;
430         }
431
432         /**
433          * Private method for re-gathering (repairing) the FQFN
434          *
435          * @param               $fqfn           The current FQFN we shall validate
436          * @param               $uniqueID               The unique ID number
437          * @return      $fqfn           The repaired FQFN when it is empty
438          * @throws      NoArrayCreatedException         If explode() has not
439          *                                                                      created an array
440          * @throws      InvalidArrayCountException      If the array count is not
441          *                                                                      as the expected
442          */
443         private function repairFQFN ($fqfn, $uniqueID) {
444                 // Cast both strings
445                 $fqfn     = (string) $fqfn;
446                 $uniqueID = (string) $uniqueID;
447
448                 // Is there pre-cached data available?
449                 if (empty($fqfn)) {
450                         // Split the unique ID up in path and file name
451                         $pathFile = explode("@", $uniqueID);
452
453                         // Are there two elements? Index 0 is the path, 1 the file name + global extension
454                         if (!is_array($pathFile)) {
455                                 // No array found
456                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
457                         } elseif (count($pathFile) != 2) {
458                                 // Invalid ID returned!
459                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
460                         }
461
462                         // Create full path name
463                         $pathName = $this->getSavePath() . $pathFile[0];
464
465                         // Nothing cached, so let's create a FQFN first
466                         $fqfn = sprintf("%s/%s.%s", $pathName, $pathFile[1], $this->getFileExtension());
467                         $this->setLastFile($fqfn);
468                 }
469
470                 // Return repaired FQFN
471                 return $fqfn;
472         }
473
474         /**
475          * Private method for re-gathering the contents of a given file
476          *
477          * @param               $contents               The (maybe) already cached contents as an array
478          * @param               $fqfn           The current FQFN we shall validate
479          * @return      $contents               The repaired contents from the given file
480          */
481         private function repairContents ($contents, $fqfn) {
482                 // Is there some content and header (2 indexes) in?
483                 if ((!is_array($contents)) || (count($contents) != 2) || (!isset($contents['header'])) || (!isset($contents['data']))) {
484                         // No content found so load the file again
485                         $contents = $inputHandler->loadFileContents($fqfn);
486
487                         // And remember all data for later usage
488                         $this->setLastContents($contents);
489                 }
490
491                 // Return the repaired contents
492                 return $contents;
493         }
494
495         /* DUMMY */ public final function loadObject () {}
496 }
497
498 // [EOF]
499 ?>