* @version 0.0.0 * @copyright Copyright(c) 2007, 2008 Roland Haeder, this is free software * @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 = 0x010; const DB_CODE_TABLE_UNWRITEABLE = 0x011; const DB_CODE_DATA_FILE_CORRUPT = 0x012; /** * 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; /** * 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__); // Set description $this->setObjectDescription("Class for local file databases"); // Create unique ID $this->generateUniqueId(); // Clean up a little $this->removeSystemArray(); } /** * 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 $dbInstance = new LocalFileDatabase(); // Set save path and IO instance $dbInstance->setSavePath($savePath); $dbInstance->setFileIoInstance($ioInstance); // "Connect" to the database $dbInstance->connectToDatabase(); // Return database instance return $dbInstance; } /** * Setter for save path * * @param $savePath The local save path where we shall put our serialized classes * @return void */ public final function setSavePath ($savePath) { // Secure string $savePath = (string) $savePath; // Set save path $this->savePath = $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; } /** * Analyses if a unique ID has already been used or not by search in the * local database folder. * * @param $uniqueID A unique ID number which shall be checked * before it will be used * @param $inConstructor If we got called in a de/con-structor or * from somewhere else * @return $isUnused true = The unique ID was not found in the database, * false = It is already in use by an other object * @throws NoArrayCreatedException If explode() fails to create an array * @throws InvalidArrayCountException If the array contains less or * more than two elements */ public function isUniqueIdUsed ($uniqueID, $inConstructor = false) { // Currently not used... ;-) $isUsed = false; // Split the unique ID up in path and file name $pathFile = explode("@", $uniqueID); // Are there two elements? Index 0 is the path, 1 the file name + global extension if (!is_array($pathFile)) { // No array found if ($inConstructor) { return false; } else { throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED); } } elseif (count($pathFile) != 2) { // Invalid ID returned! if ($inConstructor) { return false; } else { throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT); } } // Create full path name $pathName = $this->getSavePath() . $pathFile[0]; // Check if the file is there with a file handler if ($inConstructor) { // No exceptions in constructors and destructors! $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true); // Has an object being created? if (!is_object($dirInstance)) return false; } else { // Outside a constructor try { $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName); } catch (PathIsNoDirectoryException $e) { // Okay, path not found return false; } } // Initialize the search loop $isValid = false; while ($dataFile = $dirInstance->readDirectoryExcept(array(".", "..", ".htaccess", ".svn"))) { // Generate FQFN for testing $fqfn = sprintf("%s/%s", $pathName, $dataFile); $this->setLastFile($fqfn); // Get instance for file handler $inputHandler = $this->getFileIoInstance(); // Try to read from it. This makes it sure that the file is // readable and a valid database file $this->setLastFileContents($inputHandler->loadFileContents($fqfn)); // Extract filename (= unique ID) from it $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1)); // Is this the required unique ID? if ($ID == $pathFile[1]) { // Okay, already in use! $isUsed = true; } } // Close the directory handler $dirInstance->closeDirectory(); // Now the same for the file... return $isUsed; } /** * Setter for the last read file * * @param $fqfn The FQFN of the last read file * @return void */ private final function setLastFile ($fqfn) { // Cast string $fqfn = (string) $fqfn; $this->lastFile = $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 ($contents) { // Cast array $contents = (array) $contents; $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; } /** * 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(); } /** * Makes sure that the database connection is alive * * @return void */ public function connectToDatabase () { /* @TODO Do some checks on the database directory and files here */ } /** * 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' => "ok", 'rows' => array() ); // Initialize limit/skip $limitFound = 0; $skipFound = 0; // Read the directory with some exceptions while (($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess", ".svn"))) && ($limitFound < $criteriaInstance->getLimit())) { // 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 // Entry found! $resultData['rows'][] = $dataArray; $limitFound++; break; } // END - if } // END - foreach } else { // Throw an exception here throw new SqlException(sprintf("File '%s' contains invalid data.", $dataFile), self::DB_CODE_DATA_FILE_CORRUPT); } } // 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 = sprintf("%s%s/%s.%s", $this->getSavePath(), $dataSetInstance->getTableName(), md5($dataSetInstance->getUniqueValue()), $this->getFileExtension() ); // Try to save the request away try { // Write the data away $this->writeDataArrayToFqfn($fqfn, $dataSetInstance->getCriteriaArray()); // 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"))) && ($limitFound < $searchInstance->getLimit())) { // 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(); // 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); } } } // [EOF] ?>