info.magnolia.vaadin.periscope.Periscope.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.vaadin.periscope.Periscope.java

Source

/**
 * This file Copyright (c) 2017 Magnolia International
 * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the Magnolia
 * Network Agreement and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or MNA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Magnolia Network Agreement (MNA), this file
 * and the accompanying materials are made available under the
 * terms of the MNA which accompanies this distribution, and
 * is available at http://www.magnolia-cms.com/mna.html
 *
 * Any modifications to this file must keep this entire header
 * intact.
 *
 */
package info.magnolia.vaadin.periscope;

import info.magnolia.vaadin.periscope.order.NeuralNetworkManager;
import info.magnolia.vaadin.periscope.result.AsyncResultSupplier;
import info.magnolia.vaadin.periscope.result.Result;
import info.magnolia.vaadin.periscope.result.ResultSupplier;
import info.magnolia.vaadin.periscope.result.SearchFailedException;
import info.magnolia.vaadin.periscope.speech.BrowserSpeechRecognizer;
import info.magnolia.vaadin.periscope.speech.SpeechRecognizer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

import com.google.common.collect.Lists;
import com.vaadin.annotations.StyleSheet;
import com.vaadin.annotations.Widgetset;
import com.vaadin.event.ShortcutAction;
import com.vaadin.event.ShortcutListener;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.v7.ui.AbstractTextField;
import com.vaadin.v7.ui.TextField;
import com.vaadin.v7.ui.VerticalLayout;

/**
 * Periscope Vaadin component, which contains a search bar and shows live results while typing ahead.
 */
@StyleSheet("vaadin://addons/periscope/css/ionicons.min.css")
@Widgetset("com.vaadin.v7.Vaadin7WidgetSet")
public class Periscope extends VerticalLayout {

    private final Collection<ResultSupplier> resultSuppliers;
    private final Collection<AsyncResultSupplier> asyncResultSuppliers;
    private final Collection<CompletableFuture> runningAsyncSearches = new ArrayList<>();

    private final TextField input;
    private final ResultList resultList;
    private final SpeechRecognizer speechRecognizer;
    private final NeuralNetworkManager resultsNetworkManager;

    public Periscope(final Collection<ResultSupplier> resultSuppliers,
            final Collection<AsyncResultSupplier> asyncResultSuppliers) {
        this(resultSuppliers, asyncResultSuppliers, new BrowserSpeechRecognizer());
    }

    public Periscope(final Collection<ResultSupplier> resultSuppliers,
            final Collection<AsyncResultSupplier> asyncResultSuppliers, final SpeechRecognizer speechRecognizer) {
        super();

        this.resultSuppliers = resultSuppliers;
        this.asyncResultSuppliers = asyncResultSuppliers;
        this.speechRecognizer = speechRecognizer;
        this.resultsNetworkManager = new NeuralNetworkManager();

        this.addStyleName("periscope");

        input = new TextField();
        input.setInputPrompt("Type ahead to search...");
        input.addStyleName("search-field");
        this.addComponent(input);

        resultList = new ResultList();
        this.addComponent(resultList.getLayout());

        input.addTextChangeListener(event -> consumeQuery(event.getText(), false));
        input.setTextChangeEventMode(AbstractTextField.TextChangeEventMode.EAGER);

        input.addShortcutListener(
                createInputShortcut(ShortcutAction.KeyCode.ARROW_DOWN, () -> resultList.moveSelector(1)));
        input.addShortcutListener(
                createInputShortcut(ShortcutAction.KeyCode.ARROW_UP, () -> resultList.moveSelector(-1)));
        input.addShortcutListener(createInputShortcut(ShortcutAction.KeyCode.ENTER, () -> {
            final Optional<Result> selectedOrFirstResult = resultList.getSelectedOrFirstResult();
            try {
                selectedOrFirstResult.ifPresent(this::resultPicked);
            } catch (SearchFailedException e) {
                changeResultListToReflectException();
            }

            // TODO: Find a way to blur input (remove focus)
        }));
        input.addBlurListener(event -> resultList.clearSelector());

        resultList.onResultPick(this::resultPicked);

        this.addComponent(createSpeechButton());
    }

    private void resultPicked(Result result) {
        resultsNetworkManager.train(input.getValue(), result);
        result.getAction().run();
    }

    private synchronized void consumeQuery(final String query, final boolean autoExecuteFirst) {
        // FIXME: Properly use push instead
        getUI().setPollInterval(500);

        runningAsyncSearches.forEach(search -> search.cancel(true));

        resultList.clear();

        for (final ResultSupplier supplier : resultSuppliers) {
            List<Result> results = supplier.search(query);

            resultsNetworkManager.addResults(results);
            resultsNetworkManager.sort(query, results);

            if (autoExecuteFirst && !results.isEmpty()) {
                // typically a case of vocal command
                try {
                    results.get(0).getAction().run();
                }
                // TODO: shall be lesser scoped.
                catch (Exception e) {
                    changeResultListToReflectException();
                }
                return;
            }

            resultList.appendResults(supplier.getTitle(), results);
        }

        final AtomicBoolean autoExecuteDone = new AtomicBoolean(false);
        asyncResultSuppliers.forEach(supplier -> {
            resultList.showLoadingIcon();

            // XXX: Synchronize?
            final CompletableFuture<List<Result>> search = supplier.search(query);
            runningAsyncSearches.add(search);
            search.thenAccept(results -> {
                resultsNetworkManager.addResults(results);
                resultsNetworkManager.sort(query, results);

                resultList.hideLoadingIcon();
                runningAsyncSearches.remove(search);

                if (autoExecuteFirst && autoExecuteDone.get()) {
                    return;
                }

                if (autoExecuteFirst && !results.isEmpty()) {
                    // typically a case of vocal command
                    try {
                        results.get(0).getAction().run();
                    }
                    // TODO: shall be lesser scoped.
                    catch (Exception e) {
                        changeResultListToReflectException();
                    }
                    autoExecuteDone.set(true);
                    return;
                }

                resultList.appendResults(supplier.getTitle(), results);
                getUI().push();
            });
        });
    }

    private void changeResultListToReflectException() {
        resultList.clear();
        // TODO: Have to be more specific e.g. red etc.
        resultList.appendResults("Command result", Lists.newArrayList(new Result("Voice Command failed.", () -> {
        })));
    }

    private ShortcutListener createInputShortcut(final int key, final Runnable action) {
        return new ShortcutListener("Shortcut for key #" + key, key, null) {
            @Override
            public void handleAction(Object sender, Object target) {
                if (target != input) {
                    return;
                }

                action.run();
            }
        };
    }

    private Component createSpeechButton() {
        final Button startStopButton = new Button();
        startStopButton.addStyleName("record-button");
        startStopButton.setCaptionAsHtml(true);
        startStopButton.setCaption("<span class=\"ion-mic-a\"></span>");
        startStopButton.setClickShortcut(ShortcutAction.KeyCode.R, ShortcutAction.ModifierKey.SHIFT,
                ShortcutAction.ModifierKey.ALT);

        final AtomicBoolean isRecording = new AtomicBoolean(false);

        speechRecognizer.addSpeechResultListener(transcript -> {
            input.setValue(transcript);
            this.consumeQuery(transcript, true);

            startStopButton.removeStyleName("recording");
            isRecording.set(false);
        });

        startStopButton.addClickListener((Button.ClickListener) event -> {
            if (isRecording.get()) {
                return;
            }

            speechRecognizer.record();

            startStopButton.addStyleName("recording");
            isRecording.set(true);
        });

        final VerticalLayout speechWrapper = new VerticalLayout(startStopButton, speechRecognizer);
        speechWrapper.addStyleName("speech-recognition");
        return speechWrapper;
    }
}