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 {
27 // Constants for MySQL backward-compatiblity (PLEASE FIX THEM!)
28 const DB_CODE_TABLE_MISSING = 0x000;
31 * Save path for "file database"
33 private $savePath = "";
36 * The file's extension
38 private $fileExtension = "serialized";
41 * The last read file's name
43 private $lastFile = "";
46 * The last read file's content including header information
48 private $lastContents = array();
51 * Wether the "connection is already up
53 private $alreadyConnected = false;
58 private $lastError = "";
63 private $lastException = null;
66 * The protected constructor. Do never instance from outside! You need to
67 * set a local file path. The class will then validate it.
71 protected function __construct() {
72 // Call parent constructor
73 parent::__construct(__CLASS__);
76 $this->setObjectDescription("Class for local file databases");
79 $this->createUniqueID();
82 $this->removeSystemArray();
86 * Create an object of LocalFileDatabase and set the save path for local files.
87 * This method also validates the given file path.
89 * @param $savePath The local file path string
90 * @param $ioInstance The input/output handler. This
91 * should be FileIoHandler
92 * @return $dbInstance An instance of LocalFileDatabase
94 public final static function createLocalFileDatabase ($savePath, FileIoHandler $ioInstance) {
96 $dbInstance = new LocalFileDatabase();
98 // Set save path and IO instance
99 $dbInstance->setSavePath($savePath);
100 $dbInstance->setFileIoInstance($ioInstance);
102 // "Connect" to the database
103 $dbInstance->connectToDatabase();
105 // Return database instance
110 * Setter for save path
112 * @param $savePath The local save path where we shall put our serialized classes
115 public final function setSavePath ($savePath) {
117 $savePath = (string) $savePath;
120 $this->savePath = $savePath;
124 * Getter for save path
126 * @return $savePath The local save path where we shall put our serialized classes
128 public final function getSavePath () {
129 return $this->savePath;
133 * Getter for last error message
135 * @return $lastError Last error message
137 public final function getLastError () {
138 return $this->lastError;
142 * Getter for last exception
144 * @return $lastException Last thrown exception
146 public final function getLastException () {
147 return $this->lastException;
151 * Saves a given object to the local file system by serializing and
152 * transparently compressing it
154 * @param $object The object we shall save to the local file system
156 * @throws NullPointerException If the object instance is null
157 * @throws NoObjectException If the parameter $object is not
160 public final function saveObject ($object) {
161 // Some tests on the parameter...
162 if (is_null($object)) {
163 // Is null, throw exception
164 throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
165 } elseif (!is_object($object)) {
166 // Is not an object, throw exception
167 throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
168 } elseif (!method_exists($object, '__toString')) {
169 // A highly required method was not found... :-(
170 throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
173 // Get a string containing the serialized object. We cannot exchange
174 // $this and $object here because $object does not need to worry
175 // about it's limitations... ;-)
176 $serialized = $this->serializeObject($object);
178 // Get a path name plus file name and append the extension
179 $fqfn = $this->getSavePath() . $object->getPathFileNameFromObject() . "." . $this->getFileExtension();
181 // Save the file to disc we don't care here if the path is there,
182 // this must be done in later methods.
183 $this->getFileIoInstance()->saveFile($fqfn, array($this->getCompressorChannel()->getCompressorExtension(), $serialized));
187 * Get a serialized string from the given object
189 * @param $object The object we want to serialize and transparently
191 * @return $serialized A string containing the serialzed/compressed object
192 * @see ObjectLimits An object holding limition information
193 * @see SerializationContainer A special container class for e.g.
194 * attributes from limited objects
196 private function serializeObject ($object) {
197 // If there is no limiter instance we serialize the whole object
198 // otherwise only in the limiter object (ObjectLimits) specified
199 // attributes summarized in a special container class
200 if ($this->getLimitInstance() === null) {
201 // Serialize the whole object. This tribble call is the reason
202 // why we need a fall-back implementation in CompressorChannel
203 // of the methods compressStream() and decompressStream().
204 $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($object));
206 // Serialize only given attributes in a special container
207 $container = SerializationContainer::createSerializationContainer($this->getLimitInstance(), $object);
209 // Serialize the container
210 $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($container));
213 // Return the serialized object string
218 * Analyses if a unique ID has already been used or not by search in the
219 * local database folder.
221 * @param $uniqueID A unique ID number which shall be checked
222 * before it will be used
223 * @param $inConstructor If we got called in a de/con-structor or
224 * from somewhere else
225 * @return $isUnused true = The unique ID was not found in the database,
226 * false = It is already in use by an other object
227 * @throws NoArrayCreatedException If explode() fails to create an array
228 * @throws InvalidArrayCountException If the array contains less or
229 * more than two elements
231 public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
232 // Currently not used... ;-)
235 // Split the unique ID up in path and file name
236 $pathFile = explode("@", $uniqueID);
238 // Are there two elements? Index 0 is the path, 1 the file name + global extension
239 if (!is_array($pathFile)) {
241 if ($inConstructor) {
244 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
246 } elseif (count($pathFile) != 2) {
247 // Invalid ID returned!
248 if ($inConstructor) {
251 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
255 // Create full path name
256 $pathName = $this->getSavePath() . $pathFile[0];
258 // Check if the file is there with a file handler
259 if ($inConstructor) {
260 // No exceptions in constructors and destructors!
261 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
263 // Has an object being created?
264 if (!is_object($dirInstance)) return false;
266 // Outside a constructor
268 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
269 } catch (PathIsNoDirectoryException $e) {
270 // Okay, path not found
275 // Initialize the search loop
277 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", ".."))) {
278 // Generate FQFN for testing
279 $fqfn = sprintf("%s/%s", $pathName, $dataFile);
280 $this->setLastFile($fqfn);
282 // Get instance for file handler
283 $inputHandler = $this->getFileIoInstance();
285 // Try to read from it. This makes it sure that the file is
286 // readable and a valid database file
287 $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
289 // Extract filename (= unique ID) from it
290 $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
292 // Is this the required unique ID?
293 if ($ID == $pathFile[1]) {
294 // Okay, already in use!
299 // Close the directory handler
300 $dirInstance->closeDirectory();
302 // Now the same for the file...
307 * Setter for the last read file
309 * @param $fqfn The FQFN of the last read file
312 private final function setLastFile ($fqfn) {
314 $fqfn = (string) $fqfn;
315 $this->lastFile = $fqfn;
319 * Reset the last error and exception instance. This should be done after
320 * a successfull "query"
324 private final function resetLastError () {
325 $this->lastError = "";
326 $this->lastException = null;
330 * Getter for last read file
332 * @return $lastFile The last read file's name with full path
334 public final function getLastFile () {
335 return $this->lastFile;
339 * Setter for contents of the last read file
341 * @param $contents An array with header and data elements
344 private final function setLastFileContents ($contents) {
346 $contents = (array) $contents;
347 $this->lastContents = $contents;
351 * Getter for last read file's content as an array
353 * @return $lastContent The array with elements 'header' and 'data'.
355 public final function getLastContents () {
356 return $this->lastContents;
360 * Getter for file extension
362 * @return $fileExtension The array with elements 'header' and 'data'.
364 public final function getFileExtension () {
365 return $this->fileExtension;
369 * Get cached (last fetched) data from the local file database
371 * @param $uniqueID The ID number for looking up the data
372 * @return $object The restored object from the maybe compressed
374 * @throws MismatchingCompressorsException If the compressor from
376 * mismatches with the
378 * @throws NullPointerException If the restored object
380 * @throws NoObjectException If the restored "object"
381 * is not an object instance
382 * @throws MissingMethodException If the required method
383 * toString() is missing
385 public final function getObjectFromCachedData ($uniqueID) {
386 // Get instance for file handler
387 $inputHandler = $this->getFileIoInstance();
389 // Get last file's name and contents
390 $fqfn = $this->repairFQFN($this->getLastFile(), $uniqueID);
391 $contents = $this->repairContents($this->getLastContents(), $fqfn);
393 // Let's decompress it. First we need the instance
394 $compressInstance = $this->getCompressorChannel();
396 // Is the compressor's extension the same as the one from the data?
397 if ($compressInstance->getCompressorExtension() != $contents['header'][0]) {
399 * @todo For now we abort here but later we need to make this a little more dynamic.
401 throw new MismatchingCompressorsException(array($this, $contents['header'][0], $fqfn, $compressInstance->getCompressorExtension()), self::EXCEPTION_MISMATCHING_COMPRESSORS);
404 // Decompress the data now
405 $serialized = $compressInstance->getCompressor()->decompressStream($contents['data']);
407 // And unserialize it...
408 $object = unserialize($serialized);
410 // This must become a valid object, so let's check it...
411 if (is_null($object)) {
412 // Is null, throw exception
413 throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
414 } elseif (!is_object($object)) {
415 // Is not an object, throw exception
416 throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
417 } elseif (!$object instanceof FrameworkInterface) {
418 // A highly required method was not found... :-(
419 throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
422 // And return the object
427 * Private method for re-gathering (repairing) the FQFN
429 * @param $fqfn The current FQFN we shall validate
430 * @param $uniqueID The unique ID number
431 * @return $fqfn The repaired FQFN when it is empty
432 * @throws NoArrayCreatedException If explode() has not
434 * @throws InvalidArrayCountException If the array count is not
437 private function repairFQFN ($fqfn, $uniqueID) {
439 $fqfn = (string) $fqfn;
440 $uniqueID = (string) $uniqueID;
442 // Is there pre-cached data available?
444 // Split the unique ID up in path and file name
445 $pathFile = explode("@", $uniqueID);
447 // Are there two elements? Index 0 is the path, 1 the file name + global extension
448 if (!is_array($pathFile)) {
450 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
451 } elseif (count($pathFile) != 2) {
452 // Invalid ID returned!
453 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
456 // Create full path name
457 $pathName = $this->getSavePath() . $pathFile[0];
459 // Nothing cached, so let's create a FQFN first
460 $fqfn = sprintf("%s/%s.%s", $pathName, $pathFile[1], $this->getFileExtension());
461 $this->setLastFile($fqfn);
464 // Return repaired FQFN
469 * Private method for re-gathering the contents of a given file
471 * @param $contents The (maybe) already cached contents as an array
472 * @param $fqfn The current FQFN we shall validate
473 * @return $contents The repaired contents from the given file
475 private function repairContents ($contents, $fqfn) {
476 // Is there some content and header (2 indexes) in?
477 if ((!is_array($contents)) || (count($contents) != 2) || (!isset($contents['header'])) || (!isset($contents['data']))) {
478 // No content found so load the file again
479 $contents = $inputHandler->loadFileContents($fqfn);
481 // And remember all data for later usage
482 $this->setLastContents($contents);
485 // Return the repaired contents
490 * Makes sure that the database connection is alive
494 public function connectToDatabase () {
495 // @TODO Do some checks on the database directory and files here
499 * Loads data saved with saveObject from the database and re-creates a
500 * full object from it.
501 * If limitObject() was called before a new object ObjectContainer with
502 * all requested attributes will be returned instead.
504 * @return Object The fully re-created object or instance to
506 * @throws SavePathIsEmptyException If the given save path is an
508 * @throws SavePathIsNoDirectoryException If the save path is no
510 * @throws SavePathReadProtectedException If the save path is read-
512 * @throws SavePathWriteProtectedException If the save path is write-
515 public function loadObject () {
516 // Already connected? Then abort here
517 if ($this->alreadyConnected === true) return true;
520 $savePath = $this->getSavePath();
522 if (empty($savePath)) {
524 throw new SavePathIsEmptyException($dbInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
525 } elseif (!is_dir($savePath)) {
527 throw new SavePathIsNoDirectoryException($savePath, self::EXCEPTION_INVALID_PATH_NAME);
528 } elseif (!is_readable($savePath)) {
530 throw new SavePathReadProtectedException($savePath, self::EXCEPTION_READ_PROTECED_PATH);
531 } elseif (!is_writeable($savePath)) {
532 // Path not writeable
533 throw new SavePathWriteProtectedException($savePath, self::EXCEPTION_WRITE_PROTECED_PATH);
536 // "Connection" established... ;-)
537 $this->alreadyConnected = true;
541 * Starts a SELECT query on the database by given return type, table name
542 * and search criteria
544 * @param $resultType Result type ("array", "object" and "indexed" are valid)
545 * @param $tableName Name of the database table
546 * @param $criteria Local search criteria class
547 * @return $resultData Result data of the query
548 * @throws UnsupportedCriteriaException If the criteria is unsupported
549 * @throws SqlException If an "SQL error" occurs
551 public function querySelect ($resultType, $tableName, Criteria $criteriaInstance) {
552 // The result is null by any errors
555 // Is this criteria supported?
556 if (!$criteriaInstance instanceof LocalCriteria) {
557 // Not supported by this database layer
558 throw new UnsupportedCriteriaException(array($this, $criteriaInstance), self::EXCEPTION_REQUIRED_INTERFACE_MISSING);
561 // Create full path name
562 $pathName = $this->getSavePath() . $tableName . '/';
564 // A "select" query is not that easy on local files, so first try to
565 // find the "table" which is in fact a directory on the server
567 // Get a directory pointer instance
568 $directoryInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
570 // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read
576 // Read the directory with some exceptions
577 while ($dataFile = $directoryInstance->readDirectoryExcept(array(".", "..", ".htaccess"))) {
578 $this->partialStub(sprintf("File %s found.", $dataFile));
581 // Close directory and throw the instance away
582 $directoryInstance->closeDirectory();
583 unset($directoryInstance);
585 // Reset last error message and exception
586 $this->resetLastError();
587 } catch (PathIsNoDirectoryException $e) {
588 // Path not found means "table not found" for real databases...
589 $this->lastException = $e;
590 $this->lastError = $e->getMessage();
592 // So throw an SqlException here with faked error message
593 throw new SqlException (array($this, sprintf("Table '%s' not found", $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY);
594 } catch (FrameworkException $e) {
595 // Catch all exceptions and store them in last error
596 $this->lastException = $e;
597 $this->lastError = $e->getMessage();
600 // Return the gathered result