DatasourceController::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
nc 1
nop 12
dl 0
loc 25
rs 9.9
c 1
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
 * 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
}