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