3 * Database backend class for storing objects in locally created files.
5 * This class serializes objects and saves them to local files.
7 * @author Roland Haeder <webmaster@ship-simu.org>
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
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.
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.
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/>.
26 class LocalFileDatabase extends BaseDatabaseFrontend implements DatabaseFrontendInterface {
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;
34 * Save path for "file database"
36 private $savePath = "";
39 * The file's extension
41 private $fileExtension = "serialized";
44 * The last read file's name
46 private $lastFile = "";
49 * The last read file's content including header information
51 private $lastContents = array();
54 * Wether the "connection is already up
56 private $alreadyConnected = false;
61 private $lastError = "";
66 private $lastException = null;
69 * Table information array
71 private $tableInfo = array();
74 * The protected constructor. Do never instance from outside! You need to
75 * set a local file path. The class will then validate it.
79 protected function __construct() {
80 // Call parent constructor
81 parent::__construct(__CLASS__);
84 $this->removeNumberFormaters();
85 $this->removeSystemArray();
89 * Create an object of LocalFileDatabase and set the save path for local files.
90 * This method also validates the given file path.
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
97 public final static function createLocalFileDatabase ($savePath, FileIoHandler $ioInstance) {
99 $dbInstance = new LocalFileDatabase();
101 // Set save path and IO instance
102 $dbInstance->setSavePath($savePath);
103 $dbInstance->setFileIoInstance($ioInstance);
105 // "Connect" to the database
106 $dbInstance->connectToDatabase();
108 // Return database instance
113 * Setter for save path
115 * @param $savePath The local save path where we shall put our serialized classes
118 public final function setSavePath ($savePath) {
120 $savePath = (string) $savePath;
123 $this->savePath = $savePath;
127 * Getter for save path
129 * @return $savePath The local save path where we shall put our serialized classes
131 public final function getSavePath () {
132 return $this->savePath;
136 * Getter for last error message
138 * @return $lastError Last error message
140 public final function getLastError () {
141 return $this->lastError;
145 * Getter for last exception
147 * @return $lastException Last thrown exception
149 public final function getLastException () {
150 return $this->lastException;
154 * Analyses if a unique ID has already been used or not by search in the
155 * local database folder.
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
168 public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
169 // Currently not used... ;-)
172 // Split the unique ID up in path and file name
173 $pathFile = explode("@", $uniqueID);
175 // Are there two elements? Index 0 is the path, 1 the file name + global extension
176 if (!is_array($pathFile)) {
178 if ($inConstructor) {
181 throw new NoArrayCreatedException(array($this, 'pathFile'), self::EXCEPTION_ARRAY_EXPECTED);
183 } elseif (count($pathFile) != 2) {
184 // Invalid ID returned!
185 if ($inConstructor) {
188 throw new InvalidArrayCountException(array($this, 'pathFile', count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
192 // Create full path name
193 $pathName = $this->getSavePath() . $pathFile[0];
195 // Check if the file is there with a file handler
196 if ($inConstructor) {
197 // No exceptions in constructors and destructors!
198 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
200 // Has an object being created?
201 if (!is_object($dirInstance)) return false;
203 // Outside a constructor
205 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
206 } catch (PathIsNoDirectoryException $e) {
207 // Okay, path not found
212 // Initialize the search loop
214 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", "..", ".htaccess", ".svn", "info." . $this->getFileExtension()))) {
215 // Generate FQFN for testing
216 $fqfn = sprintf("%s/%s", $pathName, $dataFile);
217 $this->setLastFile($fqfn);
219 // Get instance for file handler
220 $inputHandler = $this->getFileIoInstance();
222 // Try to read from it. This makes it sure that the file is
223 // readable and a valid database file
224 $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
226 // Extract filename (= unique ID) from it
227 $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
229 // Is this the required unique ID?
230 if ($ID == $pathFile[1]) {
231 // Okay, already in use!
236 // Close the directory handler
237 $dirInstance->closeDirectory();
239 // Now the same for the file...
244 * Setter for the last read file
246 * @param $fqfn The FQFN of the last read file
249 private final function setLastFile ($fqfn) {
251 $fqfn = (string) $fqfn;
252 $this->lastFile = $fqfn;
256 * Reset the last error and exception instance. This should be done after
257 * a successfull "query"
261 private final function resetLastError () {
262 $this->lastError = "";
263 $this->lastException = null;
267 * Getter for last read file
269 * @return $lastFile The last read file's name with full path
271 public final function getLastFile () {
272 return $this->lastFile;
276 * Setter for contents of the last read file
278 * @param $contents An array with header and data elements
281 private final function setLastFileContents ($contents) {
283 $contents = (array) $contents;
284 $this->lastContents = $contents;
288 * Getter for last read file's content as an array
290 * @return $lastContent The array with elements 'header' and 'data'.
292 public final function getLastContents () {
293 return $this->lastContents;
297 * Getter for file extension
299 * @return $fileExtension The array with elements 'header' and 'data'.
301 public final function getFileExtension () {
302 return $this->fileExtension;
306 * Reads a local data file and returns it's contents in an array
308 * @param $fqfn The FQFN for the requested file
311 private function getDataArrayFromFile ($fqfn) {
312 // Get a file pointer
313 $fileInstance = FrameworkFileInputPointer::createFrameworkFileInputPointer($fqfn);
315 // Get the raw data and BASE64-decode it
316 $compressedData = base64_decode($fileInstance->readLinesFromFile());
318 // Close the file and throw the instance away
319 $fileInstance->closeFile();
320 unset($fileInstance);
323 $serializedData = $this->getCompressorChannel()->getCompressor()->decompressStream($compressedData);
326 $dataArray = unserialize($serializedData);
333 * Writes data array to local file
335 * @param $fqfn The FQFN of the local file
336 * @param $dataArray An array with all the data we shall write
339 private function writeDataArrayToFqfn ($fqfn, array $dataArray) {
340 // Get a file pointer instance
341 $fileInstance = FrameworkFileOutputPointer::createFrameworkFileOutputPointer($fqfn, 'w');
343 // Serialize and compress it
344 $compressedData = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($dataArray));
346 // Write this data BASE64 encoded to the file
347 $fileInstance->writeToFile(base64_encode($compressedData));
349 // Close the file pointer
350 $fileInstance->closeFile();
354 * Getter for table information file contents or an empty if the info file was not created
356 * @param $dataSetInstance An instance of a database set class
357 * @return $infoArray An array with all table informations
359 private function getContentsFromTableInfoFile (StoreableCriteria $dataSetInstance) {
360 // Default content is no data
361 $infoArray = array();
363 // Create FQFN for getting the table information file
364 $fqfn = $this->getSavePath() . $dataSetInstance->getTableName() . '/info.' . $this->getFileExtension();
366 // Get the file contents
368 $infoArray = $this->getDataArrayFromFile($fqfn);
369 } catch (FileNotFoundException $e) {
370 // Not found, so ignore it here
378 * Creates the table info file from given dataset instance
380 * @param $dataSetInstance An instance of a database set class
383 private function createTableInfoFile (StoreableCriteria $dataSetInstance) {
384 // Create FQFN for creating the table information file
385 $fqfn = $this->getSavePath() . $dataSetInstance->getTableName() . '/info.' . $this->getFileExtension();
387 // Get the data out from dataset in a local array
388 $this->tableInfo[$dataSetInstance->getTableName()] = array(
389 'primary' => $dataSetInstance->getPrimaryKey(),
391 'last_updated' => time()
394 // Write the data to the file
395 $this->writeDataArrayToFqfn($fqfn, $this->tableInfo[$dataSetInstance->getTableName()]);
399 * Updates the primary key information or creates the table info file if not found
401 * @param $dataSetInstance An instance of a database set class
404 private function updatePrimaryKey (StoreableCriteria $dataSetInstance) {
405 // Get the information array from lower method
406 $infoArray = $this->getContentsFromTableInfoFile($dataSetInstance);
408 // Is the primary key there?
409 if (!isset($this->tableInfo['primary'])) {
410 // Then create the info file
411 $this->createTableInfoFile($dataSetInstance);
412 } elseif (($this->getConfigInstance()->readConfig('db_update_primary_forced') === "Y") && ($dataSetInstance->getPrimaryKey() != $this->tableInfo['primary'])) {
413 // Set the array element
414 $this->tableInfo[$dataSetInstance->getTableName()]['primary'] = $dataSetInstance->getPrimaryKey();
417 $this->updateTableInfoFile($dataSetInstance);
422 * Makes sure that the database connection is alive
425 * @todo Do some checks on the database directory and files here
427 public function connectToDatabase () {
431 * Starts a SELECT query on the database by given return type, table name
432 * and search criteria
434 * @param $resultType Result type ("array", "object" and "indexed" are valid)
435 * @param $tableName Name of the database table
436 * @param $criteria Local search criteria class
437 * @return $resultData Result data of the query
438 * @throws UnsupportedCriteriaException If the criteria is unsupported
439 * @throws SqlException If an "SQL error" occurs
441 public function querySelect ($resultType, $tableName, LocalSearchCriteria $criteriaInstance) {
442 // The result is null by any errors
445 // Create full path name
446 $pathName = $this->getSavePath() . $tableName . '/';
448 // A "select" query is not that easy on local files, so first try to
449 // find the "table" which is in fact a directory on the server
451 // Get a directory pointer instance
452 $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
454 // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read
460 // Initialize limit/skip
464 // Read the directory with some exceptions
465 while (($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess", ".svn", "info." . $this->getFileExtension()))) && ($limitFound < $criteriaInstance->getLimit())) {
466 // Does the extension match?
467 if (substr($dataFile, -(strlen($this->getFileExtension()))) !== $this->getFileExtension()) {
473 $dataArray = $this->getDataArrayFromFile($pathName . $dataFile);
476 if (is_array($dataArray)) {
477 // Search in the criteria with FMFW (First Matches, First Wins)
478 foreach ($dataArray as $key=>$value) {
479 // Get criteria element
480 $criteria = $criteriaInstance->getCriteriaElemnent($key);
482 // Is the criteria met?
483 if ((!is_null($criteria)) && ($criteria == $value)) {
485 // Shall we skip this entry?
486 if ($criteriaInstance->getSkip() > 0) {
487 // We shall skip some entries
488 if ($skipFound < $criteriaInstance->getSkip()) {
496 $resultData['rows'][] = $dataArray;
502 // Throw an exception here
503 throw new SqlException(array($this, sprintf("File '%s' contains invalid data.", $dataFile), self::DB_CODE_DATA_FILE_CORRUPT), self::EXCEPTION_SQL_QUERY);
507 // Close directory and throw the instance away
508 $directoryInstance->closeDirectory();
509 unset($directoryInstance);
511 // Reset last error message and exception
512 $this->resetLastError();
513 } catch (PathIsNoDirectoryException $e) {
514 // Path not found means "table not found" for real databases...
515 $this->lastException = $e;
516 $this->lastError = $e->getMessage();
518 // So throw an SqlException here with faked error message
519 throw new SqlException (array($this, sprintf("Table '%s' not found", $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY);
520 } catch (FrameworkException $e) {
521 // Catch all exceptions and store them in last error
522 $this->lastException = $e;
523 $this->lastError = $e->getMessage();
526 // Return the gathered result
531 * "Inserts" a data set instance into a local file database folder
533 * @param $dataSetInstance A storeable data set
535 * @throws SqlException If an SQL error occurs
537 public function queryInsertDataSet (StoreableCriteria $dataSetInstance) {
538 // Create full path name
539 $fqfn = sprintf("%s%s/%s.%s",
540 $this->getSavePath(),
541 $dataSetInstance->getTableName(),
542 md5($dataSetInstance->getUniqueValue()),
543 $this->getFileExtension()
546 // Try to save the request away
548 // Write the data away
549 $this->writeDataArrayToFqfn($fqfn, $dataSetInstance->getCriteriaArray());
551 // Update the primary key
552 $this->updatePrimaryKey($dataSetInstance);
554 // Reset last error message and exception
555 $this->resetLastError();
556 } catch (FrameworkException $e) {
557 // Catch all exceptions and store them in last error
558 $this->lastException = $e;
559 $this->lastError = $e->getMessage();
561 // Throw an SQL exception
562 throw new SqlException (array($this, sprintf("Cannot write data to table '%s'", $tableName), self::DB_CODE_TABLE_UNWRITEABLE), self::EXCEPTION_SQL_QUERY);
567 * "Updates" a data set instance with a database layer
569 * @param $dataSetInstance A storeable data set
571 * @throws SqlException If an SQL error occurs
573 public function queryUpdateDataSet (StoreableCriteria $dataSetInstance) {
574 // Create full path name
575 $pathName = $this->getSavePath() . $dataSetInstance->getTableName() . '/';
577 // Try all the requests
579 // Get a file pointer instance
580 $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
582 // Initialize limit/skip
586 // Get the criteria array from the dataset
587 $criteriaArray = $dataSetInstance->getCriteriaArray();
589 // Get search criteria
590 $searchInstance = $dataSetInstance->getSearchInstance();
592 // Read the directory with some exceptions
593 while (($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess", ".svn", "info." . $this->getFileExtension()))) && ($limitFound < $searchInstance->getLimit())) {
594 // Does the extension match?
595 if (substr($dataFile, -(strlen($this->getFileExtension()))) !== $this->getFileExtension()) {
600 // Open this file for reading
601 $dataArray = $this->getDataArrayFromFile($pathName . $dataFile);
604 if (is_array($dataArray)) {
605 // Search in the criteria with FMFW (First Matches, First Wins)
606 foreach ($dataArray as $key=>$value) {
607 // Get criteria element
608 $criteria = $searchInstance->getCriteriaElemnent($key);
610 // Is the criteria met?
611 if ((!is_null($criteria)) && ($criteria == $value)) {
613 // Shall we skip this entry?
614 if ($searchInstance->getSkip() > 0) {
615 // We shall skip some entries
616 if ($skipFound < $searchInstance->getSkip()) {
623 // Entry found, so update it
624 foreach ($criteriaArray as $criteriaKey=>$criteriaValue) {
625 $dataArray[$criteriaKey] = $criteriaValue;
628 // Write the data to a local file
629 $this->writeDataArrayToFqfn($pathName . $dataFile, $dataArray);
639 // Close the file pointer
640 $directoryInstance->closeDirectory();
642 // Update the primary key
643 $this->updatePrimaryKey($dataSetInstance);
645 // Reset last error message and exception
646 $this->resetLastError();
647 } catch (FrameworkException $e) {
648 // Catch all exceptions and store them in last error
649 $this->lastException = $e;
650 $this->lastError = $e->getMessage();
652 // Throw an SQL exception
653 throw new SqlException (array($this, sprintf("Cannot write data to table '%s'", $dataSetInstance->getTableName()), self::DB_CODE_TABLE_UNWRITEABLE), self::EXCEPTION_SQL_QUERY);
658 * Getter for primary key of specified table or if not found null will be
659 * returned. This must be database-specific.
661 * @param $tableName Name of the table we need the primary key from
662 * @return $primaryKey Primary key column of the given table
664 public function getPrimaryKeyOfTable ($tableName) {
665 // Default key is null
668 // Does the table information exist?
669 if (isset($this->tableInfo[$tableName])) {
670 // Then return the primary key
671 $primaryKey = $this->tableInfo[$tableName]['primary'];