Passed
Push — master ( 30a677...1ae68b )
by Marcel
03:50 queued 12s
created

DatasourceController::read()   B

Complexity

Conditions 9
Paths 61

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 26
c 1
b 0
f 0
nc 61
nop 2
dl 0
loc 47
rs 8.0555
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\Controller;
13
14
use OCA\Analytics\Datasource\DatasourceEvent;
15
use OCA\Analytics\Datasource\Excel;
16
use OCA\Analytics\Datasource\ExternalFile;
17
use OCA\Analytics\Datasource\File;
18
use OCA\Analytics\Datasource\Github;
19
use OCA\Analytics\Datasource\Json;
20
use OCA\Analytics\Datasource\Regex;
21
use OCP\AppFramework\Controller;
22
use OCP\EventDispatcher\IEventDispatcher;
23
use OCP\Files\NotFoundException;
24
use OCP\IL10N;
25
use OCP\IRequest;
26
use Psr\Log\LoggerInterface;
27
28
class DatasourceController extends Controller
29
{
30
    private $logger;
31
    private $GithubService;
32
    private $FileService;
33
    private $ExternalFileService;
34
    private $RegexService;
35
    private $JsonService;
36
    private $ExcelService;
37
    /** @var IEventDispatcher */
38
    private $dispatcher;
39
    private $l10n;
40
41
    const DATASET_TYPE_GROUP = 0;
42
    const DATASET_TYPE_FILE = 1;
43
    const DATASET_TYPE_INTERNAL_DB = 2;
44
    const DATASET_TYPE_GIT = 3;
45
    const DATASET_TYPE_EXTERNAL_FILE = 4;
46
    const DATASET_TYPE_REGEX = 5;
47
    const DATASET_TYPE_JSON = 6;
48
    const DATASET_TYPE_EXCEL = 7;
49
50
    public function __construct(
51
        string           $AppName,
52
        IRequest         $request,
53
        LoggerInterface  $logger,
54
        Github           $GithubService,
55
        File             $FileService,
56
        Regex            $RegexService,
57
        Json             $JsonService,
58
        ExternalFile     $ExternalFileService,
59
        Excel            $ExcelService,
60
        IL10N            $l10n,
61
        IEventDispatcher $dispatcher
62
    )
63
    {
64
        parent::__construct($AppName, $request);
65
        $this->logger = $logger;
66
        $this->ExternalFileService = $ExternalFileService;
67
        $this->GithubService = $GithubService;
68
        $this->RegexService = $RegexService;
69
        $this->FileService = $FileService;
70
        $this->JsonService = $JsonService;
71
        $this->ExcelService = $ExcelService;
72
        $this->dispatcher = $dispatcher;
73
        $this->l10n = $l10n;
74
    }
75
76
    /**
77
     * get all data source ids + names
78
     *
79
     * @NoAdminRequired
80
     */
81
    public function index()
82
    {
83
        $datasources = array();
84
        $result = array();
85
        foreach ($this->getDatasources() as $key => $class) {
86
            $datasources[$key] = $class->getName();
87
        }
88
        $result['datasources'] = $datasources;
89
90
        $options = array();
91
        foreach ($this->getDatasources() as $key => $class) {
92
            $options[$key] = $class->getTemplate();
93
        }
94
        $result['options'] = $options;
95
96
        return $result;
97
    }
98
99
    /**
100
     * get all data source templates
101
     *
102
     * @NoAdminRequired
103
     * @return array
104
     */
105
    public function getTemplates()
106
    {
107
        $result = array();
108
        foreach ($this->getDatasources() as $key => $class) {
109
            $result[$key] = $class->getTemplate();
110
        }
111
        return $result;
112
    }
113
114
    /**
115
     * Get the data from a datasource;
116
     *
117
     * @NoAdminRequired
118
     * @param int $datasourceId
119
     * @param $datasetMetadata
120
     * @return array|NotFoundException
121
     */
122
    public function read(int $datasourceId, $datasetMetadata)
123
    {
124
        if (!$this->getDatasources()[$datasourceId]) {
125
            $result['error'] = $this->l10n->t('Data source not available anymore');
0 ignored issues
show
Comprehensibility Best Practice introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.
Loading history...
126
            return $result;
127
        }
128
129
        $option = array();
130
        // before 3.1.0, the options were in another format. as of 3.1.0 the standard option array is used
131
        if ($datasetMetadata['link'][0] !== '{') {
132
            $option['link'] = $datasetMetadata['link'];
133
        } else {
134
            $option = json_decode($datasetMetadata['link'], true);
135
        }
136
        $option['user_id'] = $datasetMetadata['user_id'];
137
138
        try {
139
            // read the data from the source
140
            $result = $this->getDatasources()[$datasourceId]->readData($option);
141
142
            // if data source should be timestamped/snapshoted
143
            if (isset($option['timestamp']) and $option['timestamp'] === 'true') {
144
                $result['data'] = array_map(function ($tag) {
145
                    $columns = count($tag);
146
                    return array($tag[$columns - 2], $tag[$columns - 2], $tag[$columns - 1]);
147
                }, $result['data']);
148
                date_default_timezone_set('UTC');
149
                // shift values by one dimension and stores date in second column
150
                $result['data'] = $this->replaceDimension($result['data'], 1, date("Y-m-d H:i:s") . 'Z');
151
            }
152
153
            if (isset($datasetMetadata['filteroptions']) && strlen($datasetMetadata['filteroptions']) >> 2) {
154
                // filter data
155
                $result = $this->filterData($result, $datasetMetadata['filteroptions']);
156
                // remove columns and aggregate data
157
                $result = $this->aggregateData($result, $datasetMetadata['filteroptions']);
158
            }
159
160
161
        } catch (\Error $e) {
162
            $result['error'] = $e->getMessage();
163
        }
164
165
        if (empty($result['data'])) {
166
            $result['status'] = 'nodata';
167
        }
168
        return $result;
169
    }
170
171
    /**
172
     * combine internal and registered datasources
173
     * @return array
174
     */
175
    private function getDatasources()
176
    {
177
        return $this->getOwnDatasources() + $this->getRegisteredDatasources();
178
    }
179
180
    /**
181
     * map all internal data sources to their IDs
182
     * @return array
183
     */
184
    private function getOwnDatasources()
185
    {
186
        $dataSources = [];
187
        $dataSources[self::DATASET_TYPE_FILE] = $this->FileService;
188
        $dataSources[self::DATASET_TYPE_EXCEL] = $this->ExcelService;
189
        $dataSources[self::DATASET_TYPE_GIT] = $this->GithubService;
190
        $dataSources[self::DATASET_TYPE_EXTERNAL_FILE] = $this->ExternalFileService;
191
        $dataSources[self::DATASET_TYPE_REGEX] = $this->RegexService;
192
        $dataSources[self::DATASET_TYPE_JSON] = $this->JsonService;
193
        return $dataSources;
194
    }
195
196
    /**
197
     * map all registered data sources to their IDs
198
     * @return array
199
     */
200
    private function getRegisteredDatasources()
201
    {
202
        $dataSources = [];
203
        $event = new DatasourceEvent();
204
        $this->dispatcher->dispatchTyped($event);
205
206
        foreach ($event->getDataSources() as $class) {
207
            try {
208
                $uniqueId = '99' . \OC::$server->get($class)->getId();
209
210
                if (isset($dataSources[$uniqueId])) {
211
                    $this->logger->error(new \InvalidArgumentException('Data source with the same ID already registered: ' . \OC::$server->get($class)->getName()));
212
                    continue;
213
                }
214
                $dataSources[$uniqueId] = \OC::$server->get($class);
215
            } catch (\Error $e) {
216
                $this->logger->error('Can not initialize data source: ' . json_encode($class));
217
                $this->logger->error($e->getMessage());
218
            }
219
        }
220
        return $dataSources;
221
    }
222
223
    /**
224
     * replace all values of one dimension
225
     *
226
     * @NoAdminRequired
227
     * @param $Array
228
     * @param $Find
229
     * @param $Replace
230
     * @return array
231
     */
232
    private function replaceDimension($Array, $Find, $Replace)
233
    {
234
        if (is_array($Array)) {
235
            foreach ($Array as $Key => $Val) {
236
                if (is_array($Array[$Key])) {
237
                    $Array[$Key] = $this->replaceDimension($Array[$Key], $Find, $Replace);
238
                } else {
239
                    if ($Key === $Find) {
240
                        $Array[$Key] = $Replace;
241
                    }
242
                }
243
            }
244
        }
245
        return $Array;
246
    }
247
248
    /**
249
     * apply the fiven filters to the hole result set
250
     *
251
     * @NoAdminRequired
252
     * @param $data
253
     * @param $filter
254
     * @return array
255
     */
256
    private function filterData($data, $filter)
257
    {
258
        $options = json_decode($filter, true);
259
        if (isset($options['filter'])) {
260
            foreach ($options['filter'] as $key => $value) {
261
                $filterValue = $value['value'];
262
                $filterOption = $value['option'];
263
                $filtered = array();
264
265
                foreach ($data['data'] as $record) {
266
                    if (
267
                        ($filterOption === 'EQ' && $record[$key] === $filterValue)
268
                        || ($filterOption === 'GT' && $record[$key] > $filterValue)
269
                        || ($filterOption === 'LT' && $record[$key] < $filterValue)
270
                        || ($filterOption === 'LIKE' && strpos($record[$key], $filterValue) !== FALSE)
271
                    ) {
272
                        array_push($filtered, $record);
273
                    } else if ($filterOption === 'IN') {
274
                        $filterValues = explode(',', $filterValue);
275
                        if (in_array($record[$key], $filterValues)) {
276
                            array_push($filtered, $record);
277
                        }
278
                    }
279
                }
280
                $data['data'] = $filtered;
281
            }
282
        }
283
        return $data;
284
    }
285
286
    private function aggregateData($data, $filter)
287
    {
288
        $options = json_decode($filter, true);
289
        if (isset($options['drilldown'])) {
290
            // Sort the indices in descending order
291
            $sortedIndices = array_keys($options['drilldown']);
292
            rsort($sortedIndices);
293
294
            foreach ($sortedIndices as $removeIndex) {
295
                $aggregatedData = [];
296
297
                // remove the header of the column which is not needed
298
                unset($data['header'][$removeIndex]);
299
                $data['header'] = array_values($data['header']);
300
301
                // remove the column of the data
302
                foreach ($data['data'] as $row) {
303
                    // Remove the desired column by its index
304
                    unset($row[$removeIndex]);
305
306
                    // The last column is assumed to always be the value
307
                    $value = array_pop($row);
308
309
                    // If there are no columns left except the value column
310
                    if (empty($row)) {
311
                        $key = 'xxsingle_valuexx';
312
                    } else {
313
                        // Use remaining columns as key
314
                        $key = implode("|", $row);
315
                    }
316
317
                    if (!isset($aggregatedData[$key])) {
318
                        $aggregatedData[$key] = 0;
319
                    }
320
                    $aggregatedData[$key] += $value;
321
                }
322
323
                // Convert the associative array to the desired format
324
                $result = [];
325
                foreach ($aggregatedData as $aKey => $aValue) {
326
                    // If only the value column remains, append its total value
327
                    if ($aKey === 'xxsingle_valuexx') {
328
                        $aKey = $this->l10n->t('Total');
329
                        // Add an empty column to the beginning
330
                        array_unshift($data['header'], "");
331
                    }
332
                    $result[] = [$aKey, $aValue];
333
                }
334
335
                $data['data'] = $result;
336
            }
337
        }
338
        return $data;
339
    }
340
}