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