View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.admincentral.findbar;
35  
36  import static info.magnolia.admincentral.ViewportLayout.VIEW.*;
37  import static java.util.stream.Collectors.*;
38  
39  import info.magnolia.admincentral.ViewportLayout;
40  import info.magnolia.admincentral.findbar.PeriscopeAppDetailCreator.AppDetail;
41  import info.magnolia.admincentral.findbar.layout.FilterLayout;
42  import info.magnolia.admincentral.findbar.search.ResultCollector;
43  import info.magnolia.admincentral.findbar.search.SupplierSorter;
44  import info.magnolia.cms.security.SecuritySupport;
45  import info.magnolia.cms.security.User;
46  import info.magnolia.config.registry.DefinitionProvider;
47  import info.magnolia.context.Context;
48  import info.magnolia.event.EventBus;
49  import info.magnolia.i18nsystem.SimpleTranslator;
50  import info.magnolia.icons.MagnoliaIcons;
51  import info.magnolia.jcr.util.NodeTypes;
52  import info.magnolia.periscope.Periscope;
53  import info.magnolia.periscope.SupplierAwareSearchResult;
54  import info.magnolia.periscope.ai.speech.SpeechRecognizer;
55  import info.magnolia.periscope.search.SearchQuery;
56  import info.magnolia.periscope.search.SearchResultSupplier;
57  import info.magnolia.periscope.search.jcr.JcrSearchResultSupplier;
58  import info.magnolia.periscope.search.jcr.JcrSearchResultSupplierDefinition;
59  import info.magnolia.periscope.tag.EmptyPeriscopeTagsProvider;
60  import info.magnolia.periscope.tag.PeriscopeTagsProvider;
61  import info.magnolia.ui.api.app.AppController;
62  import info.magnolia.ui.api.app.AppDescriptor;
63  import info.magnolia.ui.api.app.AppLifecycleEvent;
64  import info.magnolia.ui.api.app.AppLifecycleEventHandler;
65  import info.magnolia.ui.api.app.launcherlayout.AppLauncherGroupEntry;
66  import info.magnolia.ui.api.app.launcherlayout.AppLauncherLayout;
67  import info.magnolia.ui.api.app.launcherlayout.AppLauncherLayoutManager;
68  import info.magnolia.ui.api.app.registry.AppDescriptorRegistry;
69  import info.magnolia.ui.api.event.AdmincentralEventBus;
70  import info.magnolia.ui.api.location.DefaultLocation;
71  import info.magnolia.ui.api.location.Location;
72  import info.magnolia.ui.api.location.LocationController;
73  import info.magnolia.ui.api.view.View;
74  import info.magnolia.ui.theme.ResurfaceTheme;
75  import info.magnolia.ui.vaadin.applauncher.AppLauncher;
76  
77  import java.time.ZoneId;
78  import java.util.ArrayList;
79  import java.util.Collection;
80  import java.util.Collections;
81  import java.util.HashSet;
82  import java.util.List;
83  import java.util.Objects;
84  import java.util.Optional;
85  import java.util.Set;
86  import java.util.concurrent.atomic.AtomicBoolean;
87  
88  import javax.inject.Inject;
89  import javax.inject.Named;
90  import javax.inject.Provider;
91  
92  import org.apache.commons.collections4.CollectionUtils;
93  import org.apache.commons.lang3.StringUtils;
94  import org.slf4j.Logger;
95  import org.slf4j.LoggerFactory;
96  import org.vaadin.addons.ComboBoxMultiselect;
97  
98  import com.google.common.collect.Sets;
99  import com.vaadin.addon.daterangefield.DateRangeField;
100 import com.vaadin.event.ShortcutAction;
101 import com.vaadin.event.ShortcutListener;
102 import com.vaadin.ui.AbsoluteLayout;
103 import com.vaadin.ui.AbstractOrderedLayout;
104 import com.vaadin.ui.Button;
105 import com.vaadin.ui.Component;
106 import com.vaadin.ui.Label;
107 import com.vaadin.ui.Layout;
108 import com.vaadin.ui.TextField;
109 import com.vaadin.ui.UI;
110 import com.vaadin.ui.themes.ValoTheme;
111 import com.vaadin.v7.ui.AbstractField;
112 
113 /**
114  * The {@linkplain FindBar Find bar} brings federated search for content & apps throughout the whole Magnolia instance and repository.
115  *
116  * <p>This is powered by the {@link info.magnolia.periscope.Periscope Periscope} search engine, which offers pluggable
117  * {@linkplain info.magnolia.periscope.search.SearchResultSupplier Search result suppliers}.
118  */
119 public class FindBar implements View {
120 
121     private static final Logger log = LoggerFactory.getLogger(FindBar.class);
122 
123     private static final int DEFAULT_POLL_INTERVAL = -1;
124 
125     private final AbsoluteLayout findBarLayout = new AbsoluteLayout();
126 
127     private TextField textField;
128     private ComboBoxMultiselect<String> supplierField;
129     private ComboBoxMultiselect<String> tagField;
130     private ComboBoxMultiselect<String> editorField;
131     private DateRangeField dateRangeField;
132     SearchResultsGrid resultsGrid;
133     private Set<SearchResultSupplier> availableResultSuppliers;
134 
135     private final Periscope periscope;
136     private final PeriscopeAppDetailCreator appDetailCreator;
137     private final FindBarConfigurationProvider configProvider;
138     private final PeriscopeTagsProvider periscopeTagsProvider;
139     private final SecuritySupport securitySupport;
140     private final SimpleTranslator i18n;
141     private final EventBus eventBus;
142     private final User currentUser;
143 
144     private final LocationController locationController;
145     private final AppLauncher appLauncher;
146     private final AppLauncherLayoutManager appLauncherLayoutManager;
147     private final AppController appController;
148     private final ResultCollector resultCollector;
149     private final AppDescriptorRegistry appDescriptorRegistry;
150     private final SupplierSorter sorter;
151     private final SpeechRecognizer speechRecognizer;
152 
153     private SearchQuery latestQuery;
154 
155     private final ViewportLayout viewportLayout;
156 
157     @Inject
158     FindBar(Periscope periscope, SecuritySupport securitySupport, SpeechRecognizer speechRecognizer,
159             FindBarConfigurationProvider configProvider, PeriscopeAppDetailCreator appDetailCreator,
160             PeriscopeTagsProvider periscopeTagsProvider, @Named(AdmincentralEventBus.NAME) EventBus eventBus,
161             SimpleTranslator i18n, Provider<Context> context, LocationController locationController,
162             AppLauncherLayoutManager appLauncherLayoutManager, AppController appController, AppDescriptorRegistry appDescriptorRegistry,
163             ViewportLayout viewportLayout) {
164 
165         this.periscope = periscope;
166         this.appDetailCreator = appDetailCreator;
167         this.configProvider = configProvider;
168         this.periscopeTagsProvider = periscopeTagsProvider;
169         this.securitySupport = securitySupport;
170         this.i18n = i18n;
171         this.eventBus = eventBus;
172         this.currentUser = context.get().getUser();
173         this.locationController = locationController;
174         this.appLauncherLayoutManager = appLauncherLayoutManager;
175         this.appController = appController;
176         this.appLauncher = new AppLauncher(this::openApp);
177         this.appDescriptorRegistry = appDescriptorRegistry;
178         this.resultCollector = new ResultCollector(periscope::search, this::updateResults);
179         this.speechRecognizer = speechRecognizer;
180 
181         this.sorter = new SupplierSorter(Optional.ofNullable(configProvider.get().getSupplierOrder()).orElse(Collections.emptyList()));
182         this.viewportLayout = viewportLayout;
183         initUi();
184     }
185 
186     private synchronized void updateResults(SearchQuery searchQuery, List<SupplierAwareSearchResult> results) {
187         findUi(this.asVaadinComponent()).access(() -> {
188             resultsGrid.setCount(StringUtils.isEmpty(searchQuery.getQuery()), results.size());
189             resultsGrid.setSearchResults(results);
190         });
191     }
192 
193     private UI findUi(Component component) {
194         Component current = component;
195         while (!(current instanceof UI) && current != null) {
196             current = current.getParent();
197         }
198         if (current == null) {
199             throw new IllegalStateException("Top-level UI component could not be found");
200         }
201         return (UI) current;
202     }
203 
204     void initUi() {
205         Set<String> configuredSuppliers = configProvider.get().getSuppliers();
206         availableResultSuppliers = CollectionUtils.isEmpty(configuredSuppliers) ? new HashSet<>(periscope.getResultSuppliers())
207                 : Sets.filter(periscope.getResultSuppliers(), s -> configuredSuppliers.contains(s.getName()));
208 
209         Set<SearchResultSupplier> generatedSearchResultSuppliers = initialiseJCRSearchResultSuppliers(availableResultSuppliers);
210         availableResultSuppliers.addAll(generatedSearchResultSuppliers);
211 
212         AppLauncherLayout userLayout = this.appLauncherLayoutManager.getLayoutForUser(currentUser);
213         List<SearchResultSupplier> inaccessibleAppSuppliers = availableResultSuppliers.stream()
214                 .filter(supplier -> isForbiddenApp(supplier, userLayout))
215                 .collect(toList());
216         availableResultSuppliers.removeAll(inaccessibleAppSuppliers);
217 
218         Set<String> availableSupplierNames = availableResultSuppliers.stream()
219                 .map(SearchResultSupplier::getName)
220                 .collect(toSet());
221 
222         if (CollectionUtils.isNotEmpty(configuredSuppliers)) {
223             Sets.difference(configuredSuppliers, availableSupplierNames).forEach(supplier -> log.warn("Search result supplier enabled in config, but not found: {}", supplier));
224         }
225 
226         this.textField = new TextField();
227         this.textField.setSizeFull();
228         this.textField.addStyleNames("findbar");
229         this.textField.setPlaceholder(i18n.translate("findbar.placeholder"));
230         this.textField.addAttachListener(event -> search());
231         this.textField.focus();
232 
233         this.supplierField = new ComboBoxMultiselect<>(i18n.translate("findbar.type"));
234         this.supplierField.setItems(availableSupplierNames);
235 
236         this.tagField = new ComboBoxMultiselect<>(i18n.translate("findbar.tags"));
237         this.tagField.setItems(periscopeTagsProvider.getAllTags());
238 
239         this.editorField = new ComboBoxMultiselect<>(i18n.translate("findbar.lasteditor"));
240         this.editorField.setItems(resolveUsers());
241         this.dateRangeField = new DateRangeField(i18n.translate("findbar.lastedited"));
242 
243         this.resultsGrid = new SearchResultsGrid(i18n);
244         this.resultsGrid.addSelectionCallback(searchResult -> this.periscope.resultPicked(buildSearchQuery(), searchResult));
245 
246         FilterLayout filterLayout = periscopeTagsProvider instanceof EmptyPeriscopeTagsProvider ? new FilterLayout(dateRangeField, i18n, supplierField, editorField) : new FilterLayout(dateRangeField, i18n, supplierField, tagField, editorField);
247         Layout resultsLayout = viewportLayout.getFindBarResultsLayout();
248         resultsLayout.addComponents(filterLayout, resultsGrid);
249         resultsGrid.setSizeFull();
250         resultsLayout.addStyleName("filter-and-results");
251 
252         userLayout.getGroups().forEach(group -> {
253             appLauncher.addAppGroup(group.getName(), group.getLabel(), group.getColor(), true, group.isClientGroup());
254             group.getApps().stream().map(AppLauncherGroupEntry::getAppDescriptor).forEach(descriptor -> {
255                 appLauncher.addAppTile(descriptor.getName(), descriptor.getLabel(), descriptor.getIcon(), group.getName());
256                 appLauncher.setAppActive(descriptor.getName(), appController.isAppStarted(descriptor.getName()));
257             });
258         });
259 
260         final Label title = new Label("Apps");
261         title.addStyleName(ValoTheme.LABEL_H1);
262 
263         AbstractOrderedLayout appLauncherLayout = viewportLayout.getAppLauncherLayout();
264         appLauncher.setSizeFull();
265         appLauncherLayout.setSizeFull();
266         appLauncherLayout.addComponents(title, appLauncher);
267         appLauncherLayout.setExpandRatio(appLauncher, 1f);
268         appLauncherLayout.addStyleName("v-app-launcher");
269 
270         appController.setViewport(view -> {
271             Component content = view == null ? null : view.asVaadinComponent();
272             viewportLayout.getAppLayout().setContent(content);
273         });
274 
275         this.supplierField.addValueChangeListener(event -> search(true));
276         this.tagField.addValueChangeListener(event -> search(true));
277         this.editorField.addValueChangeListener(event -> search(true));
278         this.dateRangeField.addValueChangeListener(event -> search(true));
279 
280         Button findBarButton = new Button(MagnoliaIcons.SEARCH);
281         findBarButton.addStyleNames(ResurfaceTheme.BUTTON_ICON, "clear");
282         findBarButton.addClickListener(event -> {
283             viewportLayout.toggleFindBar();
284             if (viewportLayout.getActiveView() == FIND_BAR) {
285                 findBarButton.addStyleName("active");
286                 textField.focus();
287             } else {
288                 findBarButton.removeStyleName("active");
289             }
290         });
291 
292         Button clearTextButton = new Button(MagnoliaIcons.DELETE_SEARCH);
293         clearTextButton.addStyleNames(ResurfaceTheme.BUTTON_ICON, "clear");
294         clearTextButton.setVisible(false);
295         clearTextButton.addClickListener(event -> {
296             textField.clear();
297             textField.focus();
298             clearTextButton.setVisible(false);
299         });
300 
301         Button voiceButton = new Button(MagnoliaIcons.MICROPHONE);
302         voiceButton.addStyleNames(ResurfaceTheme.BUTTON_ICON, "voice");
303 
304         // App launcher
305         Button appLauncherButton = new Button(MagnoliaIcons.APP_LAUNCHER);
306         appLauncherButton.addStyleNames(ResurfaceTheme.BUTTON_ICON, "appLauncher");
307         appLauncherButton.addClickListener(event ->  {
308             findBarButton.removeStyleName("active");
309             viewportLayout.toggleAppLauncher();
310             appLauncherButton.setStyleName("active",viewportLayout.getActiveView() == APP_LAUNCHER);
311         });
312         appLauncherButton.addBlurListener(event -> appLauncherButton.removeStyleName("active"));
313 
314         AtomicBoolean textFieldHasFocus = new AtomicBoolean(false);
315         this.textField.addFocusListener(event -> {
316             viewportLayout.showFindBar();
317             textFieldHasFocus.set(true);
318             findBarButton.addStyleName("active");
319             appLauncherButton.removeStyleName("active");
320         });
321         this.textField.addBlurListener(event -> {
322             textFieldHasFocus.set(false);
323             findBarButton.removeStyleName("active");
324         });
325 
326         final ShortcutListener enterListener = new ShortcutListener("Enter", ShortcutAction.KeyCode.ENTER, null) {
327             @Override
328             public void handleAction(Object sender, Object target) {
329                 if (textFieldHasFocus.get()) {
330                     resultsGrid.triggerSelected();
331                 }
332             }
333         };
334         this.textField.addFocusListener((e) -> this.textField.addShortcutListener(enterListener));
335         this.textField.addBlurListener((e) -> this.textField.removeShortcutListener(enterListener));
336 
337         // TODO: Use non-deprecated alternative once this is resolved: https://github.com/vaadin/framework/issues/8297 (fix should come with Vaadin 8.7.0)
338         this.textField.addShortcutListener(new AbstractField.FocusShortcut(textField,
339                 ShortcutAction.KeyCode.F, ShortcutAction.ModifierKey.SHIFT, ShortcutAction.ModifierKey.ALT));
340 
341         // be a bit more eager than the default 400 ms to make live type-ahead search feel more responsive
342         this.textField.setValueChangeTimeout(200);
343         this.textField.addValueChangeListener(event -> {
344             if (StringUtils.isBlank(event.getValue())) {
345                 clearTextButton.setVisible(false);
346             } else {
347                 clearTextButton.setVisible(true);
348             }
349             search(true);
350         });
351 
352         // setting absolution positions below is not equivalent to putting them in CSS; they get applied to a wrapping slot
353         findBarLayout.addComponent(textField);
354         findBarLayout.addComponent(findBarButton, "top: 4px; left: 8px;");
355         findBarLayout.addComponent(clearTextButton, "top: 6px; right: 40px;");
356         findBarLayout.addComponent(voiceButton, "top: 6px; right: 40px;");
357         findBarLayout.addComponent(appLauncherButton, "top: 6px; right: 8px;");
358         findBarLayout.addStyleNames("findbar");
359 
360         initBrowserSpeechRecognition(voiceButton, () -> {
361             voiceButton.setVisible(true);
362             // push clear-text button further away
363             AbsoluteLayout.ComponentPosition clearPosition = findBarLayout.getPosition(clearTextButton);
364             clearPosition.setCSSString("top: 6px; right: 72px;");
365         });
366 
367         eventBus.addHandler(AppLifecycleEvent.class, new AppLifecycleEventHandler.Adapter() {
368 
369             @Override
370             public void onAppFocused(AppLifecycleEvent event) {
371                 log.debug("#onAppFocused: {}", event.getAppDescriptor().getName());
372                 updateFiltersForApp(event.getAppDescriptor().getName());
373                 viewportLayout.showApp();
374             }
375 
376             @Override
377             public void onAppStopped(AppLifecycleEvent event) {
378                 log.debug("#onAppStopped: {}", event.getAppDescriptor().getName());
379                 supplierField.deselectAll();
380                 appLauncher.setAppActive(event.getAppDescriptor().getName(), false);
381                 filterLayout.updateGlobalTokenLayout();
382                 // We do yet another search to fetch the latest suggestions.
383                 search();
384                 viewportLayout.hideApp();
385             }
386 
387             @Override
388             public void onAppStarted(AppLifecycleEvent event) {
389                 appLauncher.setAppActive(event.getAppDescriptor().getName(), true);
390                 log.debug("#onAppStarted: {}", event.getAppDescriptor().getName());
391                 updateFiltersForApp(event.getAppDescriptor().getName());
392                 viewportLayout.showApp();
393             }
394 
395             private void updateFiltersForApp(String appName) {
396                 if (supplierField.getValue().contains(appName)) {
397                     return;
398                 }
399 
400                 supplierField.deselectAll();
401                 if (availableSupplierNames.contains(appName)) {
402                     supplierField.select(appName);
403                     textField.clear();
404                 }
405                 filterLayout.updateGlobalTokenLayout();
406             }
407         });
408     }
409 
410     private void openApp(String appName) {
411         locationController.goTo(new DefaultLocation(Location.LOCATION_TYPE_APP, appName));
412         viewportLayout.showApp();
413     }
414 
415     private void initBrowserSpeechRecognition(Button voiceButton, SpeechRecognizer.Listener supportedListener) {
416         speechRecognizer.attachTo(findBarLayout);
417 
418         // invisible until front-end declares it supported, only in chrome as of right now
419         voiceButton.setVisible(false);
420         speechRecognizer.setSupportedListener(supportedListener);
421 
422         final AtomicBoolean isRecording = new AtomicBoolean(false);
423 
424         speechRecognizer.addSpeechResultListener(transcript -> {
425             textField.setValue(transcript);
426             voiceButton.removeStyleName("recording");
427             isRecording.set(false);
428         });
429 
430         voiceButton.addClickListener((Button.ClickListener) event -> {
431             if (isRecording.get()) {
432                 return;
433             }
434 
435             voiceButton.addStyleName("recording");
436             isRecording.set(true);
437             speechRecognizer.record();
438         });
439     }
440 
441     /**
442      * Fetches all definitions providers from {@link AppDescriptorRegistry} and initializes required
443      * {@linkplain JcrSearchResultSupplier result suppliers} from it.
444      * Skips the ones which are already present.
445      */
446     private Set<SearchResultSupplier> initialiseJCRSearchResultSuppliers(Set<SearchResultSupplier> definedResultSuppliers) {
447         Set<String> definedSupplierNames = definedResultSuppliers.stream()
448                 .map(SearchResultSupplier::getName).collect(toSet());
449 
450         return appDescriptorRegistry.getAllProviders().stream()
451                 .filter(DefinitionProvider::isValid)
452                 .filter(provider -> definedSupplierNames.stream().noneMatch(name -> name.equals(provider.get().getName())))
453                 .map(definitionProvider -> toSearchResultSupplier(definitionProvider.get()))
454                 .filter(Objects::nonNull)
455                 .collect(toSet());
456     }
457 
458     private SearchResultSupplier toSearchResultSupplier(AppDescriptor appDescriptor) {
459         JcrSearchResultSupplierDefinition definition = new JcrSearchResultSupplierDefinition();
460         Optional<AppDetail> appDetailOptional = appDetailCreator.createAppDetail(appDescriptor.getName());
461         if (!appDetailOptional.isPresent()) {
462             return null;
463         }
464 
465         AppDetail appDetail = appDetailOptional.get();
466         if (StringUtils.isBlank(appDetail.getWorkspace())) {
467             return null;
468         }
469         String workspace = appDetail.getWorkspace();
470 
471         definition.setAppName(appDescriptor.getName());
472         definition.setWorkspace(workspace);
473         // There are some cases which we cannot find nodeTypes from the contentConnector, hence we search for all.
474         if (CollectionUtils.isEmpty(appDetail.getNodeTypes())) {
475             definition.setNodeTypes(Sets.newHashSet(NodeTypes.Content.NAME));
476         } else {
477             definition.setNodeTypes(appDetail.getNodeTypes());
478         }
479         definition.setEnabled(appDescriptor.isEnabled());
480         definition.setIcon(appDescriptor.getIcon());
481 
482         return definition.getStrategy().construct(appDescriptor.getName(), definition);
483     }
484 
485     @Override
486     public Component asVaadinComponent() {
487         return findBarLayout;
488     }
489 
490     /**
491      * Detect whether a given supplier corresponds to an app disabled for the current user through permissions.
492      */
493     private boolean isForbiddenApp(SearchResultSupplier supplier, AppLauncherLayout userLayout) {
494         if (!(supplier instanceof JcrSearchResultSupplier)) {
495             return false;
496         }
497 
498         if (appDetailCreator == null) {
499             log.warn("No app detail creator provided - find bar might have problems handling JCR suppliers correctly");
500             return false;
501         }
502 
503         JcrSearchResultSupplier jcrSupplier = (JcrSearchResultSupplier) supplier;
504         Optional<AppDetail> appDetail = appDetailCreator.createAppDetailForWorkspace(jcrSupplier.getWorkspace());
505         return appDetail.filter(appDetailConfig -> !userLayout.containsApp(appDetailConfig.getAppName()))
506                 .isPresent();
507     }
508 
509     protected synchronized void search() {
510         search(false);
511     }
512 
513     protected synchronized void search(boolean forceSearch) {
514         resultsGrid.clear();
515 
516         SearchQuery searchQuery = buildSearchQuery();
517 
518         if (!forceSearch && searchQuery.equals(latestQuery)) {
519             return;
520         }
521         latestQuery = searchQuery;
522 
523         FindBarConfiguration findBarConfiguration = configProvider.get();
524         List<String> configuredSuppliers = findBarConfiguration.getSupplierOrder();
525         // Going back to default 10 items for usual search use case.
526         searchQuery.setLimitPerSupplier(findBarConfiguration.getDefaultCountPerSupplier());
527 
528         // We pass the current query to all registered sniffers (which will decide whether they take action or not)
529         if (forceSearch && StringUtils.isNotBlank(searchQuery.getQuery())) {
530             boolean succeeded = periscope.sniffQuery(searchQuery.getQuery());
531             if (succeeded) {
532                 return;
533             }
534         } else {
535             // Query is empty which means we should pull suggestions.
536             // Using default '3' items for suggestions
537             searchQuery.setLimitPerSupplier(findBarConfiguration.getSuggestionCountPerSupplier());
538         }
539 
540         Set<String> selectedSuppliers = this.supplierField.getValue();
541         Set<SearchResultSupplier> suppliers = CollectionUtils.isEmpty(selectedSuppliers) ? availableResultSuppliers :
542                 Sets.filter(availableResultSuppliers, e -> selectedSuppliers.contains(e.getName()));
543 
544         if (StringUtils.isBlank(searchQuery.getQuery()) && CollectionUtils.isNotEmpty(configuredSuppliers)) {
545             suppliers = suppliers.stream()
546                     .filter(supplier -> configuredSuppliers.contains(supplier.getName()))
547                     .collect(toSet());
548         }
549 
550         // If there are searches to execute, we enable polling because results are supplied to front end in batches.
551         final UI currentUi = UI.getCurrent();
552         if (suppliers.size() > 0) {
553             currentUi.setPollInterval(100);
554             resultsGrid.addStyleName("loading");
555         }
556 
557         final List<SearchResultSupplier> sortedSuppliers = new ArrayList<>(suppliers);
558         sorter.sort(sortedSuppliers);
559 
560         resultCollector.search(searchQuery, sortedSuppliers, () -> {
561             // only reset polling if no subsequent search has already been triggered
562             if (latestQuery == searchQuery) {
563                 currentUi.access(() -> {
564                     viewportLayout.disableAnimations();
565                     currentUi.setPollInterval(DEFAULT_POLL_INTERVAL);
566                     resultsGrid.removeStyleName("loading");
567                 });
568             }
569         });
570     }
571 
572     private SearchQuery buildSearchQuery() {
573         SearchQuery.SearchQueryBuilder searchQueryBuilder = SearchQuery.builder()
574                 .query(this.textField.getValue())
575                 .tags(this.tagField.getValue())
576                 .editors(this.editorField.getValue())
577                 .currentUser(this.currentUser);
578 
579         //TODO: Those ideally should come via user profile!
580         if (this.dateRangeField.getBeginDateField().getValue() != null) {
581             searchQueryBuilder.startDate(this.dateRangeField.getBeginDateField().getValue().atStartOfDay(ZoneId.systemDefault()));
582         }
583         if (this.dateRangeField.getEndDateField().getValue() != null) {
584             searchQueryBuilder.endDate(this.dateRangeField.getEndDateField().getValue().atStartOfDay(ZoneId.systemDefault()));
585         }
586 
587         return searchQueryBuilder.build();
588     }
589 
590     private List<String> resolveUsers() {
591         Collection<User> allUsers = securitySupport.getUserManager().getAllUsers();
592         return allUsers.stream()
593                 .map(User::getName)
594                 .collect(toList());
595     }
596 
597     /**
598      * Package visibility for testing purposes only.
599      */
600     TextField getTextField() {
601         return textField;
602     }
603 
604     /**
605      * Package visibility for testing purposes only.
606      */
607     ComboBoxMultiselect<String> getSupplierField() {
608         return supplierField;
609     }
610 }