Registry introduces, comments fixed ;)
[shipsimu.git] / inc / classes / main / database / databases / 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 <webmaster@ship-simu.org>
8  * @version             0.0.0
9  * @copyright   Copyright(c) 2007, 2008 Roland Haeder, this is free software
10  * @license             GNU GPL 3.0 or any newer version
11  * @link                http://www.ship-simu.org
12  *
13  * This program is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 3 of the License, or
16  * (at your option) any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with this program. If not, see <http://www.gnu.org/licenses/>.
25  */
26 class LocalFileDatabase extends BaseDatabaseFrontend implements DatabaseFrontendInterface {
27         // Constants for MySQL backward-compatiblity (PLEASE FIX THEM!)
28         const DB_CODE_TABLE_MISSING = 0x000;
29
30         /**
31          * Save path for "file database"
32          */
33         private $savePath = "";
34
35         /**
36          * The file's extension
37          */
38         private $fileExtension = "serialized";
39
40         /**
41          * The last read file's name
42          */
43         private $lastFile = "";
44
45         /**
46          * The last read file's content including header information
47          */
48         private $lastContents = array();
49
50         /**
51          * Wether the "connection is already up
52          */
53         private $alreadyConnected = false;
54
55         /**
56          * Last error message
57          */
58         private $lastError = "";
59
60         /**
61          * Last exception
62          */
63         private $lastException = null;
64
65         /**
66          * The protected constructor. Do never instance from outside! You need to
67          * set a local file path. The class will then validate it.
68          *
69          * @return      void
70          */
71         protected function __construct() {
72                 // Call parent constructor
73                 parent::__construct(__CLASS__);
74
75                 // Set description
76                 $this->setObjectDescription("Class for local file databases");
77
78                 // Create unique ID
79                 $this->createUniqueID();
80
81                 // Clean up a little
82                 $this->removeSystemArray();
83         }
84
85         /**
86          * Create an object of LocalFileDatabase and set the save path for local files.
87          * This method also validates the given file path.
88          *
89          * @param               $savePath                                       The local file path string
90          * @param               $ioInstance                             The input/output handler. This
91          *                                                                      should be FileIoHandler
92          * @return      $dbInstance                             An instance of LocalFileDatabase
93          */
94         public final static function createLocalFileDatabase ($savePath, FileIoHandler $ioInstance) {
95                 // Get an instance
96                 $dbInstance = new LocalFileDatabase();
97
98                 // Set save path and IO instance
99                 $dbInstance->setSavePath($savePath);
100                 $dbInstance->setFileIoInstance($ioInstance);
101
102                 // "Connect" to the database
103                 $dbInstance->connectToDatabase();
104
105                 // Return database instance
106                 return $dbInstance;
107         }
108
109         /**
110          * Setter for save path
111          *
112          * @param               $savePath               The local save path where we shall put our serialized classes
113          * @return      void
114          */
115         public final function setSavePath ($savePath) {
116                 // Secure string
117                 $savePath = (string) $savePath;
118
119                 // Set save path
120                 $this->savePath = $savePath;
121         }
122
123         /**
124          * Getter for save path
125          *
126          * @return      $savePath               The local save path where we shall put our serialized classes
127          */
128         public final function getSavePath () {
129                 return $this->savePath;
130         }
131
132         /**
133          * Getter for last error message
134          *
135          * @return      $lastError      Last error message
136          */
137         public final function getLastError () {
138                 return $this->lastError;
139         }
140
141         /**
142          * Getter for last exception
143          *
144          * @return      $lastException  Last thrown exception
145          */
146         public final function getLastException () {
147                 return $this->lastException;
148         }
149
150         /**
151          * Saves a given object to the local file system by serializing and
152          * transparently compressing it
153          *
154          * @param               $object                         The object we shall save to the local file system
155          * @return      void
156          * @throws      NullPointerException    If the object instance is null
157          * @throws      NoObjectException               If the parameter $object is not
158          *                                                              an object
159          */
160         public final function saveObject ($object) {
161                 // Some tests on the parameter...
162                 if (is_null($object)) {
163                         // Is null, throw exception
164                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
165                 } elseif (!is_object($object)) {
166                         // Is not an object, throw exception
167                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
168                 } elseif (!method_exists($object, '__toString')) {
169                         // A highly required method was not found... :-(
170                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
171                 }
172
173                 // Get a string containing the serialized object. We cannot exchange
174                 // $this and $object here because $object does not need to worry
175                 // about it's limitations... ;-)
176                 $serialized = $this->serializeObject($object);
177
178                 // Get a path name plus file name and append the extension
179                 $fqfn = $this->getSavePath() . $object->getPathFileNameFromObject() . "." . $this->getFileExtension();
180
181                 // Save the file to disc we don't care here if the path is there,
182                 // this must be done in later methods.
183                 $this->getFileIoInstance()->saveFile($fqfn, array($this->getCompressorChannel()->getCompressorExtension(), $serialized));
184         }
185
186         /**
187          * Get a serialized string from the given object
188          *
189          * @param               $object         The object we want to serialize and transparently
190          *                                              compress
191          * @return      $serialized     A string containing the serialzed/compressed object
192          * @see         ObjectLimits    An object holding limition information
193          * @see         SerializationContainer  A special container class for e.g.
194          *                                                              attributes from limited objects
195          */
196         private function serializeObject ($object) {
197                 // If there is no limiter instance we serialize the whole object
198                 // otherwise only in the limiter object (ObjectLimits) specified
199                 // attributes summarized in a special container class
200                 if ($this->getLimitInstance() === null) {
201                         // Serialize the whole object. This tribble call is the reason
202                         // why we need a fall-back implementation in CompressorChannel
203                         // of the methods compressStream() and decompressStream().
204                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($object));
205                 } else {
206                         // Serialize only given attributes in a special container
207                         $container = SerializationContainer::createSerializationContainer($this->getLimitInstance(), $object);
208
209                         // Serialize the container
210                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($container));
211                 }
212
213                 // Return the serialized object string
214                 return $serialized;
215         }
216
217         /**
218          * Analyses if a unique ID has already been used or not by search in the
219          * local database folder.
220          *
221          * @param               $uniqueID               A unique ID number which shall be checked
222          *                                              before it will be used
223          * @param               $inConstructor  If we got called in a de/con-structor or
224          *                                              from somewhere else
225          * @return      $isUnused               true    = The unique ID was not found in the database,
226          *                                              false = It is already in use by an other object
227          * @throws      NoArrayCreatedException If explode() fails to create an array
228          * @throws      InvalidArrayCountException      If the array contains less or
229          *                                                                      more than two elements
230          */
231         public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
232                 // Currently not used... ;-)
233                 $isUsed = false;
234
235                 // Split the unique ID up in path and file name
236                 $pathFile = explode("@", $uniqueID);
237
238                 // Are there two elements? Index 0 is the path, 1 the file name + global extension
239                 if (!is_array($pathFile)) {
240                         // No array found
241                         if ($inConstructor) {
242                                 return false;
243                         } else {
244                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
245                         }
246                 } elseif (count($pathFile) != 2) {
247                         // Invalid ID returned!
248                         if ($inConstructor) {
249                                 return false;
250                         } else {
251                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
252                         }
253                 }
254
255                 // Create full path name
256                 $pathName = $this->getSavePath() . $pathFile[0];
257
258                 // Check if the file is there with a file handler
259                 if ($inConstructor) {
260                         // No exceptions in constructors and destructors!
261                         $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
262
263                         // Has an object being created?
264                         if (!is_object($dirInstance)) return false;
265                 } else {
266                         // Outside a constructor
267                         try {
268                                 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
269                         } catch (PathIsNoDirectoryException $e) {
270                                 // Okay, path not found
271                                 return false;
272                         }
273                 }
274
275                 // Initialize the search loop
276                 $isValid = false;
277                 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", ".."))) {
278                         // Generate FQFN for testing
279                         $fqfn = sprintf("%s/%s", $pathName, $dataFile);
280                         $this->setLastFile($fqfn);
281
282                         // Get instance for file handler
283                         $inputHandler = $this->getFileIoInstance();
284
285                         // Try to read from it. This makes it sure that the file is
286                         // readable and a valid database file
287                         $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
288
289                         // Extract filename (= unique ID) from it
290                         $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
291
292                         // Is this the required unique ID?
293                         if ($ID == $pathFile[1]) {
294                                 // Okay, already in use!
295                                 $isUsed = true;
296                         }
297                 }
298
299                 // Close the directory handler
300                 $dirInstance->closeDirectory();
301
302                 // Now the same for the file...
303                 return $isUsed;
304         }
305
306         /**
307          * Setter for the last read file
308          *
309          * @param               $fqfn   The FQFN of the last read file
310          * @return      void
311          */
312         private final function setLastFile ($fqfn) {
313                 // Cast string
314                 $fqfn = (string) $fqfn;
315                 $this->lastFile = $fqfn;
316         }
317
318         /**
319          * Reset the last error and exception instance. This should be done after
320          * a successfull "query"
321          *
322          * @return      void
323          */
324         private final function resetLastError () {
325                 $this->lastError = "";
326                 $this->lastException = null;
327         }
328
329         /**
330          * Getter for last read file
331          *
332          * @return      $lastFile               The last read file's name with full path
333          */
334         public final function getLastFile () {
335                 return $this->lastFile;
336         }
337
338         /**
339          * Setter for contents of the last read file
340          *
341          * @param               $contents               An array with header and data elements
342          * @return      void
343          */
344         private final function setLastFileContents ($contents) {
345                 // Cast array
346                 $contents = (array) $contents;
347                 $this->lastContents = $contents;
348         }
349
350         /**
351          * Getter for last read file's content as an array
352          *
353          * @return      $lastContent    The array with elements 'header' and 'data'.
354          */
355         public final function getLastContents () {
356                 return $this->lastContents;
357         }
358
359         /**
360          * Getter for file extension
361          *
362          * @return      $fileExtension  The array with elements 'header' and 'data'.
363          */
364         public final function getFileExtension () {
365                 return $this->fileExtension;
366         }
367
368         /**
369          * Get cached (last fetched) data from the local file database
370          *
371          * @param       $uniqueID       The ID number for looking up the data
372          * @return      $object         The restored object from the maybe compressed
373          *                                              serialized data
374          * @throws      MismatchingCompressorsException         If the compressor from
375          *                                                                      the loaded file
376          *                                                                      mismatches with the
377          *                                                                      current used one.
378          * @throws      NullPointerException    If the restored object
379          *                                                                      is null
380          * @throws      NoObjectException               If the restored "object"
381          *                                                                      is not an object instance
382          * @throws      MissingMethodException  If the required method
383          *                                                                      toString() is missing
384          */
385         public final function getObjectFromCachedData ($uniqueID) {
386                 // Get instance for file handler
387                 $inputHandler = $this->getFileIoInstance();
388
389                 // Get last file's name and contents
390                 $fqfn = $this->repairFQFN($this->getLastFile(), $uniqueID);
391                 $contents = $this->repairContents($this->getLastContents(), $fqfn);
392
393                 // Let's decompress it. First we need the instance
394                 $compressInstance = $this->getCompressorChannel();
395
396                 // Is the compressor's extension the same as the one from the data?
397                 if ($compressInstance->getCompressorExtension() != $contents['header'][0]) {
398                         /**
399                          * @todo        For now we abort here but later we need to make this a little more dynamic.
400                          */
401                         throw new MismatchingCompressorsException(array($this, $contents['header'][0], $fqfn, $compressInstance->getCompressorExtension()), self::EXCEPTION_MISMATCHING_COMPRESSORS);
402                 }
403
404                 // Decompress the data now
405                 $serialized = $compressInstance->getCompressor()->decompressStream($contents['data']);
406
407                 // And unserialize it...
408                 $object = unserialize($serialized);
409
410                 // This must become a valid object, so let's check it...
411                 if (is_null($object)) {
412                         // Is null, throw exception
413                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
414                 } elseif (!is_object($object)) {
415                         // Is not an object, throw exception
416                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
417                 } elseif (!$object instanceof FrameworkInterface) {
418                         // A highly required method was not found... :-(
419                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
420                 }
421
422                 // And return the object
423                 return $object;
424         }
425
426         /**
427          * Private method for re-gathering (repairing) the FQFN
428          *
429          * @param               $fqfn           The current FQFN we shall validate
430          * @param               $uniqueID               The unique ID number
431          * @return      $fqfn           The repaired FQFN when it is empty
432          * @throws      NoArrayCreatedException         If explode() has not
433          *                                                                      created an array
434          * @throws      InvalidArrayCountException      If the array count is not
435          *                                                                      as the expected
436          */
437         private function repairFQFN ($fqfn, $uniqueID) {
438                 // Cast both strings
439                 $fqfn     = (string) $fqfn;
440                 $uniqueID = (string) $uniqueID;
441
442                 // Is there pre-cached data available?
443                 if (empty($fqfn)) {
444                         // Split the unique ID up in path and file name
445                         $pathFile = explode("@", $uniqueID);
446
447                         // Are there two elements? Index 0 is the path, 1 the file name + global extension
448                         if (!is_array($pathFile)) {
449                                 // No array found
450                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
451                         } elseif (count($pathFile) != 2) {
452                                 // Invalid ID returned!
453                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
454                         }
455
456                         // Create full path name
457                         $pathName = $this->getSavePath() . $pathFile[0];
458
459                         // Nothing cached, so let's create a FQFN first
460                         $fqfn = sprintf("%s/%s.%s", $pathName, $pathFile[1], $this->getFileExtension());
461                         $this->setLastFile($fqfn);
462                 }
463
464                 // Return repaired FQFN
465                 return $fqfn;
466         }
467
468         /**
469          * Private method for re-gathering the contents of a given file
470          *
471          * @param               $contents               The (maybe) already cached contents as an array
472          * @param               $fqfn           The current FQFN we shall validate
473          * @return      $contents               The repaired contents from the given file
474          */
475         private function repairContents ($contents, $fqfn) {
476                 // Is there some content and header (2 indexes) in?
477                 if ((!is_array($contents)) || (count($contents) != 2) || (!isset($contents['header'])) || (!isset($contents['data']))) {
478                         // No content found so load the file again
479                         $contents = $inputHandler->loadFileContents($fqfn);
480
481                         // And remember all data for later usage
482                         $this->setLastContents($contents);
483                 }
484
485                 // Return the repaired contents
486                 return $contents;
487         }
488
489         /**
490          * Makes sure that the database connection is alive
491          *
492          * @return      void
493          */
494         public function connectToDatabase () {
495                 // @TODO Do some checks on the database directory and files here
496         }
497
498         /**
499          * Loads data saved with saveObject from the database and re-creates a
500          * full object from it.
501          * If limitObject() was called before a new object ObjectContainer with
502          * all requested attributes will be returned instead.
503          *
504          * @return      Object  The fully re-created object or instance to
505          *                                      ObjectContainer
506          * @throws      SavePathIsEmptyException                If the given save path is an
507          *                                                                                      empty string
508          * @throws      SavePathIsNoDirectoryException  If the save path is no
509          *                                                                                      path (e.g. a file)
510          * @throws      SavePathReadProtectedException  If the save path is read-
511          *                                                                                      protected
512          * @throws      SavePathWriteProtectedException If the save path is write-
513          *                                                                                      protected
514          */
515         public function loadObject () {
516                 // Already connected? Then abort here
517                 if ($this->alreadyConnected === true) return true;
518
519                 // Get the save path
520                 $savePath = $this->getSavePath();
521
522                 if (empty($savePath)) {
523                         // Empty string
524                         throw new SavePathIsEmptyException($dbInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
525                 } elseif (!is_dir($savePath)) {
526                         // Is not a dir
527                         throw new SavePathIsNoDirectoryException($savePath, self::EXCEPTION_INVALID_PATH_NAME);
528                 } elseif (!is_readable($savePath)) {
529                         // Path not readable
530                         throw new SavePathReadProtectedException($savePath, self::EXCEPTION_READ_PROTECED_PATH);
531                 } elseif (!is_writeable($savePath)) {
532                         // Path not writeable
533                         throw new SavePathWriteProtectedException($savePath, self::EXCEPTION_WRITE_PROTECED_PATH);
534                 }
535
536                 // "Connection" established... ;-)
537                 $this->alreadyConnected = true;
538         }
539
540         /**
541          * Starts a SELECT query on the database by given return type, table name
542          * and search criteria
543          *
544          * @param       $resultType             Result type ("array", "object" and "indexed" are valid)
545          * @param       $tableName              Name of the database table
546          * @param       $criteria               Local search criteria class
547          * @return      $resultData             Result data of the query
548          * @throws      UnsupportedCriteriaException    If the criteria is unsupported
549          * @throws      SqlException                                    If an "SQL error" occurs
550          */
551         public function querySelect ($resultType, $tableName, Criteria $criteriaInstance) {
552                 // The result is null by any errors
553                 $resultData = null;
554
555                 // Is this criteria supported?
556                 if (!$criteriaInstance instanceof LocalCriteria) {
557                         // Not supported by this database layer
558                         throw new UnsupportedCriteriaException(array($this, $criteriaInstance), self::EXCEPTION_REQUIRED_INTERFACE_MISSING);
559                 }
560
561                 // Create full path name
562                 $pathName = $this->getSavePath() . $tableName . '/';
563
564                 // A "select" query is not that easy on local files, so first try to
565                 // find the "table" which is in fact a directory on the server
566                 try {
567                         // Get a directory pointer instance
568                         $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
569
570                         // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read
571                         $resultData = array(
572                                 'status'        => "ok",
573                                 'rows'          => array()
574                         );
575
576                         // Read the directory with some exceptions
577                         while ($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess"))) {
578                                 $this->partialStub(sprintf("File %s found.", $dataFile));
579                         } // END - while
580
581                         // Close directory and throw the instance away
582                         $directoryInstance->closeDirectory();
583                         unset($directoryInstance);
584
585                         // Reset last error message and exception
586                         $this->resetLastError();
587                 } catch (PathIsNoDirectoryException $e) {
588                         // Path not found means "table not found" for real databases...
589                         $this->lastException = $e;
590                         $this->lastError = $e->getMessage();
591
592                         // So throw an SqlException here with faked error message
593                         throw new SqlException (array($this, sprintf("Table &#39;%s&#39; not found", $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY);
594                 } catch (FrameworkException $e) {
595                         // Catch all exceptions and store them in last error
596                         $this->lastException = $e;
597                         $this->lastError = $e->getMessage();
598                 }
599
600                 // Return the gathered result
601                 return $resultData;
602         }
603 }
604
605 // [EOF]
606 ?>