DatasourceController::filterData()   C
last analyzed

Complexity

Conditions 14
Paths 2

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 15
nc 2
nop 2
dl 0
loc 22
rs 6.2666
c 2
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Controller;
10
11
use OCA\Analytics\Datasource\DatasourceEvent;
12
use OCA\Analytics\Datasource\ExternalCsv;
13
use OCA\Analytics\Datasource\ExternalJson;
14
use OCA\Analytics\Datasource\Github;
15
use OCA\Analytics\Datasource\LocalCsv;
16
use OCA\Analytics\Datasource\LocalExcel;
17
use OCA\Analytics\Datasource\LocalJson;
18
use OCA\Analytics\Datasource\Regex;
19
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...
20
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...
21
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...
22
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...
23
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...
24
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...
25
26
class DatasourceController extends Controller {
27
	private $logger;
28
	private $GithubService;
29
	private $ExternalCsvService;
30
	private $RegexService;
31
	private $ExternalJsonService;
32
	private $LocalJsonService;
33
	private $LocalCsvService;
34
	private $LocalExcelService;
35
	/** @var IEventDispatcher */
36
	private $dispatcher;
37
	private $l10n;
38
39
	const DATASET_TYPE_GROUP = 0;
40
	const DATASET_TYPE_LOCAL_CSV = 1;
41
	const DATASET_TYPE_INTERNAL_DB = 2;
42
	const DATASET_TYPE_GIT = 3;
43
	const DATASET_TYPE_EXTERNAL_CSV = 4;
44
	const DATASET_TYPE_REGEX = 5;
45
	const DATASET_TYPE_EXTERNAL_JSON = 6;
46
	const DATASET_TYPE_LOCAL_EXCEL = 7;
47
	const DATASET_TYPE_LOCAL_JSON = 8;
48
49
	public function __construct(
50
		string           $appName,
51
		IRequest         $request,
52
		LoggerInterface  $logger,
53
		Github           $GithubService,
54
		LocalCsv         $LocalCsvService,
55
		Regex            $RegexService,
56
		ExternalJson     $ExternalJsonService,
57
		LocalJson        $LocalJsonService,
58
		ExternalCsv      $ExternalCsvService,
59
		LocalExcel       $LocalExcelService,
60
		IL10N            $l10n,
61
		IEventDispatcher $dispatcher
62
	) {
63
		parent::__construct($appName, $request);
64
		$this->logger = $logger;
65
		$this->ExternalCsvService = $ExternalCsvService;
66
		$this->GithubService = $GithubService;
67
		$this->RegexService = $RegexService;
68
		$this->LocalCsvService = $LocalCsvService;
69
		$this->ExternalJsonService = $ExternalJsonService;
70
		$this->LocalJsonService = $LocalJsonService;
71
		$this->LocalExcelService = $LocalExcelService;
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
		$datasources = array();
83
		$result = array();
84
		foreach ($this->getDatasources() as $key => $class) {
85
			$datasources[$key] = $class->getName();
86
		}
87
		$result['datasources'] = $datasources;
88
89
		$options = array();
90
		foreach ($this->getDatasources() as $key => $class) {
91
			$options[$key] = $class->getTemplate();
92
		}
93
		$result['options'] = $options;
94
95
		return $result;
96
	}
97
98
	/**
99
	 * get all data source templates
100
	 *
101
	 * @NoAdminRequired
102
	 * @return array
103
	 */
104
	public function getTemplates() {
105
		$result = array();
106
		foreach ($this->getDatasources() as $key => $class) {
107
			$result[$key] = $class->getTemplate();
108
		}
109
		return $result;
110
	}
111
112
	/**
113
	 * Get the data from a datasource;
114
	 *
115
	 * @NoAdminRequired
116
	 * @param int $datasourceId
117
	 * @param $datasetMetadata
118
	 * @return array|NotFoundException
119
	 */
120
	public function read(int $datasourceId, $datasetMetadata) {
121
		if (!$this->getDatasources()[$datasourceId]) {
122
			$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...
123
			return $result;
124
		}
125
126
		$option = array();
127
		// before 3.1.0, the options were in another format. as of 3.1.0 the standard option array is used
128
		if ($datasetMetadata['link'][0] !== '{') {
129
			$option['link'] = $datasetMetadata['link'];
130
		} else {
131
			$option = json_decode($datasetMetadata['link'], true);
132
		}
133
		$option['user_id'] = $datasetMetadata['user_id'];
134
135
		try {
136
			// read the data from the source
137
			$result = $this->getDatasources()[$datasourceId]->readData($option);
138
139
			// if data source should be timestamped/snapshoted
140
			if (isset($option['timestamp']) and $option['timestamp'] === 'true') {
141
				date_default_timezone_set('UTC');
142
				$result['data'] = array_map(function ($tag) {
143
					$columns = count($tag);
144
					if ($columns > 1) {
145
						// shift values by one dimension and stores date in second column
146
						return array($tag[$columns - 2], date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
147
					} else {
148
						// just return 2 columns if the original data only has one column
149
						return array(date("Y-m-d H:i:s") . 'Z', $tag[$columns - 1]);
150
					}
151
				}, $result['data']);
152
			}
153
154
			if (isset($datasetMetadata['filteroptions']) && strlen($datasetMetadata['filteroptions']) >> 2) {
155
				// filter data
156
				$result = $this->filterData($result, $datasetMetadata['filteroptions']);
157
				// remove columns and aggregate data
158
				$result = $this->aggregateData($result, $datasetMetadata['filteroptions']);
159
			}
160
161
162
		} catch (\Error $e) {
163
			$result['error'] = $e->getMessage();
164
		}
165
166
		if (empty($result['data'])) {
167
			$result['status'] = 'nodata';
168
		}
169
		return $result;
170
	}
171
172
	/**
173
	 * combine internal and registered datasources
174
	 * @return array
175
	 */
176
	private function getDatasources() {
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
		$dataSources = [];
186
		$dataSources[self::DATASET_TYPE_LOCAL_CSV] = $this->LocalCsvService;
187
		$dataSources[self::DATASET_TYPE_LOCAL_EXCEL] = $this->LocalExcelService;
188
		$dataSources[self::DATASET_TYPE_GIT] = $this->GithubService;
189
		$dataSources[self::DATASET_TYPE_EXTERNAL_CSV] = $this->ExternalCsvService;
190
		$dataSources[self::DATASET_TYPE_REGEX] = $this->RegexService;
191
		$dataSources[self::DATASET_TYPE_EXTERNAL_JSON] = $this->ExternalJsonService;
192
		$dataSources[self::DATASET_TYPE_LOCAL_JSON] = $this->LocalJsonService;
193
194
		return $dataSources;
195
	}
196
197
	/**
198
	 * map all registered data sources to their IDs
199
	 * @return array
200
	 */
201
	private function getRegisteredDatasources() {
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)
212
																																		 ->getName()));
213
					continue;
214
				}
215
				$dataSources[$uniqueId] = \OC::$server->get($class);
216
			} catch (\Error $e) {
217
				$this->logger->error('Can not initialize data source: ' . json_encode($class));
218
				$this->logger->error($e->getMessage());
219
			}
220
		}
221
		return $dataSources;
222
	}
223
224
	/**
225
	 * apply the fiven filters to the hole result set
226
	 *
227
	 * @NoAdminRequired
228
	 * @param $data
229
	 * @param $filter
230
	 * @return array
231
	 */
232
	private function filterData($data, $filter) {
233
		$options = json_decode($filter, true);
234
		if (isset($options['filter'])) {
235
			foreach ($options['filter'] as $key => $value) {
236
				$filterValue = $value['value'];
237
				$filterOption = $value['option'];
238
				$filtered = array();
239
240
				foreach ($data['data'] as $record) {
241
					if (($filterOption === 'EQ' && $record[$key] === $filterValue) || ($filterOption === 'GT' && $record[$key] > $filterValue) || ($filterOption === 'LT' && $record[$key] < $filterValue) || ($filterOption === 'LIKE' && strpos($record[$key], $filterValue) !== false)) {
242
						array_push($filtered, $record);
243
					} else if ($filterOption === 'IN') {
244
						$filterValues = explode(',', $filterValue);
245
						if (in_array($record[$key], $filterValues)) {
246
							array_push($filtered, $record);
247
						}
248
					}
249
				}
250
				$data['data'] = $filtered;
251
			}
252
		}
253
		return $data;
254
	}
255
256
	private function aggregateData($data, $filter) {
257
		$options = json_decode($filter, true);
258
		if (isset($options['drilldown'])) {
259
			// Sort the indices in descending order
260
			$sortedIndices = array_keys($options['drilldown']);
261
			rsort($sortedIndices);
262
263
			foreach ($sortedIndices as $removeIndex) {
264
				$aggregatedData = [];
265
266
				// remove the header of the column which is not needed
267
				unset($data['header'][$removeIndex]);
268
				$data['header'] = array_values($data['header']);
269
270
				// remove the column of the data
271
				foreach ($data['data'] as $row) {
272
					// Remove the desired column by its index
273
					unset($row[$removeIndex]);
274
275
					// The last column is assumed to always be the value
276
					$value = array_pop($row);
277
278
					// If there are no columns left except the value column, insert a dummy
279
					if (empty($row)) {
280
						$key = 'xxsingle_valuexx';
281
					} else {
282
						// Use remaining columns as key
283
						$key = implode("|", $row);
284
					}
285
286
					if (!isset($aggregatedData[$key])) {
287
						$aggregatedData[$key] = 0;
288
					}
289
					$aggregatedData[$key] += $value;
290
				}
291
292
				// Convert the associative array to the desired format
293
				$result = [];
294
				foreach ($aggregatedData as $aKey => $aValue) {
295
					// If only the value column remains, append its total value
296
					if ($aKey === 'xxsingle_valuexx') {
297
						$aKey = $this->l10n->t('Total');
298
						// Add an empty column to the header because of the "total" row description
299
						array_unshift($data['header'], '');
300
					}
301
					$result[] = [$aKey, $aValue];
302
				}
303
				$data['data'] = $result;
304
			}
305
		}
306
		return $data;
307
	}
308
}