Fake class names added, insertDataSet() stub added, dataset criteria added
[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          */
157         public final function saveObject (FrameworkInterface $object) {
158                 // Get a string containing the serialized object. We cannot exchange
159                 // $this and $object here because $object does not need to worry
160                 // about it's limitations... ;-)
161                 $serialized = $this->serializeObject($object);
162
163                 // Get a path name plus file name and append the extension
164                 $fqfn = $this->getSavePath() . $object->getPathFileNameFromObject() . "." . $this->getFileExtension();
165
166                 // Save the file to disc we don't care here if the path is there,
167                 // this must be done in later methods.
168                 $this->getFileIoInstance()->saveFile($fqfn, array($this->getCompressorChannel()->getCompressorExtension(), $serialized));
169         }
170
171         /**
172          * Get a serialized string from the given object
173          *
174          * @param       $object                 The object we want to serialize and transparently
175          *                                                      compress
176          * @return      $serialized             A string containing the serialzed/compressed object
177          * @see         ObjectLimits    An object holding limition information
178          * @see         SerializationContainer  A special container class for e.g.
179          *                                                                      attributes from limited objects
180          */
181         private function serializeObject (FrameworkInterface $object) {
182                 // If there is no limiter instance we serialize the whole object
183                 // otherwise only in the limiter object (ObjectLimits) specified
184                 // attributes summarized in a special container class
185                 if ($this->getLimitInstance() === null) {
186                         // Serialize the whole object. This tribble call is the reason
187                         // why we need a fall-back implementation in CompressorChannel
188                         // of the methods compressStream() and decompressStream().
189                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($object));
190                 } else {
191                         // Serialize only given attributes in a special container
192                         $container = SerializationContainer::createSerializationContainer($this->getLimitInstance(), $object);
193
194                         // Serialize the container
195                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($container));
196                 }
197
198                 // Return the serialized object string
199                 return $serialized;
200         }
201
202         /**
203          * Analyses if a unique ID has already been used or not by search in the
204          * local database folder.
205          *
206          * @param       $uniqueID               A unique ID number which shall be checked
207          *                                                      before it will be used
208          * @param       $inConstructor  If we got called in a de/con-structor or
209          *                                                      from somewhere else
210          * @return      $isUnused               true    = The unique ID was not found in the database,
211          *                                                      false = It is already in use by an other object
212          * @throws      NoArrayCreatedException         If explode() fails to create an array
213          * @throws      InvalidArrayCountException      If the array contains less or
214          *                                                                      more than two elements
215          */
216         public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
217                 // Currently not used... ;-)
218                 $isUsed = false;
219
220                 // Split the unique ID up in path and file name
221                 $pathFile = explode("@", $uniqueID);
222
223                 // Are there two elements? Index 0 is the path, 1 the file name + global extension
224                 if (!is_array($pathFile)) {
225                         // No array found
226                         if ($inConstructor) {
227                                 return false;
228                         } else {
229                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
230                         }
231                 } elseif (count($pathFile) != 2) {
232                         // Invalid ID returned!
233                         if ($inConstructor) {
234                                 return false;
235                         } else {
236                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
237                         }
238                 }
239
240                 // Create full path name
241                 $pathName = $this->getSavePath() . $pathFile[0];
242
243                 // Check if the file is there with a file handler
244                 if ($inConstructor) {
245                         // No exceptions in constructors and destructors!
246                         $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
247
248                         // Has an object being created?
249                         if (!is_object($dirInstance)) return false;
250                 } else {
251                         // Outside a constructor
252                         try {
253                                 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
254                         } catch (PathIsNoDirectoryException $e) {
255                                 // Okay, path not found
256                                 return false;
257                         }
258                 }
259
260                 // Initialize the search loop
261                 $isValid = false;
262                 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", "..", ".htaccess"))) {
263                         // Generate FQFN for testing
264                         $fqfn = sprintf("%s/%s", $pathName, $dataFile);
265                         $this->setLastFile($fqfn);
266
267                         // Get instance for file handler
268                         $inputHandler = $this->getFileIoInstance();
269
270                         // Try to read from it. This makes it sure that the file is
271                         // readable and a valid database file
272                         $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
273
274                         // Extract filename (= unique ID) from it
275                         $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
276
277                         // Is this the required unique ID?
278                         if ($ID == $pathFile[1]) {
279                                 // Okay, already in use!
280                                 $isUsed = true;
281                         }
282                 }
283
284                 // Close the directory handler
285                 $dirInstance->closeDirectory();
286
287                 // Now the same for the file...
288                 return $isUsed;
289         }
290
291         /**
292          * Setter for the last read file
293          *
294          * @param               $fqfn   The FQFN of the last read file
295          * @return      void
296          */
297         private final function setLastFile ($fqfn) {
298                 // Cast string
299                 $fqfn = (string) $fqfn;
300                 $this->lastFile = $fqfn;
301         }
302
303         /**
304          * Reset the last error and exception instance. This should be done after
305          * a successfull "query"
306          *
307          * @return      void
308          */
309         private final function resetLastError () {
310                 $this->lastError = "";
311                 $this->lastException = null;
312         }
313
314         /**
315          * Getter for last read file
316          *
317          * @return      $lastFile               The last read file's name with full path
318          */
319         public final function getLastFile () {
320                 return $this->lastFile;
321         }
322
323         /**
324          * Setter for contents of the last read file
325          *
326          * @param               $contents               An array with header and data elements
327          * @return      void
328          */
329         private final function setLastFileContents ($contents) {
330                 // Cast array
331                 $contents = (array) $contents;
332                 $this->lastContents = $contents;
333         }
334
335         /**
336          * Getter for last read file's content as an array
337          *
338          * @return      $lastContent    The array with elements 'header' and 'data'.
339          */
340         public final function getLastContents () {
341                 return $this->lastContents;
342         }
343
344         /**
345          * Getter for file extension
346          *
347          * @return      $fileExtension  The array with elements 'header' and 'data'.
348          */
349         public final function getFileExtension () {
350                 return $this->fileExtension;
351         }
352
353         /**
354          * Get cached (last fetched) data from the local file database
355          *
356          * @param       $uniqueID       The ID number for looking up the data
357          * @return      $object         The restored object from the maybe compressed
358          *                                              serialized data
359          * @throws      MismatchingCompressorsException         If the compressor from
360          *                                                                      the loaded file
361          *                                                                      mismatches with the
362          *                                                                      current used one.
363          * @throws      NullPointerException    If the restored object
364          *                                                                      is null
365          * @throws      NoObjectException               If the restored "object"
366          *                                                                      is not an object instance
367          * @throws      MissingMethodException  If the required method
368          *                                                                      toString() is missing
369          */
370         public final function getObjectFromCachedData ($uniqueID) {
371                 // Get instance for file handler
372                 $inputHandler = $this->getFileIoInstance();
373
374                 // Get last file's name and contents
375                 $fqfn = $this->repairFQFN($this->getLastFile(), $uniqueID);
376                 $contents = $this->repairContents($this->getLastContents(), $fqfn);
377
378                 // Let's decompress it. First we need the instance
379                 $compressInstance = $this->getCompressorChannel();
380
381                 // Is the compressor's extension the same as the one from the data?
382                 if ($compressInstance->getCompressorExtension() != $contents['header'][0]) {
383                         /**
384                          * @todo        For now we abort here but later we need to make this a little more dynamic.
385                          */
386                         throw new MismatchingCompressorsException(array($this, $contents['header'][0], $fqfn, $compressInstance->getCompressorExtension()), self::EXCEPTION_MISMATCHING_COMPRESSORS);
387                 }
388
389                 // Decompress the data now
390                 $serialized = $compressInstance->getCompressor()->decompressStream($contents['data']);
391
392                 // And unserialize it...
393                 $object = unserialize($serialized);
394
395                 // This must become a valid object, so let's check it...
396                 if (is_null($object)) {
397                         // Is null, throw exception
398                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
399                 } elseif (!is_object($object)) {
400                         // Is not an object, throw exception
401                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
402                 } elseif (!$object instanceof FrameworkInterface) {
403                         // A highly required method was not found... :-(
404                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
405                 }
406
407                 // And return the object
408                 return $object;
409         }
410
411         /**
412          * Private method for re-gathering (repairing) the FQFN
413          *
414          * @param               $fqfn           The current FQFN we shall validate
415          * @param               $uniqueID               The unique ID number
416          * @return      $fqfn           The repaired FQFN when it is empty
417          * @throws      NoArrayCreatedException         If explode() has not
418          *                                                                      created an array
419          * @throws      InvalidArrayCountException      If the array count is not
420          *                                                                      as the expected
421          */
422         private function repairFQFN ($fqfn, $uniqueID) {
423                 // Cast both strings
424                 $fqfn     = (string) $fqfn;
425                 $uniqueID = (string) $uniqueID;
426
427                 // Is there pre-cached data available?
428                 if (empty($fqfn)) {
429                         // Split the unique ID up in path and file name
430                         $pathFile = explode("@", $uniqueID);
431
432                         // Are there two elements? Index 0 is the path, 1 the file name + global extension
433                         if (!is_array($pathFile)) {
434                                 // No array found
435                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
436                         } elseif (count($pathFile) != 2) {
437                                 // Invalid ID returned!
438                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
439                         }
440
441                         // Create full path name
442                         $pathName = $this->getSavePath() . $pathFile[0];
443
444                         // Nothing cached, so let's create a FQFN first
445                         $fqfn = sprintf("%s/%s.%s", $pathName, $pathFile[1], $this->getFileExtension());
446                         $this->setLastFile($fqfn);
447                 }
448
449                 // Return repaired FQFN
450                 return $fqfn;
451         }
452
453         /**
454          * Private method for re-gathering the contents of a given file
455          *
456          * @param               $contents               The (maybe) already cached contents as an array
457          * @param               $fqfn           The current FQFN we shall validate
458          * @return      $contents               The repaired contents from the given file
459          */
460         private function repairContents ($contents, $fqfn) {
461                 // Is there some content and header (2 indexes) in?
462                 if ((!is_array($contents)) || (count($contents) != 2) || (!isset($contents['header'])) || (!isset($contents['data']))) {
463                         // No content found so load the file again
464                         $contents = $inputHandler->loadFileContents($fqfn);
465
466                         // And remember all data for later usage
467                         $this->setLastContents($contents);
468                 }
469
470                 // Return the repaired contents
471                 return $contents;
472         }
473
474         /**
475          * Makes sure that the database connection is alive
476          *
477          * @return      void
478          */
479         public function connectToDatabase () {
480                 // @TODO Do some checks on the database directory and files here
481         }
482
483         /**
484          * Loads data saved with saveObject from the database and re-creates a
485          * full object from it.
486          * If limitObject() was called before a new object ObjectContainer with
487          * all requested attributes will be returned instead.
488          *
489          * @return      Object  The fully re-created object or instance to
490          *                                      ObjectContainer
491          * @throws      SavePathIsEmptyException                If the given save path is an
492          *                                                                                      empty string
493          * @throws      SavePathIsNoDirectoryException  If the save path is no
494          *                                                                                      path (e.g. a file)
495          * @throws      SavePathReadProtectedException  If the save path is read-
496          *                                                                                      protected
497          * @throws      SavePathWriteProtectedException If the save path is write-
498          *                                                                                      protected
499          */
500         public function loadObject () {
501                 // Already connected? Then abort here
502                 if ($this->alreadyConnected === true) return true;
503
504                 // Get the save path
505                 $savePath = $this->getSavePath();
506
507                 if (empty($savePath)) {
508                         // Empty string
509                         throw new SavePathIsEmptyException($dbInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
510                 } elseif (!is_dir($savePath)) {
511                         // Is not a dir
512                         throw new SavePathIsNoDirectoryException($savePath, self::EXCEPTION_INVALID_PATH_NAME);
513                 } elseif (!is_readable($savePath)) {
514                         // Path not readable
515                         throw new SavePathReadProtectedException($savePath, self::EXCEPTION_READ_PROTECED_PATH);
516                 } elseif (!is_writeable($savePath)) {
517                         // Path not writeable
518                         throw new SavePathWriteProtectedException($savePath, self::EXCEPTION_WRITE_PROTECED_PATH);
519                 }
520
521                 // "Connection" established... ;-)
522                 $this->alreadyConnected = true;
523         }
524
525         /**
526          * Starts a SELECT query on the database by given return type, table name
527          * and search criteria
528          *
529          * @param       $resultType             Result type ("array", "object" and "indexed" are valid)
530          * @param       $tableName              Name of the database table
531          * @param       $criteria               Local search criteria class
532          * @return      $resultData             Result data of the query
533          * @throws      UnsupportedCriteriaException    If the criteria is unsupported
534          * @throws      SqlException                                    If an "SQL error" occurs
535          */
536         public function querySelect ($resultType, $tableName, Criteria $criteriaInstance) {
537                 // The result is null by any errors
538                 $resultData = null;
539
540                 // Is this criteria supported?
541                 if (!$criteriaInstance instanceof LocalSearchCriteria) {
542                         // Not supported by this database layer
543                         throw new UnsupportedCriteriaException(array($this, $criteriaInstance), self::EXCEPTION_REQUIRED_INTERFACE_MISSING);
544                 }
545
546                 // Create full path name
547                 $pathName = $this->getSavePath() . $tableName . '/';
548
549                 // A "select" query is not that easy on local files, so first try to
550                 // find the "table" which is in fact a directory on the server
551                 try {
552                         // Get a directory pointer instance
553                         $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
554
555                         // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read
556                         $resultData = array(
557                                 'status'        => "ok",
558                                 'rows'          => array()
559                         );
560
561                         // Read the directory with some exceptions
562                         while ($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess"))) {
563                                 $this->partialStub(sprintf("File %s found.", $dataFile));
564                         } // END - while
565
566                         // Close directory and throw the instance away
567                         $directoryInstance->closeDirectory();
568                         unset($directoryInstance);
569
570                         // Reset last error message and exception
571                         $this->resetLastError();
572                 } catch (PathIsNoDirectoryException $e) {
573                         // Path not found means "table not found" for real databases...
574                         $this->lastException = $e;
575                         $this->lastError = $e->getMessage();
576
577                         // So throw an SqlException here with faked error message
578                         throw new SqlException (array($this, sprintf("Table &#39;%s&#39; not found", $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY);
579                 } catch (FrameworkException $e) {
580                         // Catch all exceptions and store them in last error
581                         $this->lastException = $e;
582                         $this->lastError = $e->getMessage();
583                 }
584
585                 // Return the gathered result
586                 return $resultData;
587         }
588
589         /**
590          * "Inserts" a data set instance into a local file database folder
591          *
592          * @param       $dataSetInstance        A storeable data set
593          * @return      void
594          */
595         public function insertDataSet (StoreableCriteria $dataSetInstance) {
596                 // Create full path name
597                 $fqfn = sprintf("%s%s/%s.%s",
598                         $this->getSavePath(),
599                         $dataSetInstance->getTableName(),
600                         $dataSetInstance->getCombinedUniqueKey(),
601                         $this->getFileExtension()
602                 );
603                 die($fqfn);
604
605                 // Try to save the request away
606                 try {
607                         // Get a file pointer instance
608
609                         // Reset last error message and exception
610                         $this->resetLastError();
611                 } catch (FrameworkException $e) {
612                         // Catch all exceptions and store them in last error
613                         $this->lastException = $e;
614                         $this->lastError = $e->getMessage();
615                 }
616         }
617 }
618
619 // [EOF]
620 ?>