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