Continued:
[core.git] / framework / main / classes / database / backend / lfdb_legacy / class_CachedLocalFileDatabase.php
1 <?php
2 // Own namespace
3 namespace Org\Mxchange\CoreFramework\Database\Backend\Lfdb;
4
5 // Import framework stuff
6 use Org\Mxchange\CoreFramework\Bootstrap\FrameworkBootstrap;
7 use Org\Mxchange\CoreFramework\Criteria\Criteria;
8 use Org\Mxchange\CoreFramework\Criteria\Local\LocalSearchCriteria;
9 use Org\Mxchange\CoreFramework\Criteria\Storing\StoreableCriteria;
10 use Org\Mxchange\CoreFramework\Database\Backend\BaseDatabaseBackend;
11 use Org\Mxchange\CoreFramework\Database\Backend\DatabaseBackend;
12 use Org\Mxchange\CoreFramework\Database\Sql\SqlException;
13 use Org\Mxchange\CoreFramework\EntryPoint\ApplicationEntryPoint;
14 use Org\Mxchange\CoreFramework\Factory\Object\ObjectFactory;
15 use Org\Mxchange\CoreFramework\Filesystem\FileNotFoundException;
16 use Org\Mxchange\CoreFramework\Generic\FrameworkException;
17 use Org\Mxchange\CoreFramework\Generic\FrameworkInterface;
18 use Org\Mxchange\CoreFramework\Result\Database\BaseDatabaseResult;
19 use Org\Mxchange\CoreFramework\Traits\Compressor\Channel\CompressorChannelTrait;
20 use Org\Mxchange\CoreFramework\Traits\Handler\Io\IoHandlerTrait;
21
22 // Import SPL stuff
23 use \BadMethodCallException;
24 use \InvalidArgumentException;
25 use \SplFileInfo;
26
27 /**
28  * Database backend class for storing objects in locally created files.
29  *
30  * This class serializes arrays stored in the dataset instance and saves them
31  * to local files. Every file (except 'info') represents a single line. Every
32  * directory within the 'db' (base) directory represents a table.
33  *
34  * A configurable 'file_io_class' is being used as "storage backend".
35  *
36  * @author              Roland Haeder <webmaster@shipsimu.org>
37  * @version             0.0.0
38  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 Core Developer Team
39  * @license             GNU GPL 3.0 or any newer version
40  * @link                http://www.shipsimu.org
41  *
42  * This program is free software: you can redistribute it and/or modify
43  * it under the terms of the GNU General Public License as published by
44  * the Free Software Foundation, either version 3 of the License, or
45  * (at your option) any later version.
46  *
47  * This program is distributed in the hope that it will be useful,
48  * but WITHOUT ANY WARRANTY; without even the implied warranty of
49  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
50  * GNU General Public License for more details.
51  *
52  * You should have received a copy of the GNU General Public License
53  * along with this program. If not, see <http://www.gnu.org/licenses/>.
54  */
55 class CachedLocalFileDatabase extends BaseDatabaseBackend implements DatabaseBackend {
56         // Load traits
57         use CompressorChannelTrait;
58         use IoHandlerTrait;
59
60         /**
61          * The file's extension
62          */
63         private $fileExtension = 'serialized';
64
65         /**
66          * The last read file's name
67          */
68         private $lastFile = '';
69
70         /**
71          * The last read file's content including header information
72          */
73         private $lastContents = [];
74
75         /**
76          * Whether the "connection is already up
77          */
78         private $alreadyConnected = false;
79
80         /**
81          * Table information array
82          */
83         private $tableInfo = [];
84
85         /**
86          * Element for index
87          */
88         private $indexKey = '__idx';
89
90         /**
91          * Cached file names based on table name to avoid "expensive" invocations
92          * of FrameworkConfiguration->getConfigEntry().
93          */
94         private $pathNames = [];
95
96         /**
97          * The protected constructor. Do never instance from outside! You need to
98          * set a local file path. The class will then validate it.
99          *
100          * @return      void
101          */
102         private function __construct () {
103                 // Call parent constructor
104                 parent::__construct(__CLASS__);
105         }
106
107         /**
108          * Create an object of CachedLocalFileDatabase and set the save path from
109          * configuration for local files.
110          *
111          * @return      $databaseInstance       An instance of CachedLocalFileDatabase
112          */
113         public static final function createCachedLocalFileDatabase () {
114                 // Get an instance
115                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: CALLED!');
116                 $databaseInstance = new CachedLocalFileDatabase();
117
118                 // Set the compressor channel
119                 $databaseInstance->setCompressorChannelInstance(ObjectFactory::createObjectByConfiguredName('compressor_channel_class'));
120
121                 // Get a file IO handler and set it
122                 $databaseInstance->setFileIoInstance(ObjectFactory::createObjectByConfiguredName('file_io_class'));
123
124                 // "Connect" to the database
125                 $databaseInstance->connectToDatabase();
126
127                 // Return database instance
128                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: databaseInstance=%s - EXIT!', $databaseInstance->__toString()));
129                 return $databaseInstance;
130         }
131
132         /**
133          * Setter for the last read file
134          *
135          * @param       $infoInstance   The FQFN of the last read file
136          * @return      void
137          */
138         private final function setLastFile (SplFileInfo $infoInstance) {
139                 // Cast string and set it
140                 $this->lastFile = $infoInstance;
141         }
142
143         /**
144          * Getter for last read file
145          *
146          * @return      $lastFile       The last read file's name with full path
147          */
148         public final function getLastFile () {
149                 return $this->lastFile;
150         }
151
152         /**
153          * Setter for contents of the last read file
154          *
155          * @param               $contents       An array with header and data elements
156          * @return      void
157          */
158         private final function setLastFileContents (array $contents) {
159                 // Set array
160                 $this->lastContents = $contents;
161         }
162
163         /**
164          * Getter for last read file's content as an array
165          *
166          * @return      $lastContent    The array with elements 'header' and 'data'.
167          */
168         public final function getLastContents () {
169                 return $this->lastContents;
170         }
171
172         /**
173          * Getter for file extension
174          *
175          * @return      $fileExtension  The array with elements 'header' and 'data'.
176          */
177         public final function getFileExtension () {
178                 return $this->fileExtension;
179         }
180
181         /**
182          * Getter for index key
183          *
184          * @return      $indexKey       Index key
185          */
186         public final function getIndexKey () {
187                 return $this->indexKey;
188         }
189
190         /**
191          * Reads a local data file  and returns it's contents in an array
192          *
193          * @param       $infoInstance   An instance of a SplFileInfo class
194          * @return      $dataArray
195          */
196         private function getDataArrayFromFile (SplFileInfo $infoInstance) {
197                 // Init compressed data
198                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: infoInstance=%s - CALLED!', $infoInstance->__toString()));
199                 $compressedData = $this->getFileIoInstance()->loadFileContents($infoInstance);
200
201                 // Is it valid?
202                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('CACHED-LOCAL-FILE-DATABASE: compressedData[]=%s', gettype($compressedData)));
203                 //* DEBUG-DIE: */ die(sprintf('[%s:%d]: compressedData=%s', __METHOD__, __LINE__, print_r($compressedData, TRUE)));
204                 if (!isset($compressedData['data'])) {
205                         // Important element not found
206                         ApplicationEntryPoint::exitApplication(sprintf('compressedData()=%d has no element "data"', count($compressedData)), FrameworkInterface::EXCEPTION_OUT_OF_BOUNDS);
207                 }
208
209                 // Decompress it
210                 $serializedData = $this->getCompressorChannelInstance()->getCompressor()->decompressStream($compressedData['data']);
211
212                 // Unserialize it
213                 $dataArray = json_decode($serializedData, true);
214
215                 // Finally return it
216                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataArray()=%d - EXIT!', count($dataArray)));
217                 return $dataArray;
218         }
219
220         /**
221          * Writes data array to local file
222          *
223          * @param       $infoInstance   An instance of a SplFileInfo class
224          * @param       $dataArray      An array with all the data we shall write
225          * @return      void
226          */
227         private function writeDataArrayToFqfn (SplFileInfo $infoInstance, array $dataArray) {
228                 // Serialize and compress it
229                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: Flushing ' . count($dataArray) . ' elements to database file ' . $infoInstance . ' ...');
230                 $compressedData = $this->getCompressorChannelInstance()->getCompressor()->compressStream(json_encode($dataArray));
231
232                 // Write this data BASE64 encoded to the file
233                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput('CACHED-LOCAL-FILE-DATABASE: Writing ' . strlen($compressedData) . ' bytes ...');
234                 $this->getFileIoInstance()->saveStreamToFile($infoInstance, $compressedData, $this);
235
236                 // Trace message
237                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: Flushing ' . count($dataArray) . ' elements to database file completed.');
238         }
239
240         /**
241          * Getter for table information file contents or an empty if info file was not created
242          *
243          * @param       $dataSetInstance        An instance of a database set class
244          * @return      $infoArray                      An array with all table informations
245          */
246         private function getContentsFromTableInfoFile (StoreableCriteria $dataSetInstance) {
247                 // Default content is no data
248                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
249                 $infoArray = [];
250
251                 // Create FQFN for getting the table information file
252                 $infoInstance = $this->generateFileFromDataSet($dataSetInstance, 'info');
253
254                 // Get the file contents
255                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: infoInstance=%s', $infoInstance->__toString()));
256                 try {
257                         $infoArray = $this->getDataArrayFromFile($infoInstance);
258                 } catch (FileNotFoundException $e) {
259                         // Not found, so ignore it here
260                 }
261
262                 // ... and return it
263                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: infoArray()=%d - EXIT!', count($infoArray)));
264                 return $infoArray;
265         }
266
267         /**
268          * Generates a file info class from given dataset instance and string
269          *
270          * @param       $dataSetInstance        An instance of a database set class
271          * @param       $rowName                        Name of the row
272          * @return      $infoInstance           An instance of a SplFileInfo class
273          */
274         private function generateFileFromDataSet (Criteria $dataSetInstance, string $rowName) {
275                 // Instanciate new file object
276                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s,rowName=%s - CALLED!', $dataSetInstance->__toString(), $rowName));
277                 $infoInstance = new SplFileInfo(FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('local_database_path') . $dataSetInstance->getTableName() . DIRECTORY_SEPARATOR . $rowName . '.' . $this->getFileExtension());
278
279                 // Return it
280                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: infoInstance=%s - EXIT!', $infoInstance->__toString()));
281                 return $infoInstance;
282         }
283
284         /**
285          * Creates the table info file from given dataset instance
286          *
287          * @param       $dataSetInstance        An instance of a database set class
288          * @return      void
289          */
290         private function createTableInfoFile (StoreableCriteria $dataSetInstance) {
291                 // Get table name
292                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
293                 $tableName = $dataSetInstance->getTableName();
294
295                 // Get the data out from dataset in a local array
296                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%', $tableName));
297                 $this->tableInfo[$tableName] = [
298                         'primary'      => $dataSetInstance->getPrimaryKey(),
299                         'created'      => time(),
300                         'last_updated' => time(),
301                 ];
302
303                 // Get info instance
304                 $infoInstance = $this->generateFileFromDataSet($dataSetInstance, 'info');
305
306                 // Write the data to the file
307                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Invoking this->writeDataArrayToFqfn(%s,this->tableInfo[%s]()=%d) ...', $infoInstance->__toString(), $tableName, count($this->tableInfo[$tableName])));
308                 $this->writeDataArrayToFqfn($infoInstance, $this->tableInfo[$tableName]);
309
310                 // Trace message
311                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: EXIT!');
312         }
313
314         /**
315          * Updates the table info file from given dataset instance
316          *
317          * @param       $dataSetInstance        An instance of a database set class
318          * @return      void
319          */
320         private function updateTableInfoFile (StoreableCriteria $dataSetInstance) {
321                 // Get table name from criteria
322                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
323                 $tableName = $dataSetInstance->getTableName();
324
325                 // Create FQFN for creating the table information file
326                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%', $tableName));
327                 $infoInstance = $this->generateFileFromDataSet($dataSetInstance, 'info');
328
329                 // Get the data out from dataset in a local array
330                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: infoInstance=%s', $infoInstance->__toString()));
331                 $this->tableInfo[$tableName]['primary']      = $dataSetInstance->getPrimaryKey();
332                 $this->tableInfo[$tableName]['last_updated'] = time();
333
334                 // Write the data to the file
335                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Invoking this->writeDataArrayToFqfn(%s,this->tableInfo[%s]()=%d) ...', $infoInstance->__toString(), $tableName, count($this->tableInfo[$tableName])));
336                 $this->writeDataArrayToFqfn($infoInstance, $this->tableInfo[$tableName]);
337
338                 // Trace message
339                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: EXIT!');
340         }
341
342         /**
343          * Updates the primary key information or creates the table info file if not found
344          *
345          * @param       $dataSetInstance        An instance of a database set class
346          * @return      void
347          */
348         private function updatePrimaryKey (StoreableCriteria $dataSetInstance) {
349                 // Get table name from criteria
350                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
351                 $tableName = $dataSetInstance->getTableName();
352
353                 // Get the information array from lower method
354                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%s', $tableName));
355                 $infoArray = $this->getContentsFromTableInfoFile($dataSetInstance);
356
357                 // Is the primary key there?
358                 //* PRINTR-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput('CACHED-LOCAL-FILE-DATABASE: tableInfo=' . print_r($this->tableInfo, true));
359                 if (!isset($this->tableInfo[$tableName]['primary'])) {
360                         // Then create the info file
361                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('CACHED-LOCAL-FILE-DATABASE: Creating info table for tableName=%s ...', $tableName));
362                         $this->createTableInfoFile($dataSetInstance);
363                 } elseif (FrameworkBootstrap::getConfigurationInstance()->isEnabled('db_update_primary_forced') && $dataSetInstance->getPrimaryKey() != $this->tableInfo[$tableName]['primary']) {
364                         // Set the array element
365                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('CACHED-LOCAL-FILE-DATABASE: Setting primaryKey=%s for tableName=%s ...', $dataSetInstance->getPrimaryKey(), $tableName));
366                         $this->tableInfo[$tableName]['primary'] = $dataSetInstance->getPrimaryKey();
367
368                         // Update the entry
369                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('CACHED-LOCAL-FILE-DATABASE: Updating info table for tableName=%s ...', $tableName));
370                         $this->updateTableInfoFile($dataSetInstance);
371                 }
372
373                 // Trace message
374                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: EXIT!');
375         }
376
377         /**
378          * Makes sure that the database connection is alive
379          *
380          * @return      void
381          * @todo        Do some checks on the database directory and files here
382          */
383         public function connectToDatabase () {
384                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: CALLED!');
385         }
386
387         /**
388          * Starts a SELECT query on the database by given return type, table name
389          * and search criteria
390          *
391          * @param       $tableName                      Name of the database table
392          * @param       $searchInstance         Local search criteria class
393          * @return      $resultData                     Result data of the query
394          * @throws      InvalidArgumentException        If a parameter is not valid
395          * @throws      UnsupportedCriteriaException    If the criteria is unsupported
396          * @throws      SqlException                                    If an 'SQL error' occurs
397          */
398         public function querySelect (string $tableName, LocalSearchCriteria $searchInstance) {
399                 // Validate parameter
400                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%s,searchInstance=%s - CALLED!', $tableName, $searchInstance->__toString()));
401                 if (empty($tableName)) {
402                         // Throw IAE
403                         throw new InvalidArgumentException('Parameter "tableName" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
404                 } elseif (!isset($this->pathNames[$tableName])) {
405                         // "Cache" is not present, so create and assign it
406                         $this->pathNames[$tableName] = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('local_database_path') . $tableName . DIRECTORY_SEPARATOR;
407                 }
408
409                 // The result is null by any errors
410                 $resultData = NULL;
411
412                 /*
413                  * A 'select' query is not that easy on local files, so first try to
414                  * find the 'table' which is in fact a directory on the server
415                  */
416                 try {
417                         // Get a directory pointer instance
418                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Getting directory_class for this->pathNames[%s]=%s ...', $tableName, $this->pathNames[$tableName]));
419                         $directoryInstance = ObjectFactory::createObjectByConfiguredName('directory_class', [$this->pathNames[$tableName]]);
420
421                         // Initialize the result data, this need to be rewritten e.g. if a local file cannot be read
422                         $resultData = [
423                                 BaseDatabaseResult::RESULT_NAME_STATUS    => self::RESULT_OKAY,
424                                 BaseDatabaseResult::RESULT_NAME_ROWS      => [],
425                                 BaseDatabaseResult::RESULT_NAME_EXCEPTION => NULL,
426                         ];
427
428                         // Initialize limit/skip
429                         $limitFound = 0;
430                         $skipFound = 0;
431                         $idx = 1;
432
433                         // Read the directory with some exceptions
434                         while (($fileInfoInstance = $directoryInstance->readDirectoryExcept(['.gitkeep', 'info.' . $this->getFileExtension()])) && (($limitFound < $searchInstance->getLimit()) || ($searchInstance->getLimit() == 0))) {
435                                 // Does the extension match?
436                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->extension=%s,this->fileExtension=%s', $fileInfoInstance->getExtension(), $this->getFileExtension()));
437                                 if ($fileInfoInstance->getExtension() !== $this->getFileExtension()) {
438                                         // Skip this file!
439                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Skipping fileInfoInstance->filename=%s ...', $fileInfoInstance->getFilename()));
440                                         $directoryInstance->getDirectoryIteratorInstance()->next();
441                                         continue;
442                                 }
443
444                                 // Read the file
445                                 $dataArray = $this->getDataArrayFromFile($fileInfoInstance);
446
447                                 // Is this an array?
448                                 //* PRINTR-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->pathname=' . $fileInfoInstance->getPathname() . ',dataArray='.print_r($dataArray, true));
449                                 if (is_array($dataArray)) {
450                                         // Default is nothing found
451                                         $isFound = true;
452
453                                         // Search in the criteria with FMFW (First Matches, First Wins)
454                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: data[]=%d', count($dataArray)));
455                                         foreach ($dataArray as $key => $value) {
456                                                 // Found one entry?
457                                                 $isFound = ($isFound && $searchInstance->isCriteriaMatching($key, $value));
458                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: key=%s,value[%s]=%s,isFound=%s', $key, gettype($value), $value, intval($isFound)));
459                                         }
460
461                                         // Is all found?
462                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: isFound=%d,limitFound=%d,limit=%d', intval($isFound), $limitFound, $searchInstance->getLimit()));
463                                         if ($isFound === true) {
464                                                 // Shall we skip this entry?
465                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: searchInstance->skip=%d,skipFound=%d', $searchInstance->getSkip(), $skipFound));
466                                                 if ($searchInstance->getSkip() > 0 && $skipFound < $searchInstance->getSkip()) {
467                                                         // Skip this entry
468                                                         $skipFound++;
469                                                         break;
470                                                 }
471
472                                                 // Set id number
473                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Setting dataArray[%s]=%d', $this->getIndexKey(), $idx));
474                                                 $dataArray[$this->getIndexKey()] = $idx;
475
476                                                 // Entry found!
477                                                 array_push($resultData[BaseDatabaseResult::RESULT_NAME_ROWS], $dataArray);
478
479                                                 // Count found entries up
480                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: resultData[%s]()=%d', BaseDatabaseResult::RESULT_NAME_ROWS, count($resultData[BaseDatabaseResult::RESULT_NAME_ROWS])));
481                                                 $limitFound++;
482                                         }
483                                 } else {
484                                         // Throw an exception here
485                                         throw new SqlException(array($this, sprintf('File &#39;%s&#39; contains invalid data.', $fileInfoInstance->getPathname()), self::DB_CODE_DATA_FILE_CORRUPT), self::EXCEPTION_SQL_QUERY);
486                                 }
487
488                                 // Count entry up
489                                 $idx++;
490
491                                 // Advance to next entry
492                                 $directoryInstance->getDirectoryIteratorInstance()->next();
493                         }
494
495                         // Close directory and throw the instance away
496                         $directoryInstance->closeDirectory();
497                         unset($directoryInstance);
498
499                         // Reset last exception
500                         $this->resetLastException();
501                 } catch (PathIsNoDirectoryException $e) {
502                         // Path not found means "table not found" for real databases...
503                         $this->setLastException($e);
504
505                         // So throw an SqlException here with faked error message
506                         throw new SqlException (array($this, sprintf('Table &#39;%s&#39; not found', $tableName), self::DB_CODE_TABLE_MISSING), self::EXCEPTION_SQL_QUERY);
507                 } catch (FrameworkException $e) {
508                         // Catch all exceptions and store them in last error
509                         $this->setLastException($e);
510                 }
511
512                 // Return the gathered result
513                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: resultData()=%d - EXIT!', count($resultData)));
514                 return $resultData;
515         }
516
517         /**
518          * "Inserts" a data set instance into a local file database folder
519          *
520          * @param       $dataSetInstance        A storeable data set
521          * @return      void
522          * @throws      SqlException    If an SQL error occurs
523          */
524         public function queryInsertDataSet (StoreableCriteria $dataSetInstance) {
525                 // Try to save the request away
526                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
527                 try {
528                         // Create full path name
529                         $infoInstance = $this->generateFileFromDataSet($dataSetInstance, md5($dataSetInstance->getUniqueValue()));
530
531                         // Write the data away
532                         $this->writeDataArrayToFqfn($infoInstance, $dataSetInstance->getCriteriaArray());
533
534                         // Update the primary key
535                         $this->updatePrimaryKey($dataSetInstance);
536
537                         // Reset last exception
538                         $this->resetLastException();
539                 } catch (FrameworkException $e) {
540                         // Catch all exceptions and store them in last error
541                         $this->setLastException($e);
542
543                         // Throw an SQL exception
544                         throw new SqlException([
545                                         $this,
546                                         sprintf('Cannot write data to table &#39;%s&#39;, is the table created? e=%s,e->message=%s',
547                                                 $dataSetInstance->getTableName(),
548                                                 $e->__toString(),
549                                                 $e->getMessage()
550                                         ),
551                                         self::DB_CODE_TABLE_UNWRITEABLE
552                                 ],
553                                 self::EXCEPTION_SQL_QUERY
554                         );
555                 }
556
557                 // Trace message
558                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: EXIT!');
559         }
560
561         /**
562          * "Updates" a data set instance with a database layer
563          *
564          * @param       $dataSetInstance        An instance of a StorableCriteria class
565          * @return      void
566          * @throws      InvalidArgumentException        If $tableName is empty
567          * @throws      SqlException    If an SQL error occurs
568          */
569         public function queryUpdateDataSet (StoreableCriteria $dataSetInstance) {
570                 // Get table name
571                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: dataSetInstance=%s - CALLED!', $dataSetInstance->__toString()));
572                 $tableName = $dataSetInstance->getTableName();
573
574                 // Is "cache" there?
575                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%s', $tableName));
576                 if (empty($tableName)) {
577                         // Should never be an empty string
578                         throw new InvalidArgumentException('Class field dataSetInstance->tableName is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
579                 } elseif (!isset($this->pathNames[$tableName])) {
580                         // "Cache" is not present, so create and assign it
581                         $this->pathNames[$tableName] = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('local_database_path') . $tableName . DIRECTORY_SEPARATOR;
582                 }
583
584                 // Try all the requests
585                 try {
586                         // Get a file pointer instance
587                         $directoryInstance = ObjectFactory::createObjectByConfiguredName('directory_class', [$this->pathNames[$tableName]]);
588
589                         // Initialize limit/skip
590                         $limitFound = 0;
591                         $skipFound = 0;
592
593                         // Get the criteria array from the dataset
594                         $searchArray = $dataSetInstance->getCriteriaArray();
595
596                         // Get search criteria
597                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: searchArray()=%d', count($searchArray)));
598                         $searchInstance = $dataSetInstance->getSearchInstance();
599
600                         // Read the directory with some exceptions
601                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: searchInstance=%s', $searchInstance->__toString()));
602                         while (($fileInfoInstance = $directoryInstance->readDirectoryExcept(['.gitkeep', 'info.' . $this->getFileExtension()])) && (($limitFound < $searchInstance->getLimit()) || ($searchInstance->getLimit() == 0))) {
603                                 // Does the extension match?
604                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->extension=%s,this->fileExtension=%s', $fileInfoInstance->getExtension(), $this->getFileExtension()));
605                                 if ($fileInfoInstance->getExtension() !== $this->getFileExtension()) {
606                                         // Skip this file!
607                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->extension=%s,this->fileExtension=%s - SKIPPED!', $fileInfoInstance->getExtension(), $this->getFileExtension()));
608                                         $directoryInstance->getDirectoryIteratorInstance()->next();
609                                         continue;
610                                 }
611
612                                 // Open this file for reading
613                                 $dataArray = $this->getDataArrayFromFile($fileInfoInstance);
614
615                                 // Is this an array?
616                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->pathname=' . $fileInfoInstance->getPathname() . ',dataArray='.print_r($dataArray, true));
617                                 if (is_array($dataArray)) {
618                                         // Default is nothing found
619                                         $isFound = true;
620
621                                         // Search in the criteria with FMFW (First Matches, First Wins)
622                                         foreach ($dataArray as $key => $value) {
623                                                 // Found one entry?
624                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: isFound=%d,key=%s,value[]=%s', intval($isFound), $key, gettype($value)));
625                                                 $isFound = (($isFound === true) && ($searchInstance->isCriteriaMatching($key, $value)));
626                                         }
627
628                                         // Is all found?
629                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: isFound=%s', intval($isFound)));
630                                         if ($isFound === true) {
631                                                 // Shall we skip this entry?
632                                                 if ($searchInstance->getSkip() > 0) {
633                                                         // We shall skip some entries
634                                                         if ($skipFound < $searchInstance->getSkip()) {
635                                                                 // Skip this entry
636                                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: Found entry, but skipping ...');
637                                                                 $skipFound++;
638                                                                 break;
639                                                         }
640                                                 }
641
642                                                 // Entry found, so update it
643                                                 foreach ($searchArray as $searchKey => $searchValue) {
644                                                         // Make sure the value is not bool again
645                                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: searchKey=%s,searchValue[]=%s', $searchKey, gettype($searchValue)));
646                                                         assert(!is_bool($searchValue));
647                                                         assert($searchKey != $this->getIndexKey());
648
649                                                         // Debug message + add/update it
650                                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: criteriaKey=%s,criteriaValue[%s]=%s', $searchKey, gettype($searchValue), $searchValue));
651                                                         $dataArray[$searchKey] = $searchValue;
652                                                 }
653
654                                                 // Write the data to a local file
655                                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: Writing data()=%d to %s ...', count($dataArray), $fileInfoInstance->getPathname()));
656                                                 $this->writeDataArrayToFqfn($fileInfoInstance, $dataArray);
657
658                                                 // Count found entries up
659                                                 $limitFound++;
660                                         }
661                                 }
662
663                                 // Advance to next entry
664                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: Invoking directoryInstance->directoryIteratorInstance->next() ...');
665                                 $directoryInstance->getDirectoryIteratorInstance()->next();
666                         }
667
668                         // Close the file pointer
669                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: Invoking directoryInstance->closeDirectory() ...');
670                         $directoryInstance->closeDirectory();
671
672                         // Update the primary key
673                         $this->updatePrimaryKey($dataSetInstance);
674
675                         // Reset last exception
676                         $this->resetLastException();
677                 } catch (FrameworkException $e) {
678                         // Catch all exceptions and store them in last error
679                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: e=%s,e->code=%d,e->message=%s', $e->__toString(), $e->getCode(), $e->getMessage()));
680                         $this->setLastException($e);
681
682                         // Throw an SQL exception
683                         throw new SqlException(array($this, sprintf('Cannot write data to table &#39;%s&#39;, is the table created? Exception: %s, message:%s', $dataSetInstance->getTableName(), $e->__toString(), $e->getMessage()), self::DB_CODE_TABLE_UNWRITEABLE), self::EXCEPTION_SQL_QUERY);
684                 }
685
686                 // Trace message
687                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('CACHED-LOCAL-FILE-DATABASE: EXIT!');
688         }
689
690         /**
691          * Getter for primary key of specified table or if not found null will be
692          * returned. This must be database-specific.
693          *
694          * @param       $tableName              Name of the table we need the primary key from
695          * @return      $primaryKey             Primary key column of the given table
696          * @throws      InvalidArgumentException        If a parameter is not valid
697          * @todo        Rename method to getPrimaryKeyFromTableInfo()
698          */
699         public function getPrimaryKeyOfTable (string $tableName) {
700                 // Validate parameter
701                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%s - CALLED!', $tableName));
702                 if (empty($tableName)) {
703                         // Throw IAE
704                         throw new InvalidArgumentException('Parameter "tableName" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
705                 }
706
707                 // Default key is null
708                 $primaryKey = NULL;
709
710                 // Does the table information exist?
711                 if (isset($this->tableInfo[$tableName])) {
712                         // Then return the primary key
713                         $primaryKey = $this->tableInfo[$tableName]['primary'];
714                 }
715
716                 // Return the column
717                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: primaryKey[%s]=%s - EXIT!', gettype($primaryKey), $primaryKey));
718                 return $primaryKey;
719         }
720
721         /**
722          * Removes non-public data from given array.
723          *
724          * @param       $data   An array with possible non-public data that needs to be removed.
725          * @return      $data   A cleaned up array with only public data.
726          * @throws      BadMethodCallException  If some non-public is already not found but method was called
727          * @todo        Add more generic non-public data for removal
728          */
729         public function removeNonPublicDataFromArray (array $data) {
730                 // Check parameter
731                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: data()=%d - CALLED!', count($data)));
732                 if (!isset($data[$this->getIndexKey()])) {
733                         // Throw BMCE
734                         throw new BadMethodCallException(sprintf('data()=%d does not contain this->indexKey=%s', count($data), $this->getIndexKey()));
735                 }
736
737                 // Remove '__idx'
738                 unset($data[$this->getIndexKey()]);
739
740                 // Return it
741                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: data()=%d - EXIT!', count($data)));
742                 return $data;
743         }
744
745         /**
746          * Counts total rows of given table
747          *
748          * @param       $tableName      Table name
749          * @return      $count          Total rows of given table
750          * @throws      InvalidArgumentException        If a parameter is not valid
751          */
752         public function countTotalRows (string $tableName) {
753                 // Validate parameter
754                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: tableName=%s - CALLED!', $tableName));
755                 if (empty($tableName)) {
756                         // Throw IAE
757                         throw new InvalidArgumentException('Parameter "tableName" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
758                 } elseif (!isset($this->pathNames[$tableName])) {
759                         // "Cache" is not present, so create and assign it
760                         $this->pathNames[$tableName] = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('local_database_path') . $tableName . DIRECTORY_SEPARATOR;
761                 }
762
763                 // Try all the requests
764                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: this->pathNames[%s]=%s', $tableName, $this->pathNames[$tableName]));
765                 try {
766                         // Get a file pointer instance
767                         $directoryInstance = ObjectFactory::createObjectByConfiguredName('directory_class', [$this->pathNames[$tableName]]);
768
769                         // Initialize counter
770                         $count = 0;
771
772                         // Read the directory with some exceptions
773                         while ($fileInfoInstance = $directoryInstance->readDirectoryExcept(['.gitkeep', 'info.' . $this->getFileExtension()])) {
774                                 // Does the extension match?
775                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->extension=' . $fileInfoInstance->getExtension() . ',this->getFileExtension()=' . $this->getFileExtension());
776                                 if ($fileInfoInstance->getExtension() !== $this->getFileExtension()) {
777                                         // Debug message
778                                         //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->extension=' . $fileInfoInstance->getExtension() . ',getFileExtension()=' . $this->getFileExtension() . ' - SKIPPED!');
779                                         // Skip this file!
780                                         continue;
781                                 }
782
783                                 // Count this row up
784                                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage('CACHED-LOCAL-FILE-DATABASE: fileInfoInstance->pathname=' . $fileInfoInstance->getPathname() . ',getFileExtension()=' . $this->getFileExtension() . ' - COUNTED!');
785                                 $count++;
786                         }
787                 } catch (FrameworkException $e) {
788                         // Catch all exceptions and store them in last error
789                         $this->setLastException($e);
790
791                         // Throw an SQL exception
792                         throw new SqlException(array($this, sprintf('Cannot count on table &#39;%s&#39;, is the table created?', $dataSetInstance->getTableName()), self::DB_CODE_TABLE_NOT_FOUND), self::EXCEPTION_SQL_QUERY);
793                 }
794
795                 // Return count
796                 //* NOISY-DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('CACHED-LOCAL-FILE-DATABASE: count=%d - EXIT!', $count));
797                 return $count;
798         }
799
800 }