Passed
Push — master ( 1876ce...d1e05a )
by Marcel
03:09
created

DataloadService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 11
dl 0
loc 25
rs 9.9
c 2
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;
20
use OCP\Files\NotFoundException;
21
use OCP\IL10N;
22
use Psr\Log\LoggerInterface;
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
     * delete a data load
117
     *
118
     * @param int $dataloadId
119
     * @return bool
120
     */
121
    public function delete(int $dataloadId)
122
    {
123
        return $this->DataloadMapper->delete($dataloadId);
124
    }
125
126
    /**
127
     * execute all data loads depending on their schedule
128
     * Daily or Hourly
129
     *
130
     * @param $schedule
131
     * @return void
132
     * @throws Exception
133
     */
134
    public function executeBySchedule($schedule)
135
    {
136
        $schedules = $this->DataloadMapper->getDataloadBySchedule($schedule);
137
        foreach ($schedules as $dataload) {
138
            $result = $this->execute($dataload['id']);
139
            if ($result['error'] !== 0) {
140
                // if the data source produced an error, a notification needs to be triggered
141
                $dataset = $this->DatasetService->read($dataload['dataset']);
142
                $this->NotificationManager->triggerNotification(NotificationManager::DATALOAD_ERROR, $dataload['dataset'], $dataload['id'], ['dataloadName' => $dataload['name'], 'datasetName' => $dataset['name']], $dataload['user_id']);
143
            }
144
        }
145
    }
146
147
    /**
148
     * execute a data load from data source and store into dataset
149
     *
150
     * @param int $dataloadId
151
     * @return array
152
     * @throws Exception
153
     */
154
    public function execute(int $dataloadId)
155
    {
156
        $bulkSize = 500;
157
        $insert = $update = $error = $delete = 0;
158
        $bulkInsert = null;
159
        $aggregation = null;
160
161
        // get the meta data
162
        $dataloadMetadata = $this->DataloadMapper->getDataloadById($dataloadId);
163
        $option = json_decode($dataloadMetadata['option'], true);
164
165
        // get the data from the datasource
166
        $result = $this->getDataFromDatasource($dataloadId);
167
        $datasetId = $result['datasetId'];
168
169
        // dont continue in case of datasource error
170
        if ($result['error'] !== 0) {
171
            return [
172
                'insert' => $insert,
173
                'update' => $update,
174
                'delete' => $delete,
175
                'error' => 1,
176
            ];
177
        }
178
179
        // this is a deletion request. Just run the deletion and stop after that with a return.
180
        if ($dataloadMetadata['datasource'] === 0) {
181
            $option = json_decode($dataloadMetadata['option'], true);
182
183
            // deletion jobs are using the same dimension/option/value settings a report filter
184
            $filter['filteroptions'] = '{"filter":{"' . $option['filterDimension'] . '":{"option":"' . $option['filterOption'] . '","value":"' . $option['filterValue'] . '"}}}';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$filter was never initialized. Although not strictly required by PHP, it is generally a good practice to add $filter = array(); before regardless.
Loading history...
185
            // Text variables like %xx% could be in use here
186
            $filter = $this->VariableService->replaceFilterVariables($filter);
187
188
            $this->StorageService->deleteWithFilterSimulate($dataloadMetadata['dataset'], json_decode($filter['filteroptions'], true));
189
190
            return [
191
                'insert' => $insert,
192
                'update' => $update,
193
                // use the existing row count from the deletion simulation which was run during getDataFromDatasource
194
                'delete' => $result['data']['count'],
195
                'error' => $error,
196
            ];
197
        }
198
199
        // "delete all date before loading" is true in the data source options
200
        // in this case, bulkInsert is set to true. Then no further checks for existing records are needed
201
        // reduce db selects
202
        if (isset($option['delete']) and $option['delete'] === 'true') {
203
            $this->StorageService->delete($datasetId, '*', '*', $dataloadMetadata['user_id']);
204
            $bulkInsert = true;
205
        }
206
207
        // if the data set has no data, it is the same as the delete all option
208
        // in this case, bulkInsert is set to true. Then no further checks for existing records are needed
209
        // reduce db selects
210
        $numberOfRecords = $this->StorageService->getRecordCount($datasetId);
211
        if ($numberOfRecords['count'] === 0) {
212
            $this->logger->info('Empty dataset => enabling bulk load');
213
            $bulkInsert = true;
214
        }
215
216
        // Feature not yet available
217
        if (isset($option['aggregation']) and $option['aggregation'] !== 'overwrite') {
218
            $aggregation = $option['aggregation'];
219
        }
220
221
        // collect mass updates to reduce statements to the database
222
        $this->DataloadMapper->beginTransaction();
223
        $currentCount = 0;
224
        foreach ($result['data'] as $row) {
225
            if (count($row) === 1) {
226
                // only one column is not possible
227
                $this->logger->info('loading data with only one column is not possible. This is a data load for the dataset: ' . $datasetId);
228
                $error = $error + 1;
229
                continue;
230
            }
231
            if (count($row) === 2) {
232
                // if data source only delivers 2 columns, the value needs to be in the last one
233
                $row[2] = $row[1];
234
                $row[1] = null;
235
            }
236
            $action = $this->StorageService->update($datasetId, $row[0], $row[1], $row[2], $dataloadMetadata['user_id'], $bulkInsert, $aggregation);
237
            $insert = $insert + $action['insert'];
238
            $update = $update + $action['update'];
239
            $error = $error + $action['error'];
240
241
            if ($currentCount % $bulkSize === 0) {
242
                $this->DataloadMapper->commit();
243
                $this->DataloadMapper->beginTransaction();
244
            }
245
            if ($action['error'] === 0) $currentCount++;
246
        }
247
        $this->DataloadMapper->commit();
248
249
        $result = [
250
            'insert' => $insert,
251
            'update' => $update,
252
            'delete' => $delete,
253
            'error' => $error,
254
        ];
255
256
        $this->ActivityManager->triggerEvent($datasetId, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_DATALOAD, $dataloadMetadata['user_id']);
257
        return $result;
258
    }
259
260
    /**
261
     * get the data from datasource
262
     * to be used in simulation or execution
263
     *
264
     * @param int $dataloadId
265
     * @return array|NotFoundResponse
266
     * @throws NotFoundResponse|NotFoundException
267
     * @throws \OCP\DB\Exception
268
     */
269
    public function getDataFromDatasource(int $dataloadId)
270
    {
271
        $dataloadMetadata = $this->DataloadMapper->getDataloadById($dataloadId);
272
273
        if (!empty($dataloadMetadata)) {
274
275
            if ($dataloadMetadata['datasource'] !== 0) {
276
                $dataloadMetadata['link'] = $dataloadMetadata['option']; //remap until data source table is renamed link=>option
277
                $result = $this->DatasourceController->read((int)$dataloadMetadata['datasource'], $dataloadMetadata);
278
                $result['datasetId'] = $dataloadMetadata['dataset'];
279
            } else {
280
                // this is a deletion request. Just run the simulation and return the possible row count in the expected result array
281
                $option = json_decode($dataloadMetadata['option'], true);
282
283
                // deletion jobs are using the same dimension/option/value settings a report filter
284
                $filter['filteroptions'] = '{"filter":{"' . $option['filterDimension'] . '":{"option":"' . $option['filterOption'] . '","value":"' . $option['filterValue'] . '"}}}';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$filter was never initialized. Although not strictly required by PHP, it is generally a good practice to add $filter = array(); before regardless.
Loading history...
285
                // Text variables like %xx% could be in use here
286
                $filter = $this->VariableService->replaceFilterVariables($filter);
287
288
                $result = [
289
                    'header' => '',
290
                    'dimensions' => '',
291
                    'data' => $this->StorageService->deleteWithFilterSimulate($dataloadMetadata['dataset'], json_decode($filter['filteroptions'], true)),
292
                    'error' => 0,
293
                ];
294
            }
295
296
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type OCP\Files\NotFoundException which is incompatible with the documented return type OCP\AppFramework\Http\NotFoundResponse|array.
Loading history...
297
        } else {
298
            return new NotFoundResponse();
299
        }
300
    }
301
302
    // Data Manipulation
303
    // Data Manipulation
304
    // Data Manipulation
305
306
    /**
307
     * update data from input form
308
     *
309
     * @NoAdminRequired
310
     * @param int $objectId
311
     * @param $dimension1
312
     * @param $dimension2
313
     * @param $value
314
     * @param bool $isDataset
315
     * @return array|false
316
     * @throws \OCP\DB\Exception
317
     */
318
    public function updateData(int $objectId, $dimension1, $dimension2, $value, bool $isDataset)
319
    {
320
        $dataset = $this->getDatasetId($objectId, $isDataset);
321
322
        if ($dataset != '') {
323
            $insert = $update = $errorMessage = 0;
324
            $action = array();
325
            $value = $this->floatvalue($value);
326
            if ($value === false) {
327
                $errorMessage = $this->l10n->t('3rd field must be a valid number');
328
            } else {
329
                $action = $this->StorageService->update($dataset, $dimension1, $dimension2, $value);
330
                $insert = $insert + $action['insert'];
331
                $update = $update + $action['update'];
332
            }
333
334
            $result = [
335
                'insert' => $insert,
336
                'update' => $update,
337
                'error' => $errorMessage,
338
                'validate' => $action['validate'],
339
            ];
340
341
            if ($errorMessage === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD);
342
            return $result;
343
        } else {
344
            return false;
345
        }
346
    }
347
348
    /**
349
     * Simulate delete data from input form
350
     *
351
     * @NoAdminRequired
352
     * @param int $objectId
353
     * @param $dimension1
354
     * @param $dimension2
355
     * @param bool $isDataset
356
     * @return array|false
357
     * @throws \OCP\DB\Exception
358
     */
359
    public function deleteDataSimulate(int $objectId, $dimension1, $dimension2, bool $isDataset)
360
    {
361
        $dataset = $this->getDatasetId($objectId, $isDataset);
362
        if ($dataset != '') {
363
            $result = $this->StorageService->deleteSimulate($dataset, $dimension1, $dimension2);
364
            return ['delete' => $result];
365
        } else {
366
            return false;
367
        }
368
    }
369
370
    /**
371
     * delete data from input form
372
     *
373
     * @NoAdminRequired
374
     * @param int $objectId
375
     * @param $dimension1
376
     * @param $dimension2
377
     * @param bool $isDataset
378
     * @return array|false
379
     * @throws \OCP\DB\Exception
380
     */
381
    public function deleteData(int $objectId, $dimension1, $dimension2, bool $isDataset)
382
    {
383
        $dataset = $this->getDatasetId($objectId, $isDataset);
384
        if ($dataset != '') {
385
            $result = $this->StorageService->delete($dataset, $dimension1, $dimension2);
386
            return ['delete' => $result];
387
        } else {
388
            return false;
389
        }
390
    }
391
392
    /**
393
     * Import clipboard data
394
     *
395
     * @NoAdminRequired
396
     * @param int $objectId
397
     * @param $import
398
     * @param bool $isDataset
399
     * @return array|false
400
     * @throws \OCP\DB\Exception
401
     */
402
    public function importClipboard($objectId, $import, bool $isDataset)
403
    {
404
        $dataset = $this->getDatasetId($objectId, $isDataset);
405
        if ($dataset != '') {
406
            $insert = $update = $errorMessage = $errorCounter = 0;
407
            $delimiter = '';
408
409
            if ($import === '') {
410
                $errorMessage = $this->l10n->t('No data');
411
            } else {
412
                $delimiter = $this->detectDelimiter($import);
413
                $rows = str_getcsv($import, "\n");
414
415
                foreach ($rows as &$row) {
416
                    $row = str_getcsv($row, $delimiter);
417
                    $numberOfColumns = count($row);
418
                    // last column needs to be a float
419
                    $row[2] = $this->floatvalue($row[$numberOfColumns - 1]);
420
                    if ($row[2] === false) {
421
                        $errorCounter++;
422
                    } else {
423
                        if ($numberOfColumns < 3) $row[1] = null;
424
                        $action = $this->StorageService->update($dataset, $row[0], $row[1], $row[2]);
425
                        $insert = $insert + $action['insert'];
426
                        $update = $update + $action['update'];
427
                    }
428
                    if ($errorCounter === 2) {
429
                        // first error is ignored; might be due to header row
430
                        $errorMessage = $this->l10n->t('Last field must be a valid number');
431
                        break;
432
                    }
433
                }
434
            }
435
436
            $result = [
437
                'insert' => $insert,
438
                'update' => $update,
439
                'delimiter' => $delimiter,
440
                'error' => $errorMessage,
441
            ];
442
443
            if ($errorMessage === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_IMPORT);
444
            return $result;
445
        } else {
446
            return false;
447
        }
448
    }
449
450
    /**
451
     * Import data into dataset from an internal or external file
452
     *
453
     * @NoAdminRequired
454
     * @param int $objectId
455
     * @param $path
456
     * @param bool $isDataset
457
     * @return array|false
458
     * @throws \OCP\DB\Exception
459
     */
460
    public function importFile(int $objectId, $path, bool $isDataset)
461
    {
462
        $dataset = $this->getDatasetId($objectId, $isDataset);
463
        if ($dataset != '') {
464
            $insert = $update = 0;
465
            $reportMetadata['link'] = $path;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$reportMetadata was never initialized. Although not strictly required by PHP, it is generally a good practice to add $reportMetadata = array(); before regardless.
Loading history...
466
            $reportMetadata['user_id'] = $this->userId;
467
            $result = $this->DatasourceController->read(DatasourceController::DATASET_TYPE_FILE, $reportMetadata);
468
469
            if ($result['error'] === 0) {
470
                foreach ($result['data'] as &$row) {
471
                    $action = $this->StorageService->update($dataset, $row[0], $row[1], $row[2]);
472
                    $insert = $insert + $action['insert'];
473
                    $update = $update + $action['update'];
474
                }
475
            }
476
477
            $result = [
478
                'insert' => $insert,
479
                'update' => $update,
480
                'error' => $result['error'],
481
            ];
482
483
            if ($result['error'] === 0) $this->ActivityManager->triggerEvent($dataset, ActivityManager::OBJECT_DATA, ActivityManager::SUBJECT_DATA_ADD_IMPORT);
484
            return $result;
485
        } else {
486
            return false;
487
        }
488
    }
489
490
    private function getDatasetId($objectId, bool $isDataset)
491
    {
492
        if ($isDataset) {
493
            $dataset = $objectId;
494
        } else {
495
            $reportMetadata = $this->ReportService->read($objectId);
496
            $dataset = (int)$reportMetadata['dataset'];
497
        }
498
        return $dataset;
499
    }
500
501
    private function detectDelimiter($data): string
502
    {
503
        $delimiters = ["\t", ";", "|", ","];
504
        $data_2 = null;
505
        $delimiter = $delimiters[0];
506
        foreach ($delimiters as $d) {
507
            $firstRow = str_getcsv($data, "\n")[0];
508
            $data_1 = str_getcsv($firstRow, $d);
509
            if (sizeof($data_1) > sizeof($data_2)) {
510
                $delimiter = $d;
511
                $data_2 = $data_1;
512
            }
513
        }
514
        return $delimiter;
515
    }
516
517
    private function floatvalue($val)
518
    {
519
        $val = str_replace(",", ".", $val);
520
        $val = preg_replace('/\.(?=.*\.)/', '', $val);
521
        $val = preg_replace('/[^0-9-.]+/', '', $val);
522
        if (is_numeric($val)) {
523
            return number_format(floatval($val), 2, '.', '');
524
        } else {
525
            return false;
526
        }
527
    }
528
529
}