Passed
Push — master ( 7972a5...654cd1 )
by Christoph
11:53 queued 12s
created

SearchComposer::search()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 10
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright 2020 Christoph Wurst <[email protected]>
7
 *
8
 * @author 2020 Christoph Wurst <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 */
25
26
namespace OC\Search;
27
28
use InvalidArgumentException;
29
use OCP\AppFramework\Bootstrap\IRegistrationContext;
30
use OCP\AppFramework\QueryException;
31
use OCP\ILogger;
32
use OCP\IServerContainer;
33
use OCP\IUser;
34
use OCP\Search\IProvider;
35
use OCP\Search\ISearchQuery;
36
use OCP\Search\SearchResult;
37
use function array_map;
38
39
/**
40
 * Queries individual \OCP\Search\IProvider implementations and composes a
41
 * unified search result for the user's search term
42
 *
43
 * The search process is generally split into two steps
44
 *
45
 *   1. Get a list of provider (`getProviders`)
46
 *   2. Get search results of each provider (`search`)
47
 *
48
 * The reasoning behind this is that the runtime complexity of a combined search
49
 * result would be O(n) and linearly grow with each provider added. This comes
50
 * from the nature of php where we can't concurrently fetch the search results.
51
 * So we offload the concurrency the client application (e.g. JavaScript in the
52
 * browser) and let it first get the list of providers to then fetch all results
53
 * concurrently. The client is free to decide whether all concurrent search
54
 * results are awaited or shown as they come in.
55
 *
56
 * @see IProvider::search() for the arguments of the individual search requests
57
 */
58
class SearchComposer {
59
60
	/** @var string[] */
61
	private $lazyProviders = [];
62
63
	/** @var IProvider[] */
64
	private $providers = [];
65
66
	/** @var IServerContainer */
67
	private $container;
68
69
	/** @var ILogger */
70
	private $logger;
71
72
	public function __construct(IServerContainer $container,
73
								ILogger $logger) {
74
		$this->container = $container;
75
		$this->logger = $logger;
76
	}
77
78
	/**
79
	 * Register a search provider lazily
80
	 *
81
	 * Registers the fully-qualified class name of an implementation of an
82
	 * IProvider. The service will only be queried on demand. Apps will register
83
	 * the providers through the registration context object.
84
	 *
85
	 * @see IRegistrationContext::registerSearchProvider()
86
	 *
87
	 * @param string $class
88
	 */
89
	public function registerProvider(string $class): void {
90
		$this->lazyProviders[] = $class;
91
	}
92
93
	/**
94
	 * Load all providers dynamically that were registered through `registerProvider`
95
	 *
96
	 * If a provider can't be loaded we log it but the operation continues nevertheless
97
	 */
98
	private function loadLazyProviders(): void {
99
		$classes = $this->lazyProviders;
100
		foreach ($classes as $class) {
101
			try {
102
				/** @var IProvider $provider */
103
				$provider = $this->container->query($class);
104
				$this->providers[$provider->getId()] = $provider;
105
			} catch (QueryException $e) {
106
				// Log an continue. We can be fault tolerant here.
107
				$this->logger->logException($e, [
108
					'message' => 'Could not load search provider dynamically: ' . $e->getMessage(),
109
					'level' => ILogger::ERROR,
110
				]);
111
			}
112
		}
113
		$this->lazyProviders = [];
114
	}
115
116
	/**
117
	 * Get a list of all provider IDs for the consecutive calls to `search`
118
	 *
119
	 * @return string[]
120
	 */
121
	public function getProviders(): array {
122
		$this->loadLazyProviders();
123
124
		/**
125
		 * Return an array with the IDs, but strip the associative keys
126
		 */
127
		return array_values(
128
			array_map(function (IProvider $provider) {
129
				return $provider->getId();
130
			}, $this->providers));
131
	}
132
133
	/**
134
	 * Query an individual search provider for results
135
	 *
136
	 * @param IUser $user
137
	 * @param string $providerId one of the IDs received by `getProviders`
138
	 * @param ISearchQuery $query
139
	 *
140
	 * @return SearchResult
141
	 * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider
142
	 */
143
	public function search(IUser $user,
144
						   string $providerId,
145
						   ISearchQuery $query): SearchResult {
146
		$this->loadLazyProviders();
147
148
		$provider = $this->providers[$providerId] ?? null;
149
		if ($provider === null) {
150
			throw new InvalidArgumentException("Provider $providerId is unknown");
151
		}
152
		return $provider->search($user, $query);
153
	}
154
}
155