SearchComposer.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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@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 OCP\AppFramework\QueryException;
  29. use OCP\IServerContainer;
  30. use OCP\IUser;
  31. use OCP\Search\IProvider;
  32. use OCP\Search\ISearchQuery;
  33. use OCP\Search\SearchResult;
  34. use OC\AppFramework\Bootstrap\Coordinator;
  35. use Psr\Log\LoggerInterface;
  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. private LoggerInterface $logger;
  64. public function __construct(Coordinator $bootstrapCoordinator,
  65. IServerContainer $container,
  66. LoggerInterface $logger) {
  67. $this->container = $container;
  68. $this->logger = $logger;
  69. $this->bootstrapCoordinator = $bootstrapCoordinator;
  70. }
  71. /**
  72. * Load all providers dynamically that were registered through `registerProvider`
  73. *
  74. * If a provider can't be loaded we log it but the operation continues nevertheless
  75. */
  76. private function loadLazyProviders(): void {
  77. $context = $this->bootstrapCoordinator->getRegistrationContext();
  78. if ($context === null) {
  79. // Too early, nothing registered yet
  80. return;
  81. }
  82. $registrations = $context->getSearchProviders();
  83. foreach ($registrations as $registration) {
  84. try {
  85. /** @var IProvider $provider */
  86. $provider = $this->container->query($registration->getService());
  87. $this->providers[$provider->getId()] = $provider;
  88. } catch (QueryException $e) {
  89. // Log an continue. We can be fault tolerant here.
  90. $this->logger->error('Could not load search provider dynamically: ' . $e->getMessage(), [
  91. 'exception' => $e,
  92. 'app' => $registration->getAppId(),
  93. ]);
  94. }
  95. }
  96. }
  97. /**
  98. * Get a list of all provider IDs & Names for the consecutive calls to `search`
  99. * Sort the list by the order property
  100. *
  101. * @param string $route the route the user is currently at
  102. * @param array $routeParameters the parameters of the route the user is currently at
  103. *
  104. * @return array
  105. */
  106. public function getProviders(string $route, array $routeParameters): array {
  107. $this->loadLazyProviders();
  108. $providers = array_values(
  109. array_map(function (IProvider $provider) use ($route, $routeParameters) {
  110. return [
  111. 'id' => $provider->getId(),
  112. 'name' => $provider->getName(),
  113. 'order' => $provider->getOrder($route, $routeParameters),
  114. ];
  115. }, $this->providers)
  116. );
  117. usort($providers, function ($provider1, $provider2) {
  118. return $provider1['order'] <=> $provider2['order'];
  119. });
  120. /**
  121. * Return an array with the IDs, but strip the associative keys
  122. */
  123. return $providers;
  124. }
  125. /**
  126. * Query an individual search provider for results
  127. *
  128. * @param IUser $user
  129. * @param string $providerId one of the IDs received by `getProviders`
  130. * @param ISearchQuery $query
  131. *
  132. * @return SearchResult
  133. * @throws InvalidArgumentException when the $providerId does not correspond to a registered provider
  134. */
  135. public function search(IUser $user,
  136. string $providerId,
  137. ISearchQuery $query): SearchResult {
  138. $this->loadLazyProviders();
  139. $provider = $this->providers[$providerId] ?? null;
  140. if ($provider === null) {
  141. throw new InvalidArgumentException("Provider $providerId is unknown");
  142. }
  143. return $provider->search($user, $query);
  144. }
  145. }