Passed
Push — master ( 411e7a...439431 )
by Marcel
05:13 queued 15s
created

DatasourceController::replaceDimension()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 14
rs 9.6111
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;
0 ignored issues
show
Bug introduced by
The type OCP\AppFramework\Controller 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 OCP\EventDispatcher\IEventDispatcher;
0 ignored issues
show
Bug introduced by
The type OCP\EventDispatcher\IEventDispatcher 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
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...
24
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...
25
use OCP\IRequest;
0 ignored issues
show
Bug introduced by
The type OCP\IRequest 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...
26
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...
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
                date_default_timezone_set('UTC');
145
                $result['data'] = array_map(function ($tag) {
146
                    $columns = count($tag);
147
                    if ($columns > 1) {
148
                        // shift values by one dimension and stores date in second column
149
                        return array($tag[$columns - 2], date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
150
                    } else {
151
                        // just return 2 columns if the original data only has one column
152
                        return array(date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
153
                    }}, $result['data']);
154
            }
155
156
            if (isset($datasetMetadata['filteroptions']) && strlen($datasetMetadata['filteroptions']) >> 2) {
157
                // filter data
158
                $result = $this->filterData($result, $datasetMetadata['filteroptions']);
159
                // remove columns and aggregate data
160
                $result = $this->aggregateData($result, $datasetMetadata['filteroptions']);
161
            }
162
163
164
        } catch (\Error $e) {
165
            $result['error'] = $e->getMessage();
166
        }
167
168
        if (empty($result['data'])) {
169
            $result['status'] = 'nodata';
170
        }
171
        return $result;
172
    }
173
174
    /**
175
     * combine internal and registered datasources
176
     * @return array
177
     */
178
    private function getDatasources()
179
    {
180
        return $this->getOwnDatasources() + $this->getRegisteredDatasources();
181
    }
182
183
    /**
184
     * map all internal data sources to their IDs
185
     * @return array
186
     */
187
    private function getOwnDatasources()
188
    {
189
        $dataSources = [];
190
        $dataSources[self::DATASET_TYPE_FILE] = $this->FileService;
191
        $dataSources[self::DATASET_TYPE_EXCEL] = $this->ExcelService;
192
        $dataSources[self::DATASET_TYPE_GIT] = $this->GithubService;
193
        $dataSources[self::DATASET_TYPE_EXTERNAL_FILE] = $this->ExternalFileService;
194
        $dataSources[self::DATASET_TYPE_REGEX] = $this->RegexService;
195
        $dataSources[self::DATASET_TYPE_JSON] = $this->JsonService;
196
        return $dataSources;
197
    }
198
199
    /**
200
     * map all registered data sources to their IDs
201
     * @return array
202
     */
203
    private function getRegisteredDatasources()
204
    {
205
        $dataSources = [];
206
        $event = new DatasourceEvent();
207
        $this->dispatcher->dispatchTyped($event);
208
209
        foreach ($event->getDataSources() as $class) {
210
            try {
211
                $uniqueId = '99' . \OC::$server->get($class)->getId();
212
213
                if (isset($dataSources[$uniqueId])) {
214
                    $this->logger->error(new \InvalidArgumentException('Data source with the same ID already registered: ' . \OC::$server->get($class)->getName()));
215
                    continue;
216
                }
217
                $dataSources[$uniqueId] = \OC::$server->get($class);
218
            } catch (\Error $e) {
219
                $this->logger->error('Can not initialize data source: ' . json_encode($class));
220
                $this->logger->error($e->getMessage());
221
            }
222
        }
223
        return $dataSources;
224
    }
225
226
    /**
227
     * apply the fiven filters to the hole result set
228
     *
229
     * @NoAdminRequired
230
     * @param $data
231
     * @param $filter
232
     * @return array
233
     */
234
    private function filterData($data, $filter)
235
    {
236
        $options = json_decode($filter, true);
237
        if (isset($options['filter'])) {
238
            foreach ($options['filter'] as $key => $value) {
239
                $filterValue = $value['value'];
240
                $filterOption = $value['option'];
241
                $filtered = array();
242
243
                foreach ($data['data'] as $record) {
244
                    if (
245
                        ($filterOption === 'EQ' && $record[$key] === $filterValue)
246
                        || ($filterOption === 'GT' && $record[$key] > $filterValue)
247
                        || ($filterOption === 'LT' && $record[$key] < $filterValue)
248
                        || ($filterOption === 'LIKE' && strpos($record[$key], $filterValue) !== FALSE)
249
                    ) {
250
                        array_push($filtered, $record);
251
                    } else if ($filterOption === 'IN') {
252
                        $filterValues = explode(',', $filterValue);
253
                        if (in_array($record[$key], $filterValues)) {
254
                            array_push($filtered, $record);
255
                        }
256
                    }
257
                }
258
                $data['data'] = $filtered;
259
            }
260
        }
261
        return $data;
262
    }
263
264
    private function aggregateData($data, $filter)
265
    {
266
        $options = json_decode($filter, true);
267
        if (isset($options['drilldown'])) {
268
            // Sort the indices in descending order
269
            $sortedIndices = array_keys($options['drilldown']);
270
            rsort($sortedIndices);
271
272
            foreach ($sortedIndices as $removeIndex) {
273
                $aggregatedData = [];
274
275
                // remove the header of the column which is not needed
276
                unset($data['header'][$removeIndex]);
277
                $data['header'] = array_values($data['header']);
278
279
                // remove the column of the data
280
                foreach ($data['data'] as $row) {
281
                    // Remove the desired column by its index
282
                    unset($row[$removeIndex]);
283
284
                    // The last column is assumed to always be the value
285
                    $value = array_pop($row);
286
287
                    // If there are no columns left except the value column, insert a dummy
288
                    if (empty($row)) {
289
                        $key = 'xxsingle_valuexx';
290
                    } else {
291
                        // Use remaining columns as key
292
                        $key = implode("|", $row);
293
                    }
294
295
                    if (!isset($aggregatedData[$key])) {
296
                        $aggregatedData[$key] = 0;
297
                    }
298
                    $aggregatedData[$key] += $value;
299
                }
300
301
                // Convert the associative array to the desired format
302
                $result = [];
303
                foreach ($aggregatedData as $aKey => $aValue) {
304
                    // If only the value column remains, append its total value
305
                    if ($aKey === 'xxsingle_valuexx') {
306
                        $aKey = $this->l10n->t('Total');
307
                        // Add an empty column to the header because of the "total" row description
308
                        array_unshift($data['header'], '');
309
                    }
310
                    $result[] = [$aKey, $aValue];
311
                }
312
                $data['data'] = $result;
313
            }
314
        }
315
        return $data;
316
    }
317
}