You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 lines
5.1 KiB

3 years ago
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
  5. *
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  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. use InvalidArgumentException;
  28. use OC\AppFramework\Bootstrap\Coordinator;
  29. use OCP\AppFramework\QueryException;
  30. use OCP\ILogger;
  31. use OCP\IServerContainer;
  32. use OCP\IUser;
  33. use OCP\Search\IProvider;
  34. use OCP\Search\ISearchQuery;
  35. use OCP\Search\SearchResult;
  36. use function array_map;
  37. /**
  38. * Queries individual \OCP\Search\IProvider implementations and composes a
  39. * unified search result for the user's search term
  40. *
  41. * The search process is generally split into two steps
  42. *
  43. * 1. Get a list of provider (`getProviders`)
  44. * 2. Get search results of each provider (`search`)
  45. *
  46. * The reasoning behind this is that the runtime complexity of a combined search
  47. * result would be O(n) and linearly grow with each provider added. This comes
  48. * from the nature of php where we can't concurrently fetch the search results.
  49. * So we offload the concurrency the client application (e.g. JavaScript in the
  50. * browser) and let it first get the list of providers to then fetch all results
  51. * concurrently. The client is free to decide whether all concurrent search
  52. * results are awaited or shown as they come in.
  53. *
  54. * @see IProvider::search() for the arguments of the individual search requests
  55. */
  56. class SearchComposer {
  57. /** @var IProvider[] */
  58. private $providers = [];
  59. /** @var Coordinator */
  60. private $bootstrapCoordinator;
  61. /** @var IServerContainer */
  62. private $container;
  63. /** @var ILogger */
  64. private $logger;
  65. public function __construct(Coordinator $bootstrapCoordinator,
  66. IServerContainer $container,
  67. ILogger $logger) {
  68. $this->container = $container;
  69. $this->logger = $logger;
  70. $this->bootstrapCoordinator = $bootstrapCoordinator;
  71. }
  72. /**
  73. * Load all providers dynamically that were registered through `registerProvider`
  74. *
  75. * If a provider can't be loaded we log it but the operation continues nevertheless
  76. */
  77. private function loadLazyProviders(): void {
  78. $context = $this->bootstrapCoordinator->getRegistrationContext();
  79. if ($context === null) {
  80. // Too early, nothing registered yet
  81. return;
  82. }
  83. $registrations = $context->getSearchProviders();
  84. foreach ($registrations as $registration) {
  85. try {
  86. /** @var IProvider $provider */
  87. $provider = $this->container->query($registration['class']);
  88. $this->providers[$provider->getId()] = $provider;
  89. } catch (QueryException $e) {
  90. // Log an continue. We can be fault tolerant here.
  91. $this->logger->logException($e, [
  92. 'message' => 'Could not load search provider dynamically: ' . $e->getMessage(),
  93. 'level' => ILogger::ERROR,
  94. ]);
  95. }
  96. }
  97. }
  98. /**
  99. * Get a list of all provider IDs & Names for the consecutive calls to `search`
  100. * Sort the list by the order property
  101. *
  102. * @param string $route the route the user is currently at
  103. * @param array $routeParameters the parameters of the route the user is currently at
  104. *
  105. * @return array
  106. */
  107. public function getProviders(string $route, array $routeParameters): array {
  108. $this->loadLazyProviders();
  109. $providers = array_values(
  110. array_map(function (IProvider $provider) use ($route, $routeParameters) {
  111. return [
  112. 'id' => $provider->getId(),
  113. 'name' => $provider->getName(),
  114. 'order' => $provider->getOrder($route, $routeParameters),
  115. ];
  116. }, $this->providers)
  117. );
  118. usort($providers, function ($provider1, $provider2) {
  119. return $provider1['order'] <=> $provider2['order'];
  120. });
  121. /**
  122. * Return an array with the IDs, but strip the associative keys
  123. */
  124. return $providers;
  125. }
  126. /**
  127. * Query an individual search provider for results
  128. *
  129. * @param IUser $user
  130. * @param string $providerId one of the IDs received by `getProviders`
  131. * @param ISearchQuery $query
  132. *
  133. * @return SearchResult
  134. * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider
  135. */
  136. public function search(IUser $user,
  137. string $providerId,
  138. ISearchQuery $query): SearchResult {
  139. $this->loadLazyProviders();
  140. $provider = $this->providers[$providerId] ?? null;
  141. if ($provider === null) {
  142. throw new InvalidArgumentException("Provider $providerId is unknown");
  143. }
  144. return $provider->search($user, $query);
  145. }
  146. }