DataloadService   C
last analyzed

Complexity

Total Complexity 53

Size/Duplication

Total Lines 535
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 223
dl 0
loc 535
rs 6.96
c 0
b 0
f 0
wmc 53

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getDataFromDatasource() 0 32 3
A copy() 0 3 1
A delete() 0 3 1
A read() 0 3 1
A create() 0 3 1
A update() 0 9 2
A executeBySchedule() 0 9 3
A __construct() 0 25 1
C execute() 0 104 13
A detectDelimiter() 0 14 3
B importClipboard() 0 48 7
A getDatasetId() 0 9 2
A importFile() 0 31 4
A updateData() 0 30 3
A floatvalue() 0 15 4
A deleteData() 0 12 2
A deleteDataSimulate() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like DataloadService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataloadService, and based on these observations, apply Extract Interface, too.

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