Comment header fixed
[shipsimu.git] / inc / classes / main / database / classes / 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          * Save path for "file database"
29          */
30         private $savePath = "";
31
32         /**
33          * The file's extension
34          */
35         private $fileExtension = "serialized";
36
37         /**
38          * The last read file's name
39          */
40         private $lastFile = "";
41
42         /**
43          * The last read file's content including header information
44          */
45         private $lastContents = array();
46
47         /**
48          * The private constructor. Do never instance from outside!
49          * You need to set a local file path. The class will then validate it.
50          *
51          * @return      void
52          */
53         protected function __construct() {
54                 // Call parent constructor
55                 parent::__construct(__CLASS__);
56
57                 // Set description
58                 $this->setObjectDescription("Class for local file databases");
59
60                 // Create unique ID
61                 $this->createUniqueID();
62
63                 // Clean up a little
64                 $this->removeSystemArray();
65         }
66
67         /**
68          * Create an object of LocalFileDatabase and set the save path for local files.
69          * This method also validates the given file path.
70          *
71          * @param               $savePath                                       The local file path string
72          * @param               $ioInstance                             The input/output handler. This
73          *                                                                      should be FileIoHandler
74          * @return      $dbInstance                             An instance of LocalFileDatabase
75          * @throws      SavePathIsEmptyException                If the given save path is an
76          *                                                                      empty string
77          * @throws      SavePathIsNoDirectoryException  If the save path is no
78          *                                                                              path (e.g. a file)
79          * @throws      SavePathReadProtectedException  If the save path is read-
80          *                                                                              protected
81          * @throws      SavePathWriteProtectedException If the save path is write-
82          *                                                                              protected
83          */
84         public final static function createLocalFileDatabase ($savePath, FileIoHandler $ioInstance) {
85                 // Get an instance
86                 $dbInstance = new LocalFileDatabase();
87
88                 if (empty($savePath)) {
89                         // Empty string
90                         throw new SavePathIsEmptyException($dbInstance, self::EXCEPTION_UNEXPECTED_EMPTY_STRING);
91                 } elseif (!is_dir($savePath)) {
92                         // Is not a dir
93                         throw new SavePathIsNoDirectoryException($savePath, self::EXCEPTION_INVALID_PATH_NAME);
94                 } elseif (!is_readable($savePath)) {
95                         // Path not readable
96                         throw new SavePathReadProtectedException($savePath, self::EXCEPTION_READ_PROTECED_PATH);
97                 } elseif (!is_writeable($savePath)) {
98                         // Path not writeable
99                         throw new SavePathWriteProtectedException($savePath, self::EXCEPTION_WRITE_PROTECED_PATH);
100                 }
101
102                 // Set save path and IO instance
103                 $dbInstance->setSavePath($savePath);
104                 $dbInstance->setFileIoInstance($ioInstance);
105
106                 // Return database instance
107                 return $dbInstance;
108         }
109
110         /**
111          * Setter for save path
112          *
113          * @param               $savePath               The local save path where we shall put our serialized classes
114          * @return      void
115          */
116         public final function setSavePath ($savePath) {
117                 // Secure string
118                 $savePath = (string) $savePath;
119
120                 // Set save path
121                 $this->savePath = $savePath;
122         }
123
124         /**
125          * Getter for save path
126          *
127          * @return      $savePath               The local save path where we shall put our serialized classes
128          */
129         public final function getSavePath () {
130                 return $this->savePath;
131         }
132
133         /**
134          * Saves a given object to the local file system by serializing and
135          * transparently compressing it
136          *
137          * @param               $object                         The object we shall save to the local file system
138          * @return      void
139          * @throws      NullPointerException    If the object instance is null
140          * @throws      NoObjectException               If the parameter $object is not
141          *                                                              an object
142          */
143         public final function saveObject ($object) {
144                 // Some tests on the parameter...
145                 if (is_null($object)) {
146                         // Is null, throw exception
147                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
148                 } elseif (!is_object($object)) {
149                         // Is not an object, throw exception
150                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
151                 } elseif (!method_exists($object, '__toString')) {
152                         // A highly required method was not found... :-(
153                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
154                 }
155
156                 // Get a string containing the serialized object. We cannot exchange
157                 // $this and $object here because $object does not need to worry
158                 // about it's limitations... ;-)
159                 $serialized = $this->serializeObject($object);
160
161                 // Get a path name plus file name and append the extension
162                 $fqfn = $this->getSavePath() . $object->getPathFileNameFromObject() . "." . $this->getFileExtension();
163
164                 // Save the file to disc we don't care here if the path is there,
165                 // this must be done in later methods.
166                 $this->getFileIoInstance()->saveFile($fqfn, array($this->getCompressorChannel()->getCompressorExtension(), $serialized));
167         }
168
169         /**
170          * Get a serialized string from the given object
171          *
172          * @param               $object         The object we want to serialize and transparently
173          *                                              compress
174          * @return      $serialized     A string containing the serialzed/compressed object
175          * @see         ObjectLimits    An object holding limition information
176          * @see         SerializationContainer  A special container class for e.g.
177          *                                                              attributes from limited objects
178          */
179         private function serializeObject ($object) {
180                 // If there is no limiter instance we serialize the whole object
181                 // otherwise only in the limiter object (ObjectLimits) specified
182                 // attributes summarized in a special container class
183                 if ($this->getLimitInstance() === null) {
184                         // Serialize the whole object. This tribble call is the reason
185                         // why we need a fall-back implementation in CompressorChannel
186                         // of the methods compressStream() and decompressStream().
187                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($object));
188                 } else {
189                         // Serialize only given attributes in a special container
190                         $container = SerializationContainer::createSerializationContainer($this->getLimitInstance(), $object);
191
192                         // Serialize the container
193                         $serialized = $this->getCompressorChannel()->getCompressor()->compressStream(serialize($container));
194                 }
195
196                 // Return the serialized object string
197                 return $serialized;
198         }
199
200         /**
201          * Analyses if a unique ID has already been used or not by search in the
202          * local database folder.
203          *
204          * @param               $uniqueID               A unique ID number which shall be checked
205          *                                              before it will be used
206          * @param               $inConstructor  If we got called in a de/con-structor or
207          *                                              from somewhere else
208          * @return      $isUnused               true    = The unique ID was not found in the database,
209          *                                              false = It is already in use by an other object
210          * @throws      NoArrayCreatedException If explode() fails to create an array
211          * @throws      InvalidArrayCountException      If the array contains less or
212          *                                                                      more than two elements
213          */
214         public function isUniqueIdUsed ($uniqueID, $inConstructor = false) {
215                 // Currently not used... ;-)
216                 $isUsed = false;
217
218                 // Split the unique ID up in path and file name
219                 $pathFile = explode("@", $uniqueID);
220
221                 // Are there two elements? Index 0 is the path, 1 the file name + global extension
222                 if (!is_array($pathFile)) {
223                         // No array found
224                         if ($inConstructor) {
225                                 return false;
226                         } else {
227                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
228                         }
229                 } elseif (count($pathFile) != 2) {
230                         // Invalid ID returned!
231                         if ($inConstructor) {
232                                 return false;
233                         } else {
234                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
235                         }
236                 }
237
238                 // Create full path name
239                 $pathName = $this->getSavePath() . $pathFile[0];
240
241                 // Check if the file is there with a file handler
242                 if ($inConstructor) {
243                         // No exceptions in constructors and destructors!
244                         $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName, true);
245
246                         // Has an object being created?
247                         if (!is_object($dirInstance)) return false;
248                 } else {
249                         // Outside a constructor
250                         try {
251                                 $dirInstance = FrameworkDirectoryPointer::createFrameworkDirectoryPointer($pathName);
252                         } catch (PathIsNoDirectoryException $e) {
253                                 // Okay, path not found
254                                 return false;
255                         }
256                 }
257
258                 // Initialize the search loop
259                 $isValid = false;
260                 while ($dataFile = $dirInstance->readDirectoryExcept(array(".", ".."))) {
261                         // Generate FQFN for testing
262                         $fqfn = sprintf("%s/%s", $pathName, $dataFile);
263                         $this->setLastFile($fqfn);
264
265                         // Get instance for file handler
266                         $inputHandler = $this->getFileIoInstance();
267
268                         // Try to read from it. This makes it sure that the file is
269                         // readable and a valid database file
270                         $this->setLastFileContents($inputHandler->loadFileContents($fqfn));
271
272                         // Extract filename (= unique ID) from it
273                         $ID = substr(basename($fqfn), 0, -(strlen($this->getFileExtension()) + 1));
274
275                         // Is this the required unique ID?
276                         if ($ID == $pathFile[1]) {
277                                 // Okay, already in use!
278                                 $isUsed = true;
279                         }
280                 }
281
282                 // Close the directory handler
283                 $dirInstance->closeDirectory();
284
285                 // Now the same for the file...
286                 return $isUsed;
287         }
288
289         /**
290          * Setter for the last read file
291          *
292          * @param               $fqfn   The FQFN of the last read file
293          * @return      void
294          */
295         private final function setLastFile ($fqfn) {
296                 // Cast string
297                 $fqfn = (string) $fqfn;
298                 $this->lastFile = $fqfn;
299         }
300
301         /**
302          * Getter for last read file
303          *
304          * @return      $lastFile               The last read file's name with full path
305          */
306         public final function getLastFile () {
307                 return $this->lastFile;
308         }
309
310         /**
311          * Setter for contents of the last read file
312          *
313          * @param               $contents               An array with header and data elements
314          * @return      void
315          */
316         private final function setLastFileContents ($contents) {
317                 // Cast array
318                 $contents = (array) $contents;
319                 $this->lastContents = $contents;
320         }
321
322         /**
323          * Getter for last read file's content as an array
324          *
325          * @return      $lastContent    The array with elements 'header' and 'data'.
326          */
327         public final function getLastContents () {
328                 return $this->lastContents;
329         }
330
331         /**
332          * Getter for file extension
333          *
334          * @return      $fileExtension  The array with elements 'header' and 'data'.
335          */
336         public final function getFileExtension () {
337                 return $this->fileExtension;
338         }
339
340         /**
341          * Get cached (last fetched) data from the local file database
342          *
343          * @param               $uniqueID               The ID number for looking up the data
344          * @return      $object         The restored object from the maybe compressed
345          *                                              serialized data
346          * @throws      MismatchingCompressorsException If the compressor from
347          *                                                                              the loaded file
348          *                                                                              mismatches with the
349          *                                                                              current used one.
350          * @throws      NullPointerException                    If the restored object
351          *                                                                              is null
352          * @throws      NoObjectException                               If the restored "object"
353          *                                                                              is not an object instance
354          * @throws      MissingMethodException                  If the required method
355          *                                                                              toString() is missing
356          */
357         public final function getObjectFromCachedData ($uniqueID) {
358                 // Get instance for file handler
359                 $inputHandler = $this->getFileIoInstance();
360
361                 // Get last file's name and contents
362                 $fqfn = $this->repairFQFN($this->getLastFile(), $uniqueID);
363                 $contents = $this->repairContents($this->getLastContents(), $fqfn);
364
365                 // Let's decompress it. First we need the instance
366                 $compressInstance = $this->getCompressorChannel();
367
368                 // Is the compressor's extension the same as the one from the data?
369                 if ($compressInstance->getCompressorExtension() != $contents['header'][0]) {
370                         /**
371                          * @todo        For now we abort here but later we need to make this a little more dynamic.
372                          */
373                         throw new MismatchingCompressorsException(array($this, $contents['header'][0], $fqfn, $compressInstance->getCompressorExtension()), self::EXCEPTION_MISMATCHING_COMPRESSORS);
374                 }
375
376                 // Decompress the data now
377                 $serialized = $compressInstance->getCompressor()->decompressStream($contents['data']);
378
379                 // And unserialize it...
380                 $object = unserialize($serialized);
381
382                 // This must become a valid object, so let's check it...
383                 if (is_null($object)) {
384                         // Is null, throw exception
385                         throw new NullPointerException($object, self::EXCEPTION_IS_NULL_POINTER);
386                 } elseif (!is_object($object)) {
387                         // Is not an object, throw exception
388                         throw new NoObjectException($object, self::EXCEPTION_IS_NO_OBJECT);
389                 } elseif (!method_exists($object, '__toString')) {
390                         // A highly required method was not found... :-(
391                         throw new MissingMethodException(array($object, '__toString'), self::EXCEPTION_MISSING_METHOD);
392                 }
393
394                 // And return the object
395                 return $object;
396         }
397
398         /**
399          * Private method for re-gathering (repairing) the FQFN
400          *
401          * @param               $fqfn           The current FQFN we shall validate
402          * @param               $uniqueID               The unique ID number
403          * @return      $fqfn           The repaired FQFN when it is empty
404          * @throws      NoArrayCreatedException         If explode() has not
405          *                                                                      created an array
406          * @throws      InvalidArrayCountException      If the array count is not
407          *                                                                      as the expected
408          */
409         private function repairFQFN ($fqfn, $uniqueID) {
410                 // Cast both strings
411                 $fqfn     = (string) $fqfn;
412                 $uniqueID = (string) $uniqueID;
413
414                 // Is there pre-cached data available?
415                 if (empty($fqfn)) {
416                         // Split the unique ID up in path and file name
417                         $pathFile = explode("@", $uniqueID);
418
419                         // Are there two elements? Index 0 is the path, 1 the file name + global extension
420                         if (!is_array($pathFile)) {
421                                 // No array found
422                                 throw new NoArrayCreatedException(array($this, "pathFile"), self::EXCEPTION_ARRAY_EXPECTED);
423                         } elseif (count($pathFile) != 2) {
424                                 // Invalid ID returned!
425                                 throw new InvalidArrayCountException(array($this, "pathFile", count($pathFile), 2), self::EXCEPTION_ARRAY_HAS_INVALID_COUNT);
426                         }
427
428                         // Create full path name
429                         $pathName = $this->getSavePath() . $pathFile[0];
430
431                         // Nothing cached, so let's create a FQFN first
432                         $fqfn = sprintf("%s/%s.%s", $pathName, $pathFile[1], $this->getFileExtension());
433                         $this->setLastFile($fqfn);
434                 }
435
436                 // Return repaired FQFN
437                 return $fqfn;
438         }
439
440         /**
441          * Private method for re-gathering the contents of a given file
442          *
443          * @param               $contents               The (maybe) already cached contents as an array
444          * @param               $fqfn           The current FQFN we shall validate
445          * @return      $contents               The repaired contents from the given file
446          */
447         private function repairContents ($contents, $fqfn) {
448                 // Is there some content and header (2 indexes) in?
449                 if ((!is_array($contents)) || (count($contents) != 2) || (!isset($contents['header'])) || (!isset($contents['data']))) {
450                         // No content found so load the file again
451                         $contents = $inputHandler->loadFileContents($fqfn);
452
453                         // And remember all data for later usage
454                         $this->setLastContents($contents);
455                 }
456
457                 // Return the repaired contents
458                 return $contents;
459         }
460
461         /* DUMMY */ public final function loadObject () {}
462 }
463
464 // [EOF]
465 ?>