Completed
Push — master ( fd0ef2...824644 )
by Morris
33s queued 10s
created

ElasticSearchPlatform::cleanHost()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
declare(strict_types=1);
3
4
5
/**
6
 * FullTextSearch_Elasticsearch - Use Elasticsearch to index the content of your nextcloud
7
 *
8
 * This file is licensed under the Affero General Public License version 3 or
9
 * later. See the COPYING file.
10
 *
11
 * @author Maxence Lange <[email protected]>
12
 * @copyright 2018
13
 * @license GNU AGPL version 3 or any later version
14
 *
15
 * This program is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License as
17
 * published by the Free Software Foundation, either version 3 of the
18
 * License, or (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License
26
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
27
 *
28
 */
29
30
31
namespace OCA\FullTextSearch_Elasticsearch\Platform;
32
33
34
use daita\MySmallPhpTools\Traits\TArrayTools;
35
use daita\MySmallPhpTools\Traits\TPathTools;
36
use Elasticsearch\Client;
37
use Elasticsearch\ClientBuilder;
38
use Elasticsearch\Common\Exceptions\BadRequest400Exception;
39
use Exception;
40
use InvalidArgumentException;
41
use OCA\FullTextSearch_Elasticsearch\Exceptions\AccessIsEmptyException;
42
use OCA\FullTextSearch_Elasticsearch\Exceptions\ConfigurationException;
43
use OCA\FullTextSearch_Elasticsearch\Service\ConfigService;
44
use OCA\FullTextSearch_Elasticsearch\Service\IndexService;
45
use OCA\FullTextSearch_Elasticsearch\Service\MiscService;
46
use OCA\FullTextSearch_Elasticsearch\Service\SearchService;
47
use OCP\FullTextSearch\IFullTextSearchPlatform;
48
use OCP\FullTextSearch\Model\IDocumentAccess;
49
use OCP\FullTextSearch\Model\IIndex;
50
use OCP\FullTextSearch\Model\IIndexDocument;
51
use OCP\FullTextSearch\Model\IRunner;
52
use OCP\FullTextSearch\Model\ISearchResult;
53
54
55
/**
56
 * Class ElasticSearchPlatform
57
 *
58
 * @package OCA\FullTextSearch_Elasticsearch\Platform
59
 */
60
class ElasticSearchPlatform implements IFullTextSearchPlatform {
61
62
63
	use TPathTools;
0 ignored issues
show
Deprecated Code introduced by
The trait daita\MySmallPhpTools\Traits\TPathTools has been deprecated with message: - 19

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
64
	use TArrayTools;
65
66
67
	/** @var ConfigService */
68
	private $configService;
69
70
	/** @var IndexService */
71
	private $indexService;
72
73
	/** @var SearchService */
74
	private $searchService;
75
76
	/** @var MiscService */
77
	private $miscService;
78
79
	/** @var Client */
80
	private $client;
81
82
	/** @var IRunner */
83
	private $runner;
84
85
86
	/**
87
	 * ElasticSearchPlatform constructor.
88
	 *
89
	 * @param ConfigService $configService
90
	 * @param IndexService $indexService
91
	 * @param SearchService $searchService
92
	 * @param MiscService $miscService
93
	 */
94
	public function __construct(
95
		ConfigService $configService, IndexService $indexService, SearchService $searchService,
96
		MiscService $miscService
97
	) {
98
		$this->configService = $configService;
99
		$this->indexService = $indexService;
100
		$this->searchService = $searchService;
101
		$this->miscService = $miscService;
102
	}
103
104
105
	/**
106
	 * return a unique Id of the platform.
107
	 */
108
	public function getId(): string {
109
		return 'elastic_search';
110
	}
111
112
113
	/**
114
	 * return a unique Id of the platform.
115
	 */
116
	public function getName(): string {
117
		return 'Elasticsearch';
118
	}
119
120
121
	/**
122
	 * @return array
123
	 * @throws ConfigurationException
124
	 */
125
	public function getConfiguration(): array {
126
		$result = $this->configService->getConfig();
127
128
		$sanitizedHosts = [];
129
		$hosts = $this->configService->getElasticHost();
130
		foreach ($hosts as $host) {
131
			$parsedHost = parse_url($host);
132
			$safeHost = $parsedHost['scheme'] . '://';
133
			if (array_key_exists('user', $parsedHost)) {
134
				$safeHost .= $parsedHost['user'] . ':' . '********' . '@';
135
			}
136
			$safeHost .= $parsedHost['host'];
137
			$safeHost .= ':' . $parsedHost['port'];
138
139
			$sanitizedHosts[] = $safeHost;
140
		}
141
142
		$result['elastic_host'] = $sanitizedHosts;
143
144
		return $result;
145
	}
146
147
148
	/**
149
	 * @param IRunner $runner
150
	 */
151
	public function setRunner(IRunner $runner) {
152
		$this->runner = $runner;
153
	}
154
155
156
	/**
157
	 * Called when loading the platform.
158
	 *
159
	 * Loading some container and connect to ElasticSearch.
160
	 *
161
	 * @throws ConfigurationException
162
	 * @throws Exception
163
	 */
164
	public function loadPlatform() {
165
		try {
166
			$this->connectToElastic($this->configService->getElasticHost());
167
		} catch (ConfigurationException $e) {
168
			throw $e;
169
		}
170
	}
171
172
173
	/**
174
	 * not used yet.
175
	 *
176
	 * @return bool
177
	 */
178
	public function testPlatform(): bool {
179
		return $this->client->ping();
180
	}
181
182
183
	/**
184
	 * called before any index
185
	 *
186
	 * We create a general index.
187
	 *
188
	 * @throws ConfigurationException
189
	 * @throws BadRequest400Exception
190
	 */
191
	public function initializeIndex() {
192
		$this->indexService->initializeIndex($this->client);
193
	}
194
195
196
	/**
197
	 * resetIndex();
198
	 *
199
	 * Called when admin wants to remove an index specific to a $provider.
200
	 * $provider can be null, meaning a reset of the whole index.
201
	 *
202
	 * @param string $providerId
203
	 *
204
	 * @throws ConfigurationException
205
	 */
206
	public function resetIndex(string $providerId) {
207
		if ($providerId === 'all') {
208
			$this->indexService->resetIndexAll($this->client);
209
		} else {
210
			$this->indexService->resetIndex($this->client, $providerId);
211
		}
212
	}
213
214
215
	/**
216
	 * @param IIndexDocument $document
217
	 *
218
	 * @return IIndex
219
	 */
220
	public function indexDocument(IIndexDocument $document): IIndex {
221
222
		$document->initHash();
223
224
		try {
225
			$result = $this->indexService->indexDocument($this->client, $document);
226
			$index = $this->indexService->parseIndexResult($document->getIndex(), $result);
227
228
			$this->updateNewIndexResult(
229
				$document->getIndex(), json_encode($result), 'ok',
230
				IRunner::RESULT_TYPE_SUCCESS
231
			);
232
233
			return $index;
234
		} catch (Exception $e) {
235
			$this->updateNewIndexResult(
236
				$document->getIndex(), '', 'issue while indexing, testing with empty content',
237
				IRunner::RESULT_TYPE_WARNING
238
			);
239
240
			$this->manageIndexErrorException($document, $e);
241
		}
242
243
		try {
244
			$result = $this->indexDocumentError($document, $e);
245
			$index = $this->indexService->parseIndexResult($document->getIndex(), $result);
246
247
			$this->updateNewIndexResult(
248
				$document->getIndex(), json_encode($result), 'ok',
249
				IRunner::RESULT_TYPE_WARNING
250
			);
251
252
			return $index;
253
		} catch (Exception $e) {
254
			$this->updateNewIndexResult(
255
				$document->getIndex(), '', 'fail',
256
				IRunner::RESULT_TYPE_FAIL
257
			);
258
			$this->manageIndexErrorException($document, $e);
259
		}
260
261
		return $document->getIndex();
262
	}
263
264
265
	/**
266
	 * @param IIndexDocument $document
267
	 * @param Exception $e
268
	 *
269
	 * @return array
270
	 * @throws AccessIsEmptyException
271
	 * @throws ConfigurationException
272
	 * @throws Exception
273
	 */
274
	private function indexDocumentError(IIndexDocument $document, Exception $e): array {
275
276
		$this->updateRunnerAction('indexDocumentWithoutContent', true);
277
278
		$document->setContent('');
279
//		$index = $document->getIndex();
280
//		$index->unsetStatus(Index::INDEX_CONTENT);
281
282
		return $this->indexService->indexDocument($this->client, $document);
283
	}
284
285
286
	/**
287
	 * @param IIndexDocument $document
288
	 * @param Exception $e
289
	 */
290
	private function manageIndexErrorException(IIndexDocument $document, Exception $e) {
291
292
		$message = $this->parseIndexErrorException($e);
293
		$document->getIndex()
294
				 ->addError($message, get_class($e), IIndex::ERROR_SEV_3);
295
		$this->updateNewIndexError(
296
			$document->getIndex(), $message, get_class($e), IIndex::ERROR_SEV_3
297
		);
298
	}
299
300
301
	/**
302
	 * @param Exception $e
303
	 *
304
	 * @return string
305
	 */
306
	private function parseIndexErrorException(Exception $e): string {
307
		$arr = json_decode($e->getMessage(), true);
308
		if (!is_array($arr) || !isset($arr['error'])) {
309
			return $e->getMessage();
310
		}
311
312
		try {
313
			return $this->parseCausedBy($arr['error']);
314
		} catch (InvalidArgumentException $e) {
315
			// unable to parse caused_by -> fallback to root_cause
316
		}
317
318
		$cause = $this->getArray('error.root_cause', $arr);
319
		if (!empty($cause) && $this->get('reason', $cause[0]) !== '') {
320
			return $this->get('reason', $cause[0]);
321
		}
322
323
		return $e->getMessage();
324
	}
325
326
	/**
327
	 * @throws InvalidArgumentException
328
	 */
329
	private function parseCausedBy(array $error): string {
330
		if (isset($error['caused_by'])) {
331
			return $this->parseCausedBy($error['caused_by']);
332
		}
333
334
		if (isset($error['reason'])) {
335
			return $error['reason'];
336
		}
337
338
		throw new InvalidArgumentException('Unable to parse given response structure');
339
	}
340
341
	/**
342
	 * {@inheritdoc}
343
	 * @throws ConfigurationException
344
	 */
345
	public function deleteIndexes(array $indexes) {
346
		try {
347
			$this->indexService->deleteIndexes($this->client, $indexes);
348
		} catch (ConfigurationException $e) {
349
			throw $e;
350
		}
351
	}
352
353
354
	/**
355
	 * {@inheritdoc}
356
	 * @throws Exception
357
	 */
358
	public function searchRequest(ISearchResult $result, IDocumentAccess $access) {
359
		$this->searchService->searchRequest($this->client, $result, $access);
360
	}
361
362
363
	/**
364
	 * @param string $providerId
365
	 * @param string $documentId
366
	 *
367
	 * @return IIndexDocument
368
	 * @throws ConfigurationException
369
	 */
370
	public function getDocument(string $providerId, string $documentId): IIndexDocument {
371
		return $this->searchService->getDocument($this->client, $providerId, $documentId);
372
	}
373
374
375
	private function cleanHost($host) {
376
		return $this->withoutEndSlash($host, false, false);
377
	}
378
379
	/**
380
	 * @param array $hosts
381
	 *
382
	 * @throws Exception
383
	 */
384
	private function connectToElastic(array $hosts) {
385
386
		try {
387
			$hosts = array_map([$this, 'cleanHost'], $hosts);
388
			$this->client = ClientBuilder::create()
389
										 ->setHosts($hosts)
390
										 ->setRetries(3)
391
										 ->build();
392
393
//		}
394
//		catch (CouldNotConnectToHost $e) {
395
//			$this 'CouldNotConnectToHost';
396
//			$previous = $e->getPrevious();
397
//			if ($previous instanceof MaxRetriesException) {
398
//				echo "Max retries!";
399
//			}
400
		} catch (Exception $e) {
401
			throw $e;
402
//			echo ' ElasticSearchPlatform::load() Exception --- ' . $e->getMessage() . "\n";
403
		}
404
	}
405
406
407
	/**
408
	 * @param string $action
409
	 * @param bool $force
410
	 *
411
	 * @throws Exception
412
	 */
413
	private function updateRunnerAction(string $action, bool $force = false) {
414
		if ($this->runner === null) {
415
			return;
416
		}
417
418
		$this->runner->updateAction($action, $force);
419
	}
420
421
422
	/**
423
	 * @param IIndex $index
424
	 * @param string $message
425
	 * @param string $exception
426
	 * @param int $sev
427
	 */
428
	private function updateNewIndexError(IIndex $index, string $message, string $exception, int $sev
429
	) {
430
		if ($this->runner === null) {
431
			return;
432
		}
433
434
		$this->runner->newIndexError($index, $message, $exception, $sev);
435
	}
436
437
438
	/**
439
	 * @param IIndex $index
440
	 * @param string $message
441
	 * @param string $status
442
	 * @param int $type
443
	 */
444
	private function updateNewIndexResult(IIndex $index, string $message, string $status, int $type
445
	) {
446
		if ($this->runner === null) {
447
			return;
448
		}
449
450
		$this->runner->newIndexResult($index, $message, $status, $type);
451
	}
452
453
454
}
455