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