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