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