annis.gui.QueryController.java Source code

Java tutorial

Introduction

Here is the source code for annis.gui.QueryController.java

Source

/*
 * Copyright 2014 Corpuslinguistic working group Humboldt University Berlin.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package annis.gui;

import annis.gui.components.ExceptionDialog;
import annis.gui.controller.CountCallback;
import annis.gui.controller.ExportBackgroundJob;
import annis.gui.controller.FrequencyBackgroundJob;
import annis.gui.controller.SpecificPagingCallback;
import annis.gui.controlpanel.QueryPanel;
import annis.gui.controlpanel.SearchOptionsPanel;
import annis.gui.exporter.Exporter;
import annis.gui.frequency.FrequencyQueryPanel;
import annis.gui.frequency.UserGeneratedFrequencyEntry;
import annis.gui.objects.ContextualizedQuery;
import annis.gui.objects.DisplayedResultQuery;
import annis.gui.objects.ExportQuery;
import annis.gui.objects.FrequencyQuery;
import annis.gui.objects.PagedResultQuery;
import annis.gui.objects.Query;
import annis.gui.objects.QueryGenerator;
import annis.gui.objects.QueryUIState;
import annis.gui.resultfetch.ResultFetchJob;
import annis.gui.resultfetch.SingleResultFetchJob;
import annis.gui.resultview.ResultViewPanel;
import annis.gui.resultview.VisualizerContextChanger;
import annis.libgui.Background;
import annis.libgui.Helper;
import annis.libgui.media.MediaController;
import annis.libgui.visualizers.IFrameResourceMap;
import annis.model.AqlParseError;
import annis.model.QueryNode;
import annis.service.objects.CorpusConfig;
import annis.service.objects.FrequencyTableEntry;
import annis.service.objects.FrequencyTableEntryType;
import annis.service.objects.FrequencyTableQuery;
import annis.service.objects.Match;
import annis.service.objects.MatchAndDocumentCount;
import com.google.common.base.Joiner;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.FutureCallback;
import com.sun.jersey.api.client.AsyncWebResource;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.GenericType;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.vaadin.data.Property;
import com.vaadin.data.util.BeanContainer;
import com.vaadin.server.FontAwesome;
import com.vaadin.server.VaadinSession;
import com.vaadin.ui.Component;
import com.vaadin.ui.Notification;
import com.vaadin.ui.TabSheet;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import org.apache.commons.lang3.StringUtils;
import org.corpus_tools.salt.common.SaltProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A controller to modifiy the query UI state.
 *
 * @author Thomas Krause <krauseto@hu-berlin.de>
 */
public class QueryController implements Serializable {

    private static final Logger log = LoggerFactory.getLogger(QueryController.class);

    private final SearchView searchView;
    private final AnnisUI ui;

    private final QueryUIState state;

    private final Map<String, Exporter> exporterMap = new HashMap<>();

    public QueryController(SearchView searchView, AnnisUI ui) {
        this.searchView = searchView;
        this.ui = ui;
        this.state = ui.getQueryState();

        this.state.getAql().addValueChangeListener(new Property.ValueChangeListener() {

            @Override
            public void valueChange(Property.ValueChangeEvent event) {
                validateQuery();
            }
        });

        this.state.getSelectedCorpora().addValueChangeListener(new Property.ValueChangeListener() {
            @Override
            public void valueChange(Property.ValueChangeEvent event) {
                validateQuery();
            }
        });

        for (Exporter e : SearchView.EXPORTER) {
            String name = e.getClass().getSimpleName();
            exporterMap.put(name, e);
        }

        this.state.getSelectedCorpora().addValueChangeListener(new Property.ValueChangeListener() {

            @Override
            public void valueChange(Property.ValueChangeEvent event) {
                // TODO: check if the corpus is actually available to the user
            }
        });
    }

    public void validateQuery() {
        QueryPanel qp = searchView.getControlPanel().getQueryPanel();

        // reset status
        qp.setErrors(null);
        qp.setNodes(null);

        String query = state.getAql().getValue();
        if (query == null || query.isEmpty()) {
            qp.setStatus("Empty query");

        } else {
            // validate query
            try {
                AsyncWebResource annisResource = Helper.getAnnisAsyncWebResource();
                Future<List<QueryNode>> future = annisResource.path("query").path("parse/nodes")
                        .queryParam("q", Helper.encodeJersey(query)).get(new GenericType<List<QueryNode>>() {
                        });

                // wait for maximal one seconds
                try {
                    List<QueryNode> nodes = future.get(1, TimeUnit.SECONDS);

                    qp.setNodes(nodes);

                    if (state.getSelectedCorpora().getValue() == null
                            || state.getSelectedCorpora().getValue().isEmpty()) {
                        qp.setStatus("Please select a corpus from the list below, then click on \"Search\".");
                    } else {
                        qp.setStatus("Valid query, click on \"Search\" to start searching.");
                    }

                } catch (InterruptedException ex) {
                    log.warn(null, ex);
                } catch (ExecutionException ex) {
                    if (ex.getCause() instanceof UniformInterfaceException) {
                        reportServiceException((UniformInterfaceException) ex.getCause(), false);
                    } else {
                        // something unknown, report
                        ExceptionDialog.show(ex);
                    }
                } catch (TimeoutException ex) {
                    qp.setStatus("Validation of query took too long.");
                }

            } catch (ClientHandlerException ex) {
                log.error("Could not connect to web service", ex);
                ExceptionDialog.show(ex, "Could not connect to web service");
            }
        }
    }

    /**
     * Show errors that occured during the execution of a query to the user.
     *
     * @param ex The exception to report in the user interface
     * @param showNotification If true a notification is shown instead of only
     * displaying the error in the status label.
     */
    public void reportServiceException(UniformInterfaceException ex, boolean showNotification) {
        QueryPanel qp = searchView.getControlPanel().getQueryPanel();

        String caption = null;
        String description = null;

        if (ex.getResponse().getStatus() == 400) {
            List<AqlParseError> errors = ex.getResponse().getEntity(new GenericType<List<AqlParseError>>() {
            });
            caption = "Parsing error";
            description = Joiner.on("\n").join(errors);
            qp.setStatus(description);
            qp.setErrors(errors);
        } else if (ex.getResponse().getStatus() == 504) {
            caption = "Timeout";
            description = "Query execution took too long.";
            qp.setStatus(caption + ": " + description);
        } else if (ex.getResponse().getStatus() == 403) {

            if (Helper.getUser() == null) {
                // not logged in
                qp.setStatus("You don't have the access rights to query this corpus. "
                        + "You might want to login to access more corpora.");
                searchView.getMainToolbar().showLoginWindow(true);
            } else {
                // logged in but wrong user
                caption = "You don't have the access rights to query this corpus. "
                        + "You might want to login as another user to access more corpora.";
                qp.setStatus(caption);
            }
        } else {
            log.error("Exception when communicating with service", ex);
            qp.setStatus("Unexpected exception:  " + ex.getMessage());
            ExceptionDialog.show(ex, "Exception when communicating with service.");
        }

        if (showNotification && caption != null) {
            Notification.show(caption, description, Notification.Type.WARNING_MESSAGE);
        }

    }

    /**
     * Only changes the value of the property if it is not equals to the old one.
     * @param <T>
     * @param prop
     * @param newValue 
     */
    private static <T> void setIfNew(Property<T> prop, T newValue) {
        if (!Objects.equals(prop.getValue(), newValue)) {
            prop.setValue(newValue);
        }
    }

    public void setQuery(Query q) {
        // only change the values if actually changed (the value change listeners should not be triggered if not necessary)
        setIfNew(state.getAql(), q.getQuery());
        setIfNew(state.getSelectedCorpora(), q.getCorpora());

        if (q instanceof ContextualizedQuery) {
            setIfNew(state.getLeftContext(), ((ContextualizedQuery) q).getLeftContext());
            setIfNew(state.getRightContext(), ((ContextualizedQuery) q).getRightContext());
            setIfNew(state.getContextSegmentation(), ((ContextualizedQuery) q).getSegmentation());
        }
        if (q instanceof PagedResultQuery) {
            setIfNew(state.getOffset(), ((PagedResultQuery) q).getOffset());
            setIfNew(state.getLimit(), ((PagedResultQuery) q).getLimit());
            setIfNew(state.getOrder(), ((PagedResultQuery) q).getOrder());
        }
        if (q instanceof DisplayedResultQuery) {
            setIfNew(state.getSelectedMatches(), ((DisplayedResultQuery) q).getSelectedMatches());
            setIfNew(state.getVisibleBaseText(), ((DisplayedResultQuery) q).getBaseText());
        }
        if (q instanceof ExportQuery) {
            setIfNew(state.getExporterName(), ((ExportQuery) q).getExporterName());
            setIfNew(state.getExportAnnotationKeys(), ((ExportQuery) q).getAnnotationKeys());
            setIfNew(state.getExportParameters(), ((ExportQuery) q).getParameters());
        }
    }

    /**
     * Get the current query as it is defined by the current {@link QueryUIState}.
     *
     * @return
     */
    public DisplayedResultQuery getSearchQuery() {
        return QueryGenerator.displayed().query(state.getAql().getValue())
                .corpora(state.getSelectedCorpora().getValue()).left(state.getLeftContext().getValue())
                .right(state.getRightContext().getValue()).segmentation(state.getContextSegmentation().getValue())
                .baseText(state.getVisibleBaseText().getValue()).limit(state.getLimit().getValue())
                .offset(state.getOffset().getValue()).order(state.getOrder().getValue())
                .selectedMatches(state.getSelectedMatches().getValue()).build();
    }

    /**
     * Get the current query as it is defined by the UI controls.
     *
     * @return
     */
    public ExportQuery getExportQuery() {
        return QueryGenerator.export().query(state.getAql().getValue())
                .corpora(state.getSelectedCorpora().getValue()).left(state.getLeftContext().getValue())
                .right(state.getRightContext().getValue()).segmentation(state.getVisibleBaseText().getValue())
                .exporter(state.getExporterName().getValue())
                .annotations(state.getExportAnnotationKeys().getValue())
                .param(state.getExportParameters().getValue()).build();
    }

    /**
     * Executes a query.
     * @param replaceOldTab
     * @param freshQuery If true the offset and the selected matches are reset before executing the query. 
     */
    public void executeSearch(boolean replaceOldTab, boolean freshQuery) {
        if (freshQuery) {
            getState().getOffset().setValue(0l);
            getState().getSelectedMatches().setValue(new TreeSet<Long>());
            // get the value for the visible segmentation from the configured context
            Set<String> selectedCorpora = getState().getSelectedCorpora().getValue();
            CorpusConfig config = new CorpusConfig();
            if (selectedCorpora != null && !selectedCorpora.isEmpty()) {
                config = ui.getCorpusConfigWithCache(selectedCorpora.iterator().next());
            }

            if (config.containsKey(SearchOptionsPanel.KEY_DEFAULT_BASE_TEXT_SEGMENTATION)) {
                String configVal = config.getConfig(SearchOptionsPanel.KEY_DEFAULT_BASE_TEXT_SEGMENTATION);
                if ("".equals(configVal) || "tok".equals(configVal)) {
                    configVal = null;
                }
                getState().getVisibleBaseText().setValue(configVal);
            } else {
                getState().getVisibleBaseText().setValue(getState().getContextSegmentation().getValue());
            }
        }
        // construct a query from the current properties
        DisplayedResultQuery displayedQuery = getSearchQuery();

        searchView.getControlPanel().getQueryPanel().setStatus("Searching...");

        cancelSearch();

        // cleanup resources
        VaadinSession session = VaadinSession.getCurrent();
        session.setAttribute(IFrameResourceMap.class, new IFrameResourceMap());
        if (session.getAttribute(MediaController.class) != null) {
            session.getAttribute(MediaController.class).clearMediaPlayers();
        }

        searchView.updateFragment(displayedQuery);

        if (displayedQuery.getCorpora() == null || displayedQuery.getCorpora().isEmpty()) {
            Notification.show("Please select a corpus", Notification.Type.WARNING_MESSAGE);
            return;
        }
        if ("".equals(displayedQuery.getQuery())) {
            Notification.show("Empty query", Notification.Type.WARNING_MESSAGE);
            return;
        }

        addHistoryEntry(displayedQuery);

        AsyncWebResource res = Helper.getAnnisAsyncWebResource();

        //
        // begin execute match fetching
        //
        ResultViewPanel oldPanel = searchView.getLastSelectedResultView();
        if (replaceOldTab) {
            // remove old panel from view
            searchView.closeTab(oldPanel);
        }

        ResultViewPanel newResultView = new ResultViewPanel(ui, ui, ui.getInstanceConfig(), displayedQuery);
        newResultView.getPaging()
                .addCallback(new SpecificPagingCallback(ui, searchView, newResultView, displayedQuery));

        TabSheet.Tab newTab;

        List<ResultViewPanel> existingResultPanels = getResultPanels();
        String caption = existingResultPanels.isEmpty() ? "Query Result"
                : "Query Result #" + (existingResultPanels.size() + 1);

        newTab = searchView.getMainTab().addTab(newResultView, caption);
        newTab.setClosable(true);
        newTab.setIcon(FontAwesome.SEARCH);

        searchView.getMainTab().setSelectedTab(newResultView);
        searchView.notifiyQueryStarted();

        Background.run(new ResultFetchJob(displayedQuery, newResultView, ui));

        //
        // end execute match fetching
        //
        // 
        // begin execute count
        //
        // start count query
        searchView.getControlPanel().getQueryPanel().setCountIndicatorEnabled(true);

        AsyncWebResource countRes = res.path("query").path("search").path("count")
                .queryParam("q", Helper.encodeJersey(displayedQuery.getQuery()))
                .queryParam("corpora", Helper.encodeJersey(StringUtils.join(displayedQuery.getCorpora(), ",")));
        Future<MatchAndDocumentCount> futureCount = countRes.get(MatchAndDocumentCount.class);
        state.getExecutedTasks().put(QueryUIState.QueryType.COUNT, futureCount);

        Background.run(new CountCallback(newResultView, displayedQuery.getLimit(), ui));

        //
        // end execute count
        //
    }

    public void executeExport(ExportPanel panel, EventBus eventBus) {

        Future exportFuture = state.getExecutedTasks().get(QueryUIState.QueryType.EXPORT);
        if (exportFuture != null && !exportFuture.isDone()) {
            exportFuture.cancel(true);
        }

        ExportQuery query = getExportQuery();

        addHistoryEntry(query);

        exportFuture = Background.call(
                new ExportBackgroundJob(query, getExporterByName(query.getExporterName()), ui, eventBus, panel));
        state.getExecutedTasks().put(QueryUIState.QueryType.EXPORT, exportFuture);
    }

    public void cancelExport() {
        Future exportFuture = state.getExecutedTasks().get(QueryUIState.QueryType.EXPORT);
        if (exportFuture != null && !exportFuture.isDone()) {
            if (!exportFuture.cancel(true)) {
                log.warn("Could not cancel export");
            }
        }
    }

    public void executeFrequency(FrequencyQueryPanel panel) {
        // kill old request
        Future freqFuture = state.getExecutedTasks().get(QueryUIState.QueryType.FREQUENCY);
        if (freqFuture != null && !freqFuture.isDone()) {
            freqFuture.cancel(true);
        }

        if ("".equals(state.getAql().getValue())) {
            Notification.show("Empty query", Notification.Type.WARNING_MESSAGE);
            panel.showQueryDefinitionPanel();
            return;
        } else if (state.getSelectedCorpora().getValue().isEmpty()) {
            Notification.show("Please select a corpus", Notification.Type.WARNING_MESSAGE);
            panel.showQueryDefinitionPanel();
            return;
        }

        BeanContainer<Integer, UserGeneratedFrequencyEntry> container = state.getFrequencyTableDefinition();

        FrequencyTableQuery freqDefinition = new FrequencyTableQuery();
        for (Integer id : container.getItemIds()) {
            UserGeneratedFrequencyEntry userGen = container.getItem(id).getBean();
            freqDefinition.add(userGen.toFrequencyTableEntry());
        }
        // additionally add meta data columns
        for (String m : state.getFrequencyMetaData().getValue()) {
            FrequencyTableEntry entry = new FrequencyTableEntry();
            entry.setType(FrequencyTableEntryType.meta);
            entry.setKey(m);
            freqDefinition.add(entry);
        }

        FrequencyQuery query = QueryGenerator.frequency().query(state.getAql().getValue())
                .corpora(state.getSelectedCorpora().getValue()).def(freqDefinition).build();

        addHistoryEntry(query);

        FrequencyBackgroundJob job = new FrequencyBackgroundJob(ui, query, panel);

        freqFuture = Background.call(job);
        state.getExecutedTasks().put(QueryUIState.QueryType.FREQUENCY, freqFuture);
    }

    public Exporter getExporterByName(String name) {
        return exporterMap.get(name);
    }

    private List<ResultViewPanel> getResultPanels() {
        ArrayList<ResultViewPanel> result = new ArrayList<>();
        for (int i = 0; i < searchView.getMainTab().getComponentCount(); i++) {
            Component c = searchView.getMainTab().getTab(i).getComponent();
            if (c instanceof ResultViewPanel) {
                result.add((ResultViewPanel) c);
            }
        }
        return result;
    }

    /**
     * Cancel queries from the client side.
     *
     * Important: This does not magically cancel the query on the server side, so
     * don't use this to implement a "real" query cancelation.
     */
    private void cancelSearch() {
        // don't spin forever when canceled
        searchView.getControlPanel().getQueryPanel().setCountIndicatorEnabled(false);

        Map<QueryUIState.QueryType, Future<?>> exec = state.getExecutedTasks();
        // abort last tasks if running
        if (exec.containsKey(QueryUIState.QueryType.COUNT) && !exec.get(QueryUIState.QueryType.COUNT).isDone()) {
            exec.get(QueryUIState.QueryType.COUNT).cancel(true);
        }
        if (exec.containsKey(QueryUIState.QueryType.FIND) && !exec.get(QueryUIState.QueryType.FIND).isDone()) {
            exec.get(QueryUIState.QueryType.FIND).cancel(true);
        }

        exec.remove(QueryUIState.QueryType.COUNT);
        exec.remove(QueryUIState.QueryType.FIND);

    }

    /**
     * Adds a history entry to the history panel.
     *
     * @param q the entry, which is added.
     *
     * @see HistoryPanel
     */
    public void addHistoryEntry(Query q) {
        try {
            Query queryCopy = q.clone();
            // remove it first in order to let it appear on the beginning of the list
            state.getHistory().removeItem(queryCopy);
            state.getHistory().addItemAt(0, queryCopy);
            searchView.getControlPanel().getQueryPanel().updateShortHistory();
        } catch (CloneNotSupportedException ex) {
            log.error("Can't clone the query", ex);
        }
    }

    public void changeContext(PagedResultQuery originalQuery, Match match, long offset, int newContext,
            final VisualizerContextChanger visCtxChange, boolean left) {

        try {
            final PagedResultQuery newQuery = (PagedResultQuery) originalQuery.clone();
            if (left) {
                newQuery.setLeftContext(newContext);
            } else {
                newQuery.setRightContext(newContext);
            }

            newQuery.setOffset(offset);

            Background.runWithCallback(new SingleResultFetchJob(match, newQuery),
                    new FutureCallback<SaltProject>() {

                        @Override
                        public void onSuccess(SaltProject result) {
                            visCtxChange.updateResult(result, newQuery);
                        }

                        @Override
                        public void onFailure(Throwable t) {
                            ExceptionDialog.show(t, "Could not extend context.");
                        }
                    });
        } catch (CloneNotSupportedException ex) {
            log.error("Can't clone the query", ex);
        }
    }

    public void corpusSelectionChangedInBackground() {
        searchView.getControlPanel().getSearchOptions()
                .updateSearchPanelConfigurationInBackground(getState().getSelectedCorpora().getValue(), ui);
    }

    public QueryUIState getState() {
        return ui.getQueryState();
    }

}