Passed
Push — master ( 952c0a...1807d7 )
by Marcel
07:54 queued 01:08
created

DataloadService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 11
dl 0
loc 25
rs 9.9
c 1
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * Analytics
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the LICENSE.md file.
7
 *
8
 * @author Marcel Scherello <[email protected]>
9
 * @copyright 2019-2022 Marcel Scherello
10
 */
11
12
namespace OCA\Analytics\Service;
13
14
use Exception;
15
use OCA\Analytics\Activity\ActivityManager;
16
use OCA\Analytics\Controller\DatasourceController;
17
use OCA\Analytics\Notification\NotificationManager;
18
use OCA\Analytics\Db\DataloadMapper;
19
use OCP\AppFramework\Http\NotFoundResponse;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Http\NotFoundResponse was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use OCP\Files\NotFoundException;
0 ignored issues
show
Bug introduced by
The type OCP\Files\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use OCP\IL10N;
0 ignored issues
show
Bug introduced by
The type OCP\IL10N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use Psr\Log\LoggerInterface;
0 ignored issues
show
Bug introduced by
The type Psr\Log\LoggerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
24
class DataloadService
25
{
26
    private $userId;
27
    private $logger;
28
    private $StorageService;
29
    private $DatasourceController;
30
    private $ActivityManager;
31
    private $ReportService;
32
    private $DatasetService;
33
    private $VariableService;
34
    private $l10n;
35
    private $DataloadMapper;
36
    private $NotificationManager;
37
38
    public function __construct(
39
        $userId,
40
        IL10N $l10n,
41
        LoggerInterface $logger,
42
        ActivityManager $ActivityManager,
43
        DatasourceController $DatasourceController,
44
        ReportService $ReportService,
45
        DatasetService $DatasetService,
46
        StorageService $StorageService,
47
        VariableService $VariableService,
48
        NotificationManager $NotificationManager,
49
        DataloadMapper $DataloadMapper
50
    )
51
    {
52
        $this->userId = $userId;
53
        $this->l10n = $l10n;
54
        $this->logger = $logger;
55
        $this->StorageService = $StorageService;
56
        $this->ActivityManager = $ActivityManager;
57
        $this->DatasourceController = $DatasourceController;
58
        $this->ReportService = $ReportService;
59
        $this->DatasetService = $DatasetService;
60
        $this->VariableService = $VariableService;
61
        $this->NotificationManager = $NotificationManager;
62
        $this->DataloadMapper = $DataloadMapper;
63
    }
64
65
    // Data loads
66
    // Data loads
67
    // Data loads
68
69
    /**
70
     * create a new data load
71
     *
72
     * @param $datasetId
73
     * @param $reportId
74
     * @param int $datasourceId
75
     * @return int
76
     * @throws \OCP\DB\Exception
77
     */
78
    public function create($datasetId, int $datasourceId)
79
    {
80
        return $this->DataloadMapper->create((int)$datasetId, $datasourceId);
81
    }
82
83
    /**
84
     * get all data loads for a dataset or report
85
     *
86
     * @param int $datasetId
87
     * @param $reportId
88
     * @return array
89
     */
90
    public function read($datasetId)
91
    {
92
        return $this->DataloadMapper->read((int)$datasetId);
93
    }
94
95
    /**
96
     * update data load
97
     *
98
     * @param int $dataloadId
99
     * @param $name
100
     * @param $option
101
     * @param $schedule
102
     * @return bool
103
     */
104
    public function update(int $dataloadId, $name, $option, $schedule)
105
    {
106
        $array = json_decode($option, true);
107
        foreach ($array as $key => $value) {
108
            $array[$key] = htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8');
109
        }
110
        $option = json_encode($array);
111
112
        return $this->DataloadMapper->update($dataloadId, $name, $option, $schedule);
113
    }
114
115
    /**
116
     * copy a data load
117
     *
118
     * @param int $dataloadId
119
     * @return bool
120
     */
121
    public function copy(int $dataloadId)
122
    {
123
        return $this->DataloadMapper->copy($dataloadId);
124
    }
125
126
    /**
127
     * delete a data load
128
     *
129
     * @param int $dataloadId
130
     * @return bool
131
     */
132
    public function delete(int $dataloadId)
133
    {
134
        return $this->DataloadMapper->delete($dataloadId);
135
    }
136
137
    /**
138
     * execute all data loads depending on their schedule
139
     * Daily or Hourly
140
     *
141
     * @param $schedule
142
     * @return void
143
     * @throws Exception
144
     */
145
    public function executeBySchedule($schedule)
146
    {
147
        $schedules = $this->DataloadMapper->getDataloadBySchedule($schedule);
148
        foreach ($schedules as $dataload) {
149
            $result = $this->execute($dataload['id']);
150
            if ($result['error'] !== 0) {
151
                // if the data source produced an error, a notification needs to be triggered
152
                $dataset = $this->DatasetService->read($dataload['dataset']);
153
                $this->NotificationManager->triggerNotification(NotificationManager::DATALOAD_ERROR, $dataload['dataset'], $dataload['id'], ['dataloadName' => $dataload['name'], 'datasetName' => $dataset['name']], $dataload['user_id']);
154
            }
155
        }
156
    }
157
158
    /**
159
     * execute a data load from data source and store into dataset
160
     *
161
     * @param int $dataloadId
162
     * @return array
163
     * @throws Exception
164
     */
165
    public function execute(int $dataloadId)
166
    {
167
        $bulkSize = 500;
168
        $insert = $update = $error = $delete = $currentCount = 0;
169
        $bulkInsert = null;
170
        $aggregation = null;
171
172
        // get the data from the datasource
173
        $result = $this->getDataFromDatasource($dataloadId);
174
175
        // dont continue in case of datasource error
176
        if ($result['error'] !== 0) {
177
            return [
178
                'insert' => $insert,
179
                'update' => $update,
180
                'delete' => $delete,
181
                'error' => 1,
182
            ];
183
        }
184
185
        // get the meta data
186
        $dataloadMetadata = $this->DataloadMapper->getDataloadById($dataloadId);
187
        $option = json_decode($dataloadMetadata['option'], true);
188
        $datasetId = $dataloadMetadata['dataset'];
189
190
        // this is a deletion request. Just run the deletion and stop after that with a return.
191
        if ($dataloadMetadata['datasource'] === 0) {
192
            // deletion jobs are using the same dimension/option/value settings a report filter
193
            $filter = array();
194
            $filter['filteroptions'] = '{"filter":{"' . $option['filterDimension'] . '":{"option":"' . $option['filterOption'] . '","value":"' . $option['filterValue'] . '"}}}';
195
            // Text variables like %xx% could be in use here
196
            $filter = $this->VariableService->replaceFilterVariables($filter);
197
198
            $records = $this->StorageService->deleteWithFilter($dataloadMetadata['dataset'], json_decode($filter['filteroptions'], true));
199
200
            return [
201
                'insert' => $insert,
202
                'update' => $update,
203
                'delete' => $records,
204
                'error' => $error,
205
            ];
206
        }
207
208
        // "delete all date before loading" is true in the data source options
209
        // in this case, bulkInsert is additionally set to true. Then no further checks for existing records are needed
210
        // to reduce db selects
211
        if (isset($option['delete']) and $option['delete'] === 'true') {
212
            $this->StorageService->delete($datasetId, '*', '*', $dataloadMetadata['user_id']);
213
            $bulkInsert = true;
214
        }
215
216
        // if the data set has no data, it is the same as the delete all option
217
        // in this case, bulkInsert is set to true. Then no further checks for existing records are needed
218
        // to reduce db selects
219
        $numberOfRecords = $this->StorageService->getRecordCount($datasetId, $dataloadMetadata['user_id']);
220
        if ($numberOfRecords['count'] === 0) {
221
            $bulkInsert = true;
222
        }
223
224
        // Feature not yet available
225
        if (isset($option['aggregation']) and $option['aggregation'] !== 'overwrite') {
226
            $aggregation = $option['aggregation'];
227
        }
228
229
        // collect mass updates to reduce statements to the database
230
        $this->DataloadMapper->beginTransaction();
231
        foreach ($result['data'] as $row) {
232
            // only one column is not possible
233
            if (count($row) === 1) {
234
                $this->logger->info('loading data with only one column is not possible. This is a data load for the dataset: ' . $datasetId);
235
                $error = $error + 1;
236
                continue;
237
            }
238
            // if data source only delivers 2 columns, the value needs to be in the last one
239
            if (count($row) === 2) {
240
                $row[2] = $row[1];
241
                $row[1] = null;
242
            }
243
244
            $action = $this->StorageService->update($datasetId, $row[0], $row[1], $row[2], $dataloadMetadata['user_id'], $bulkInsert, $aggregation);
245
            $insert = $insert + $action['insert'];
246
            $update = $update + $action['update'];
247
            $error = $error + $action['error'];
248
249
            if ($currentCount % $bulkSize === 0) {
250
                $this->DataloadMapper->commit();
251
                $this->DataloadMapper->beginTransaction();
252
            }
253
            if ($action['error'] === 0) $currentCount++;
254
        }
255
        $this->DataloadMapper->commit();
256
257
        $result = [
258
            'insert' => $insert,
259
            'update' => $update,
260
            'delete' => $delete,
261
            'error' => $error,
262
        ];
263
264
        $this->ActivityManager->triggerEvent($datasetId, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_DATALOAD, $dataloadMetadata['user_id']);
265
        return $result;
266
    }
267
268
    /**
269
     * get the data from datasource
270
     * to be used in simulation or execution
271
     *
272
     * @param int $dataloadId
273
     * @return array|NotFoundResponse
274
     * @throws NotFoundResponse
275
     * @throws \OCP\DB\Exception
276
     */
277
    public function getDataFromDatasource(int $dataloadId)
278
    {
279
        $dataloadMetadata = $this->DataloadMapper->getDataloadById($dataloadId);
280
281
        if (!empty($dataloadMetadata)) {
282
283
            if ($dataloadMetadata['datasource'] !== 0) {
284
                $dataloadMetadata['link'] = $dataloadMetadata['option']; //remap until data source table is renamed link=>option
285
286
                $result = $this->DatasourceController->read((int)$dataloadMetadata['datasource'], $dataloadMetadata);
287
                $result['datasetId'] = $dataloadMetadata['dataset'];
288
            } else {
289
                // this is a deletion request. Just run the simulation and return the possible row count in the expected result array
290
                $option = json_decode($dataloadMetadata['option'], true);
291
292
                // deletion jobs are using the same dimension/option/value settings a report filter
293
                $filter = array();
294
                $filter['filteroptions'] = '{"filter":{"' . $option['filterDimension'] . '":{"option":"' . $option['filterOption'] . '","value":"' . $option['filterValue'] . '"}}}';
295
                // Text variables like %xx% could be in use here
296
                $filter = $this->VariableService->replaceFilterVariables($filter);
297
298
                $result = [
299
                    'header' => '',
300
                    'dimensions' => '',
301
                    'data' => $this->StorageService->deleteWithFilterSimulate($dataloadMetadata['dataset'], json_decode($filter['filteroptions'], true)),
302
                    'error' => 0,
303
                ];
304
            }
305
306
            return $result;
307
        } else {
308
            return new NotFoundResponse();
309
        }
310
    }
311
312
    // Data Manipulation
313
    // Data Manipulation
314
    // Data Manipulation
315
316
    /**
317
     * update data from input form
318
     *
319
     * @NoAdminRequired
320
     * @param int $objectId
321
     * @param $dimension1
322
     * @param $dimension2
323
     * @param $value
324
     * @param bool $isDataset
325
     * @return array|false
326
     * @throws \OCP\DB\Exception
327
     */
328
    public function updateData(int $objectId, $dimension1, $dimension2, $value, bool $isDataset)
329
    {
330
        $dataset = $this->getDatasetId($objectId, $isDataset);
331
332
        if ($dataset != '') {
333
            $insert = $update = $errorMessage = 0;
334
            $action = array();
335
            $value = $this->floatvalue($value);
336
            if ($value === false) {
337
                $errorMessage = $this->l10n->t('3rd field must be a valid number');
338
            } else {
339
                $action = $this->StorageService->update($dataset, $dimension1, $dimension2, $value);
340
                $insert = $insert + $action['insert'];
341
                $update = $update + $action['update'];
342
            }
343
344
            $result = [
345
                'insert' => $insert,
346
                'update' => $update,
347
                'error' => $errorMessage,
348
                'validate' => $action['validate'],
349
            ];
350
351
            if ($errorMessage === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD);
352
            return $result;
353
        } else {
354
            return false;
355
        }
356
    }
357
358
    /**
359
     * Simulate delete data from input form
360
     *
361
     * @NoAdminRequired
362
     * @param int $objectId
363
     * @param $dimension1
364
     * @param $dimension2
365
     * @param bool $isDataset
366
     * @return array|false
367
     * @throws \OCP\DB\Exception
368
     */
369
    public function deleteDataSimulate(int $objectId, $dimension1, $dimension2, bool $isDataset)
370
    {
371
        $dataset = $this->getDatasetId($objectId, $isDataset);
372
        if ($dataset != '') {
373
            $result = $this->StorageService->deleteSimulate($dataset, $dimension1, $dimension2);
374
            return ['delete' => $result];
375
        } else {
376
            return false;
377
        }
378
    }
379
380
    /**
381
     * delete data from input form
382
     *
383
     * @NoAdminRequired
384
     * @param int $objectId
385
     * @param $dimension1
386
     * @param $dimension2
387
     * @param bool $isDataset
388
     * @return array|false
389
     * @throws \OCP\DB\Exception
390
     */
391
    public function deleteData(int $objectId, $dimension1, $dimension2, bool $isDataset)
392
    {
393
        $dataset = $this->getDatasetId($objectId, $isDataset);
394
        if ($dataset != '') {
395
            $result = $this->StorageService->delete($dataset, $dimension1, $dimension2);
396
            return ['delete' => $result];
397
        } else {
398
            return false;
399
        }
400
    }
401
402
    /**
403
     * Import clipboard data
404
     *
405
     * @NoAdminRequired
406
     * @param int $objectId
407
     * @param $import
408
     * @param bool $isDataset
409
     * @return array|false
410
     * @throws \OCP\DB\Exception
411
     */
412
    public function importClipboard($objectId, $import, bool $isDataset)
413
    {
414
        $dataset = $this->getDatasetId($objectId, $isDataset);
415
        if ($dataset != '') {
416
            $insert = $update = $errorMessage = $errorCounter = 0;
417
            $delimiter = '';
418
419
            if ($import === '') {
420
                $errorMessage = $this->l10n->t('No data');
421
            } else {
422
                $delimiter = $this->detectDelimiter($import);
423
                $rows = str_getcsv($import, "\n");
424
425
                foreach ($rows as &$row) {
426
                    $row = str_getcsv($row, $delimiter);
427
                    $numberOfColumns = count($row);
428
                    // last column needs to be a float
429
                    $row[2] = $this->floatvalue($row[$numberOfColumns - 1]);
430
                    if ($row[2] === false) {
431
                        $errorCounter++;
432
                    } else {
433
                        if ($numberOfColumns < 3) $row[1] = null;
434
                        $action = $this->StorageService->update($dataset, $row[0], $row[1], $row[2]);
435
                        $insert = $insert + $action['insert'];
436
                        $update = $update + $action['update'];
437
                    }
438
                    if ($errorCounter === 2) {
439
                        // first error is ignored; might be due to header row
440
                        $errorMessage = $this->l10n->t('Last field must be a valid number');
441
                        break;
442
                    }
443
                }
444
            }
445
446
            $result = [
447
                'insert' => $insert,
448
                'update' => $update,
449
                'delimiter' => $delimiter,
450
                'error' => $errorMessage,
451
            ];
452
453
            if ($errorMessage === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_IMPORT);
454
            return $result;
455
        } else {
456
            return false;
457
        }
458
    }
459
460
    /**
461
     * Import data into dataset from an internal or external file
462
     *
463
     * @NoAdminRequired
464
     * @param int $objectId
465
     * @param $path
466
     * @param bool $isDataset
467
     * @return array|false
468
     * @throws \OCP\DB\Exception
469
     */
470
    public function importFile(int $objectId, $path, bool $isDataset)
471
    {
472
        $dataset = $this->getDatasetId($objectId, $isDataset);
473
        if ($dataset != '') {
474
            $insert = $update = 0;
475
            $reportMetadata = array();
476
            $reportMetadata['link'] = $path;
477
            $reportMetadata['user_id'] = $this->userId;
478
            $result = $this->DatasourceController->read(DatasourceController::DATASET_TYPE_FILE, $reportMetadata);
479
480
            if ($result['error'] === 0) {
481
                foreach ($result['data'] as &$row) {
482
                    $action = $this->StorageService->update($dataset, $row[0], $row[1], $row[2]);
483
                    $insert = $insert + $action['insert'];
484
                    $update = $update + $action['update'];
485
                }
486
            }
487
488
            $result = [
489
                'insert' => $insert,
490
                'update' => $update,
491
                'error' => $result['error'],
492
            ];
493
494
            if ($result['error'] === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_IMPORT);
495
            return $result;
496
        } else {
497
            return false;
498
        }
499
    }
500
501
    private function getDatasetId($objectId, bool $isDataset)
502
    {
503
        if ($isDataset) {
504
            $dataset = $objectId;
505
        } else {
506
            $reportMetadata = $this->ReportService->read($objectId);
507
            $dataset = (int)$reportMetadata['dataset'];
508
        }
509
        return $dataset;
510
    }
511
512
    private function detectDelimiter($data): string
513
    {
514
        $delimiters = ["\t", ";", "|", ","];
515
        $data_2 = array();
516
        $delimiter = $delimiters[0];
517
        foreach ($delimiters as $d) {
518
            $firstRow = str_getcsv($data, "\n")[0];
519
            $data_1 = str_getcsv($firstRow, $d);
520
            if (sizeof($data_1) > sizeof($data_2)) {
521
                $delimiter = $d;
522
                $data_2 = $data_1;
523
            }
524
        }
525
        return $delimiter;
526
    }
527
528
    private function floatvalue($val)
529
    {
530
        // if value is a 3 digit comma number with one leading zero like 0,111, it should not go through the 1000 separator removal
531
        if (preg_match('/(?<=\b0)\,(?=\d{3}\b)/', $val) === 0 && preg_match('/(?<=\b0)\.(?=\d{3}\b)/', $val) === 0) {
532
            // remove , as 1000 separator
533
            $val = preg_replace('/(?<=\d)\,(?=\d{3}\b)/', '', $val);
534
            // remove . as 1000 separator
535
            $val = preg_replace('/(?<=\d)\.(?=\d{3}\b)/', '', $val);
536
        }
537
        // convert remaining comma to decimal point
538
        $val = str_replace(",", ".", $val);
539
        if (is_numeric($val)) {
540
            return number_format(floatval($val), 2, '.', '');
541
        } else {
542
            return false;
543
        }
544
    }
545
546
}