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