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