* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009, 2010 Core Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.ship-simu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ class LocalFileDatabase extends BaseDatabaseFrontend implements DatabaseFrontendInterface { // Constants for MySQL backward-compatiblity (PLEASE FIX THEM!) const DB_CODE_TABLE_MISSING = 0x100; const DB_CODE_TABLE_UNWRITEABLE = 0x101; const DB_CODE_DATA_FILE_CORRUPT = 0x102; // Status results const RESULT_OKAY = 'ok'; /** * Save path for "file database" */ private $savePath = ''; /** * The file's extension */ private $fileExtension = 'serialized'; /** * The last read file's name */ private $lastFile = ''; /** * The last read file's content including header information */ private $lastContents = array(); /** * Wether the "connection is already up */ private $alreadyConnected = false; /** * Last error message */ private $lastError = ''; /** * Last exception */ private $lastException = null; /** * Table information array */ private $tableInfo = array(); /** * Element for index */ private $indexKey = '__idx'; /** * The protected constructor. Do never instance from outside! You need to * set a local file path. The class will then validate it. * * @return void */ protected function __construct() { // Call parent constructor parent::__construct(__CLASS__); } /** * Create an object of LocalFileDatabase and set the save path for local files. * This method also validates the given file path. * * @param $savePath The local file path string * @param $ioInstance The input/output handler. This * should be FileIoHandler * @return $dbInstance An instance of LocalFileDatabase */ public final static function createLocalFileDatabase ($savePath, FileIoHandler $ioInstance) { // Get an instance $databaseInstance = new LocalFileDatabase(); // Set save path and IO instance $databaseInstance->setSavePath($savePath); $databaseInstance->setFileIoInstance($ioInstance); // Set the compressor channel $databaseInstance->setCompressorChannel(CompressorChannel::createCompressorChannel( $databaseInstance->getConfigInstance()->getConfigEntry('base_path'). $databaseInstance->getConfigInstance()->getConfigEntry('compressor_base_path') )); // "Connect" to the database $databaseInstance->connectToDatabase(); // Return database instance return $databaseInstance; } /** * Setter for save path * * @param $savePath The local save path where we shall put our serialized classes * @return void */ public final function setSavePath ($savePath) { // Set save path $this->savePath = (string) $savePath; } /** * Getter for save path * * @return $savePath The local save path where we shall put our serialized classes */ public final function getSavePath () { return $this->savePath; } /** * Getter for last error message * * @return $lastError Last error message */ public final function getLastError () { return $this->lastError; } /** * Getter for last exception * * @return $lastException Last thrown exception */ public final function getLastException () { return $this->lastException; } /** * Setter for the last read file * * @param $fqfn The FQFN of the last read file * @return void */ private final function setLastFile ($fqfn) { // Cast string and set it $this->lastFile = (string) $fqfn; } /** * Reset the last error and exception instance. This should be done after * a successfull "query" * * @return void */ private final function resetLastError () { $this->lastError = ''; $this->lastException = null; } /** * Getter for last read file * * @return $lastFile The last read file's name with full path */ public final function getLastFile () { return $this->lastFile; } /** * Setter for contents of the last read file * * @param $contents An array with header and data elements * @return void */ private final function setLastFileContents (array $contents) { // Set array $this->lastContents = $contents; } /** * Getter for last read file's content as an array * * @return $lastContent The array with elements 'header' and 'data'. */ public final function getLastContents () { return $this->lastContents; } /** * Getter for file extension * * @return $fileExtension The array with elements 'header' and 'data'. */ public final function getFileExtension () { return $this->fileExtension; } /** * Getter for index key * * @return $indexKey Index key */ public final function getIndexKey () { return $this->indexKey; } /** * Reads a local data file and returns it's contents in an array * * @param $fqfn The FQFN for the requested file * @return $dataArray */ private function getDataArrayFromFile ($fqfn) { // Get a file pointer $fileInstance = FrameworkFileInputPointer::createFrameworkFileInputPointer($fqfn); // Get the raw data and BASE64-decode it $compressedData = base64_decode($fileInstance->readLinesFromFile()); // Close the file and throw the instance away $fileInstance->closeFile(); unset($fileInstance); // Decompress it $serializedData = $this->getCompressorChannel()->getCompressor()->decompressStream($compressedData); // Unserialize it $dataArray = unserialize($serializedData); // Finally return it return $dataArray; } /** * Writes data array to local file * * @param $fqfn The FQFN of the local file * @param $dataArray An array with all the data we shall write * @return void */ private function writeDataArrayToFqfn ($fqfn, array $dataArray) { // Get a file pointer instance $fileInstance = FrameworkFileOutputPointer::createFrameworkFileOutputPointer($fqfn, 'w'); // Serialize and compress it $compressedData = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($dataArray)); // Write this data BASE64 encoded to the file $fileInstance->writeToFile(base64_encode($compressedData)); // Close the file pointer $fileInstance->closeFile(); } /** * Getter for table information file contents or an empty if info file was not created * * @param $dataSetInstance An instance of a database set class * @return $infoArray An array with all table informations */ private function getContentsFromTableInfoFile (StoreableCriteria $dataSetInstance) { // Default content is no data $infoArray = array(); // Create FQFN for getting the table information file $fqfn = $this->generateFqfnFromDataSet($dataSetInstance, 'info'); // Get the file contents try { $infoArray = $this->getDataArrayFromFile($fqfn); } catch (FileIoException $e) { // Not found, so ignore it here } // ... and return it return $infoArray; } /** * Generates an FQFN from given dataset instance and string * * @param $dataSetInstance An instance of a database set class * @param $rowName Name of the row * @return $fqfn The FQFN for this row */ private function generateFqfnFromDataSet (Criteria $dataSetInstance, $rowName) { // This is the FQFN $fqfn = $this->getSavePath() . $dataSetInstance->getTableName() . '/' . $rowName . '.' . $this->getFileExtension(); // Return it return $fqfn; } /** * Creates the table info file from given dataset instance * * @param $dataSetInstance An instance of a database set class * @return void */ private function createTableInfoFile (StoreableCriteria $dataSetInstance) { // Create FQFN for creating the table information file $fqfn = $this->generateFqfnFromDataSet($dataSetInstance, 'info'); // Get the data out from dataset in a local array $this->tableInfo[$dataSetInstance->getTableName()] = array( 'primary' => $dataSetInstance->getPrimaryKey(), 'created' => time(), 'last_updated' => time() ); // Write the data to the file $this->writeDataArrayToFqfn($fqfn, $this->tableInfo[$dataSetInstance->getTableName()]); } /** * Updates the primary key information or creates the table info file if not found * * @param $dataSetInstance An instance of a database set class * @return void */ private function updatePrimaryKey (StoreableCriteria $dataSetInstance) { // Get the information array from lower method $infoArray = $this->getContentsFromTableInfoFile($dataSetInstance); // Is the primary key there? if (!isset($this->tableInfo['primary'])) { // Then create the info file $this->createTableInfoFile($dataSetInstance); } elseif (($this->getConfigInstance()->getConfigEntry('db_update_primary_forced') == 'Y') && ($dataSetInstance->getPrimaryKey() != $this->tableInfo['primary'])) { // Set the array element $this->tableInfo[$dataSetInstance->getTableName()]['primary'] = $dataSetInstance->getPrimaryKey(); // Update the entry $this->updateTableInfoFile($dataSetInstance); } } /** * Makes sure that the database connection is alive * * @return void * @todo Do some checks on the database directory and files here */ public function connectToDatabase () { } /** * Starts a SELECT query on the database by given return type, table name * and search criteria * * @param $resultType Result type ('array', 'object' and 'indexed' are valid) * @param $tableName Name of the database table * @param $criteria Local search criteria class * @return $resultData Result data of the query * @throws UnsupportedCriteriaException If the criteria is unsupported * @throws SqlException If an 'SQL error' occurs */ public function querySelect ($resultType, $tableName, LocalSearchCriteria $criteriaInstance) { // The result is null by any errors $resultData = null; // Create full path name $pathName = $this->getSavePath() . $tableName . '/'; // A 'select' query is not that easy on local files, so first try to // find the 'table' which is in fact a directory on the server try { // Get a directory pointer instance $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName); // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read $resultData = array( 'status' => LocalfileDatabase::RESULT_OKAY, 'rows' => array() ); // Initialize limit/skip $limitFound = 0; $skipFound = 0; $idx = 1; // Read the directory with some exceptions while (($dataFile = $directoryInstance->readDirectoryExcept(array('.', '..', '.htaccess', '.svn', "info." . $this->getFileExtension()))) && ($limitFound < $criteriaInstance->getLimit())) { // Does the extension match? if (substr($dataFile, -(strlen($this->getFileExtension()))) !== $this->getFileExtension()) { // Skip this file! continue; } // END - if // Read the file $dataArray = $this->getDataArrayFromFile($pathName . $dataFile); // Is this an array? if (is_array($dataArray)) { // Search in the criteria with FMFW (First Matches, First Wins) foreach ($dataArray as $key => $value) { // Get criteria element $criteria = $criteriaInstance->getCriteriaElemnent($key); // Is the criteria met? if ((!is_null($criteria)) && ($criteria == $value)) { // Shall we skip this entry? if ($criteriaInstance->getSkip() > 0) { // We shall skip some entries if ($skipFound < $criteriaInstance->getSkip()) { // Skip this entry $skipFound++; break; } // END - if } // END - if // Set id number $dataArray[$this->getIndexKey()] = $idx; // Entry found! $resultData['rows'][] = $dataArray; // Count found entries up $limitFound++; break; } // END - if } // END - foreach } else { // Throw an exception here throw new SqlException(array($this, sprintf("File '%s' contains invalid data.", $dataFile), self::DB_CODE_DATA_FILE_CORRUPT), self::EXCEPTION_SQL_QUERY); } // Count entry up $idx++; } // END - while // Close directory and throw the instance away $directoryInstance->closeDirectory(); unset($directoryInstance); // Reset last error message and exception $this->resetLastError(); } catch (PathIsNoDirectoryException $e) { // Path not found means "table not found" for real databases... $this->lastException = $e; $this->lastError = $e->getMessage(); // So throw an SqlException here with faked error message throw new SqlException (array($this, sprintf("Table '%s' not found", $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY); } catch (FrameworkException $e) { // Catch all exceptions and store them in last error $this->lastException = $e; $this->lastError = $e->getMessage(); } // Return the gathered result return $resultData; } /** * "Inserts" a data set instance into a local file database folder * * @param $dataSetInstance A storeable data set * @return void * @throws SqlException If an SQL error occurs */ public function queryInsertDataSet (StoreableCriteria $dataSetInstance) { // Create full path name $fqfn = $this->generateFqfnFromDataSet($dataSetInstance, md5($dataSetInstance->getUniqueValue())); // Try to save the request away try { // Write the data away $this->writeDataArrayToFqfn($fqfn, $dataSetInstance->getCriteriaArray()); // Update the primary key $this->updatePrimaryKey($dataSetInstance); // Reset last error message and exception $this->resetLastError(); } catch (FrameworkException $e) { // Catch all exceptions and store them in last error $this->lastException = $e; $this->lastError = $e->getMessage(); // Throw an SQL exception throw new SqlException (array($this, sprintf("Cannot write data to table '%s'", $tableName), self::DB_CODE_TABLE_UNWRITEABLE), self::EXCEPTION_SQL_QUERY); } } /** * "Updates" a data set instance with a database layer * * @param $dataSetInstance A storeable data set * @return void * @throws SqlException If an SQL error occurs */ public function queryUpdateDataSet (StoreableCriteria $dataSetInstance) { // Create full path name $pathName = $this->getSavePath() . $dataSetInstance->getTableName() . '/'; // Try all the requests try { // Get a file pointer instance $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName); // Initialize limit/skip $limitFound = 0; $skipFound = 0; // Get the criteria array from the dataset $criteriaArray = $dataSetInstance->getCriteriaArray(); // Get search criteria $searchInstance = $dataSetInstance->getSearchInstance(); // Read the directory with some exceptions while (($dataFile = $directoryInstance->readDirectoryExcept(array('.', '..', '.htaccess', '.svn', "info." . $this->getFileExtension()))) && ($limitFound < $searchInstance->getLimit())) { // Does the extension match? if (substr($dataFile, -(strlen($this->getFileExtension()))) !== $this->getFileExtension()) { // Skip this file! continue; } // Open this file for reading $dataArray = $this->getDataArrayFromFile($pathName . $dataFile); // Is this an array? if (is_array($dataArray)) { // Search in the criteria with FMFW (First Matches, First Wins) foreach ($dataArray as $key => $value) { // Get criteria element $criteria = $searchInstance->getCriteriaElemnent($key); // Is the criteria met? if ((!is_null($criteria)) && ($criteria == $value)) { // Shall we skip this entry? if ($searchInstance->getSkip() > 0) { // We shall skip some entries if ($skipFound < $searchInstance->getSkip()) { // Skip this entry $skipFound++; break; } // END - if } // END - if // Entry found, so update it foreach ($criteriaArray as $criteriaKey => $criteriaValue) { $dataArray[$criteriaKey] = $criteriaValue; } // END - foreach // Write the data to a local file $this->writeDataArrayToFqfn($pathName . $dataFile, $dataArray); // Count it $limitFound++; break; } // END - if } // END - foreach } // END - if } // END - while // Close the file pointer $directoryInstance->closeDirectory(); // Update the primary key $this->updatePrimaryKey($dataSetInstance); // Reset last error message and exception $this->resetLastError(); } catch (FrameworkException $e) { // Catch all exceptions and store them in last error $this->lastException = $e; $this->lastError = $e->getMessage(); // Throw an SQL exception throw new SqlException (array($this, sprintf("Cannot write data to table '%s'", $dataSetInstance->getTableName()), self::DB_CODE_TABLE_UNWRITEABLE), self::EXCEPTION_SQL_QUERY); } } /** * Getter for primary key of specified table or if not found null will be * returned. This must be database-specific. * * @param $tableName Name of the table we need the primary key from * @return $primaryKey Primary key column of the given table */ public function getPrimaryKeyOfTable ($tableName) { // Default key is null $primaryKey = null; // Does the table information exist? if (isset($this->tableInfo[$tableName])) { // Then return the primary key $primaryKey = $this->tableInfo[$tableName]['primary']; } // END - if // Return the column return $primaryKey; } } // [EOF] ?>