Java tutorial
/* * Copyright 2011 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.controlpanel; import annis.gui.AnnisUI; import annis.gui.components.HelpButton; import static annis.gui.controlpanel.SearchOptionsPanel.NULL_SEGMENTATION_VALUE; import annis.gui.objects.QueryUIState; import annis.libgui.Background; import annis.libgui.Helper; import annis.service.objects.CorpusConfig; import annis.service.objects.CorpusConfigMap; import annis.service.objects.OrderType; import annis.service.objects.SegmentationList; import com.google.common.collect.ImmutableList; import com.google.common.escape.Escaper; import com.google.common.net.UrlEscapers; import com.google.gwt.thirdparty.guava.common.collect.Lists; import com.sun.jersey.api.client.UniformInterfaceException; import com.sun.jersey.api.client.WebResource; import com.vaadin.data.Container; import com.vaadin.data.Property; import com.vaadin.data.util.BeanItemContainer; import com.vaadin.data.util.IndexedContainer; import com.vaadin.data.util.ItemSorter; import com.vaadin.ui.AbstractSelect; import com.vaadin.ui.ComboBox; import com.vaadin.ui.FormLayout; import com.vaadin.ui.Notification; import com.vaadin.ui.ProgressBar; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Thomas Krause <krauseto@hu-berlin.de> * @author Benjamin Weienfels <b.pixeldrama@gmail.com> */ public class SearchOptionsPanel extends FormLayout { public static final String NULL_SEGMENTATION_VALUE = "tokens (default)"; public static final String KEY_DEFAULT_CONTEXT_SEGMENTATION = "default-context-segmentation"; public static final String KEY_DEFAULT_BASE_TEXT_SEGMENTATION = "default-base-text-segmentation"; public static final String KEY_MAX_CONTEXT_LEFT = "max-context-left"; public static final String KEY_MAX_CONTEXT_RIGHT = "max-context-right"; public static final String KEY_CONTEXT_STEPS = "context-steps"; public static final String KEY_DEFAULT_CONTEXT = "default-context"; public static final String KEY_RESULT_PER_PAGE = "results-per-page"; public static final String DEFAULT_CONFIG = "default-config"; public static final int DEFAULT_CONTEXT = 5; public static final int DEFAULT_CONTEXT_STEPS = 5; public static final int DEFAULT_MAX_CONTEXT = 20; private static final Logger log = LoggerFactory.getLogger(SearchOptionsPanel.class); private final ComboBox cbLeftContext; private final ComboBox cbRightContext; private final ComboBox cbResultsPerPage; private final ComboBox cbSegmentation; private final HelpButton segmentationHelp; private final ComboBox cbOrder; // TODO: make this configurable private static final List<Integer> PREDEFINED_PAGE_SIZES = ImmutableList.of(1, 2, 5, 10, 20, 25); public static final List<Integer> PREDEFINED_CONTEXTS = ImmutableList.of(0, 1, 2, 5, 10, 20); private final ProgressBar pbLoadConfig; private final BeanItemContainer<OrderType> orderContainer = new BeanItemContainer<>(OrderType.class, Lists.newArrayList(OrderType.values())); private final IndexedContainer contextContainerLeft = new IndexedContainer(); private final IndexedContainer contextContainerRight = new IndexedContainer(); private final IndexedContainer segmentationContainer = new IndexedContainer(); private final IndexedContainer resultsPerPageContainer = new IndexedContainer(); private final AtomicInteger maxLeftContext = new AtomicInteger(Integer.MAX_VALUE); private final AtomicInteger maxRightContext = new AtomicInteger(Integer.MAX_VALUE); private boolean updateStateFromConfig = true; private QueryUIState state; public SearchOptionsPanel() { setWidth("100%"); setHeight("-1px"); pbLoadConfig = new ProgressBar(); pbLoadConfig.setIndeterminate(true); pbLoadConfig.setCaption("Loading search options..."); addComponent(pbLoadConfig); cbLeftContext = new ComboBox("Left Context", contextContainerLeft); cbRightContext = new ComboBox("Right Context", contextContainerRight); cbResultsPerPage = new ComboBox("Results Per Page", resultsPerPageContainer); cbLeftContext.setNullSelectionAllowed(false); cbRightContext.setNullSelectionAllowed(false); cbResultsPerPage.setNullSelectionAllowed(false); cbLeftContext.setNewItemsAllowed(true); cbRightContext.setNewItemsAllowed(true); cbResultsPerPage.setNewItemsAllowed(true); cbLeftContext.setTextInputAllowed(true); cbRightContext.setTextInputAllowed(true); cbResultsPerPage.setTextInputAllowed(true); cbLeftContext.setImmediate(true); cbRightContext.setImmediate(true); cbResultsPerPage.setImmediate(true); cbSegmentation = new ComboBox("Show context in", segmentationContainer); cbSegmentation.setTextInputAllowed(false); cbSegmentation.setNullSelectionAllowed(true); cbSegmentation.setNewItemsAllowed(true); cbSegmentation.setNullSelectionItemId(NULL_SEGMENTATION_VALUE); cbSegmentation.addItem(NULL_SEGMENTATION_VALUE); cbSegmentation.setDescription("If corpora with multiple " + "context definitions are selected, a list of available context units will be " + "displayed. By default context is calculated in tokens " + "(e.g. 5 minimal units to the left and right of a search result). " + "Some corpora might offer further context definitions, e.g. in " + "syllables, word forms belonging to different speakers, normalized or " + "diplomatic segmentations of a manuscript, etc."); segmentationHelp = new HelpButton(cbSegmentation); cbOrder = new ComboBox("Order", orderContainer); cbOrder.setNewItemsAllowed(false); cbOrder.setNullSelectionAllowed(false); cbOrder.setImmediate(true); cbLeftContext.setVisible(false); cbRightContext.setVisible(false); cbResultsPerPage.setVisible(false); cbOrder.setVisible(false); segmentationHelp.setVisible(false); addComponent(cbLeftContext); addComponent(cbRightContext); addComponent(segmentationHelp); addComponent(cbResultsPerPage); addComponent(cbOrder); } @Override public void attach() { super.attach(); contextContainerLeft.setItemSorter(new IntegerIDSorter()); contextContainerRight.setItemSorter(new IntegerIDSorter()); resultsPerPageContainer.setItemSorter(new IntegerIDSorter()); resultsPerPageContainer.removeAllItems(); for (Integer i : PREDEFINED_PAGE_SIZES) { resultsPerPageContainer.addItem(i); } if (getUI() instanceof AnnisUI) { AnnisUI ui = (AnnisUI) getUI(); state = ui.getQueryState(); Background.run(new CorpusConfigUpdater(ui, state.getSelectedCorpora().getValue(), false)); cbLeftContext.setNewItemHandler( new CustomContext(maxLeftContext, contextContainerLeft, state.getLeftContext())); cbRightContext.setNewItemHandler( new CustomContext(maxRightContext, contextContainerRight, state.getRightContext())); cbResultsPerPage.setNewItemHandler(new CustomResultSize(resultsPerPageContainer, state.getLimit())); cbLeftContext.setPropertyDataSource(state.getLeftContext()); cbRightContext.setPropertyDataSource(state.getRightContext()); cbResultsPerPage.setPropertyDataSource(state.getLimit()); cbSegmentation.setPropertyDataSource(state.getContextSegmentation()); orderContainer.removeAllItems(); for (OrderType t : OrderType.values()) { orderContainer.addItem(t); } cbOrder.setPropertyDataSource(state.getOrder()); } } public void updateSearchPanelConfigurationInBackground(final Set<String> corpora, final AnnisUI ui) { setLoadingState(true); // remove custom adjustments contextContainerLeft.removeAllItems(); contextContainerRight.removeAllItems(); cbSegmentation.removeAllItems(); // reload the config in the background Background.run(new CorpusConfigUpdater(ui, corpora, true)); } private static Integer getInteger(String key, CorpusConfig config) { String s = config.getConfig(key); if (s != null) { return Integer.parseInt(s); } return null; } private static List<String> getSegmentationNamesFromService(Set<String> corpora) { List<String> segNames = new ArrayList<>(); WebResource service = Helper.getAnnisWebResource(); if (service != null) { for (String corpus : corpora) { try { SegmentationList segList = service.path("query").path("corpora") .path(Helper.encodeJersey(corpus)).path("segmentation-names") .get(SegmentationList.class); segNames.addAll(segList.getSegmentatioNames()); } catch (UniformInterfaceException ex) { if (ex.getResponse().getStatus() == 403) { log.debug("Did not have access rights to query segmentation names for corpus", ex); } else { log.warn("Could not query segmentation names for corpus", ex); } } } } return segNames; } private void updateSegmentations(String segment, List<String> segNames, boolean updateValue) { cbSegmentation.setNullSelectionItemId(NULL_SEGMENTATION_VALUE); cbSegmentation.addItem(NULL_SEGMENTATION_VALUE); if ("tok".equalsIgnoreCase(segment)) { if (state != null && updateValue) { state.getContextSegmentation().setValue(null); } } else if (segment != null) { cbSegmentation.addItem(segment); if (state != null && updateValue) { cbSegmentation.setValue(segment); } } if (segNames != null && !segNames.isEmpty()) { for (String s : segNames) { if (!s.equalsIgnoreCase(segment)) { cbSegmentation.addItem(s); } } } } /** * If all values of a specific corpus property have the same value, this value * is returned, otherwise the value of the default configuration is choosen. * * @param key The property key. * @param corpora Specifies the selected corpora. * @return A value defined in the copurs.properties file or in the * admin-service.properties */ private String mergeConfigValue(String key, Set<String> corpora, CorpusConfigMap corpusConfigurations) { Set<String> values = new TreeSet<>(); for (String corpus : corpora) { CorpusConfig config = corpusConfigurations.get(corpus); if (config != null) { String v = config.getConfig(key); if (v != null) { values.add(v); } } } if (values.size() > 1 || values.isEmpty()) { // fallback to the default values CorpusConfig defaultConfig = corpusConfigurations.get(DEFAULT_CONFIG); if (defaultConfig != null && defaultConfig.containsKey(key)) { return defaultConfig.getConfig(key); } } // ok, just return the first value as a fallback of the fallback if (!values.isEmpty()) { return values.iterator().next(); } return null; } /** * Builds a single config for selection of one or muliple corpora. * * @param corpora Specifies the combination of corpora, for which the config * is calculated. * @param corpusConfigurations A map containg the known corpus configurations. * @return A new config which takes into account the segementation of all * selected corpora. */ private CorpusConfig mergeConfigs(Set<String> corpora, CorpusConfigMap corpusConfigurations) { CorpusConfig corpusConfig = new CorpusConfig(); // calculate the left and right context. String leftCtx = mergeConfigValue(KEY_MAX_CONTEXT_LEFT, corpora, corpusConfigurations); String rightCtx = mergeConfigValue(KEY_MAX_CONTEXT_RIGHT, corpora, corpusConfigurations); corpusConfig.setConfig(KEY_MAX_CONTEXT_LEFT, leftCtx); corpusConfig.setConfig(KEY_MAX_CONTEXT_RIGHT, rightCtx); // calculate the default-context corpusConfig.setConfig(KEY_CONTEXT_STEPS, mergeConfigValue(KEY_CONTEXT_STEPS, corpora, corpusConfigurations)); corpusConfig.setConfig(KEY_DEFAULT_CONTEXT, mergeConfigValue(KEY_DEFAULT_CONTEXT, corpora, corpusConfigurations)); // get the results per page corpusConfig.setConfig(KEY_RESULT_PER_PAGE, mergeConfigValue(KEY_RESULT_PER_PAGE, corpora, corpusConfigurations)); corpusConfig.setConfig(KEY_DEFAULT_CONTEXT_SEGMENTATION, checkSegments(KEY_DEFAULT_CONTEXT_SEGMENTATION, corpora, corpusConfigurations)); corpusConfig.setConfig(KEY_DEFAULT_BASE_TEXT_SEGMENTATION, checkSegments(KEY_DEFAULT_BASE_TEXT_SEGMENTATION, corpora, corpusConfigurations)); return corpusConfig; } /** * Checks, if all selected corpora have the same default segmentation layer. * If not the tok layer is taken, because every corpus has this one. * * @param key the key for the segementation config, must be * {@link #KEY_DEFAULT_BASE_TEXT_SEGMENTATION} or * {@link #KEY_DEFAULT_CONTEXT_SEGMENTATION}. * @param corpora the corpora which has to be checked. * @return "tok" or a segment which is defined in all corpora. */ private String checkSegments(String key, Set<String> corpora, CorpusConfigMap corpusConfigurations) { String segmentation = null; for (String corpus : corpora) { CorpusConfig c = null; if (corpusConfigurations.containsConfig(corpus)) { c = corpusConfigurations.get(corpus); } else { c = corpusConfigurations.get(DEFAULT_CONFIG); } // do nothing if not even default config is set if (c == null) { continue; } String tmpSegment = c.getConfig(key); /** * If no segment is set in the corpus config use always the tok segment. */ if (tmpSegment == null) { return corpusConfigurations.get(DEFAULT_CONFIG).getConfig(key); } if (segmentation == null) { segmentation = tmpSegment; continue; } if (!segmentation.equals(tmpSegment)) // return the default config { return corpusConfigurations.get(DEFAULT_CONFIG).getConfig(key); } } if (segmentation == null) { return corpusConfigurations.get(DEFAULT_CONFIG).getConfig(key); } else { return segmentation; } } /** * Updates context combo boxes. * * @param c the container, which is updated. * @param maxCtx the larges context values until context steps are calculated. * @param ctxSteps the step range. * @param keepCustomValues If this is true all custom values are kept. */ private void updateContext(Container c, int maxCtx, int ctxSteps, boolean keepCustomValues) { if (!keepCustomValues) { c.removeAllItems(); } for (Integer i : PREDEFINED_CONTEXTS) { if (i < maxCtx) { c.addItem(i); } } for (int step = ctxSteps; step < maxCtx; step += ctxSteps) { c.addItem(step); } c.addItem(maxCtx); } private void setLoadingState(boolean isLoading) { pbLoadConfig.setVisible(isLoading); cbLeftContext.setVisible(!isLoading); cbRightContext.setVisible(!isLoading); cbResultsPerPage.setVisible(!isLoading); cbOrder.setVisible(!isLoading); segmentationHelp.setVisible(!isLoading); } public boolean isUpdateStateFromConfig() { return updateStateFromConfig; } public void setUpdateStateFromConfig(boolean updateStateFromConfig) { this.updateStateFromConfig = updateStateFromConfig; } private static class CustomResultSize implements AbstractSelect.NewItemHandler { private final IndexedContainer container; private final Property<Integer> prop; public CustomResultSize(IndexedContainer container, Property<Integer> prop) { this.container = container; this.prop = prop; } @Override public void addNewItem(String resultPerPage) { try { int i = Integer.parseInt((String) resultPerPage); if (i < 1) { throw new IllegalArgumentException( "result number has to be a positive number greater or equal than 1"); } container.addItem(i); container.sort(null, null); prop.setValue(i); } catch (NumberFormatException ex) { Notification.show("invalid result per page input", "Please enter valid numbers [0-9]", Notification.Type.WARNING_MESSAGE); } catch (IllegalArgumentException ex) { Notification.show("invalid result per page input", ex.getMessage(), Notification.Type.WARNING_MESSAGE); } } } private class CorpusConfigUpdater implements Runnable { private final AnnisUI ui; private final Set<String> corpora; private final QueryUIState state; private final boolean corpusSelectionChanged; public CorpusConfigUpdater(AnnisUI ui, Set<String> corpora, boolean corpusSelectionChanged) { this.ui = ui; this.state = ui.getQueryState(); this.corpora = corpora; this.corpusSelectionChanged = corpusSelectionChanged; } @Override public void run() { final List<String> segmentations = getSegmentationNamesFromService(corpora); final Set<String> corporaWithDefault = new TreeSet<>(corpora); corporaWithDefault.add(DEFAULT_CONFIG); final CorpusConfigMap corpusConfigs = new CorpusConfigMap(); for (String c : corporaWithDefault) { corpusConfigs.put(c, ui.getCorpusConfigWithCache(c)); } // if there are not any defaults create them if (!corpusConfigs.containsConfig(DEFAULT_CONFIG)) { CorpusConfig defaultConfig = new CorpusConfig(); defaultConfig.setConfig(KEY_MAX_CONTEXT_LEFT, "" + DEFAULT_MAX_CONTEXT); defaultConfig.setConfig(KEY_MAX_CONTEXT_RIGHT, "" + DEFAULT_MAX_CONTEXT); defaultConfig.setConfig(KEY_CONTEXT_STEPS, "" + DEFAULT_CONTEXT_STEPS); defaultConfig.setConfig(KEY_RESULT_PER_PAGE, "10"); defaultConfig.setConfig(KEY_DEFAULT_CONTEXT, "" + DEFAULT_CONTEXT); defaultConfig.setConfig(KEY_DEFAULT_CONTEXT_SEGMENTATION, "tok"); defaultConfig.setConfig(KEY_DEFAULT_BASE_TEXT_SEGMENTATION, "tok"); corpusConfigs.put(DEFAULT_CONFIG, defaultConfig); } // update GUI ui.access(new Runnable() { @Override public void run() { setLoadingState(false); CorpusConfig c = mergeConfigs(corpora, corpusConfigs); Integer resultsPerPage = getInteger(KEY_RESULT_PER_PAGE, c); Integer leftCtx = getInteger(KEY_MAX_CONTEXT_LEFT, c); if (leftCtx != null) { maxLeftContext.set(leftCtx); } Integer rightCtx = getInteger(KEY_MAX_CONTEXT_RIGHT, c); if (rightCtx != null) { maxRightContext.set(rightCtx); } Integer defaultCtx = getInteger(KEY_DEFAULT_CONTEXT, c); Integer ctxSteps = getInteger(KEY_CONTEXT_STEPS, c); String segment = c.getConfig(KEY_DEFAULT_CONTEXT_SEGMENTATION); updateContext(contextContainerLeft, leftCtx == null ? DEFAULT_CONTEXT : leftCtx, ctxSteps == null ? DEFAULT_CONTEXT_STEPS : ctxSteps, true); updateContext(contextContainerRight, rightCtx == null ? DEFAULT_CONTEXT : rightCtx, ctxSteps == null ? DEFAULT_CONTEXT_STEPS : ctxSteps, true); if (defaultCtx != null && updateStateFromConfig && corpusSelectionChanged) { state.getLeftContext().setValue(defaultCtx); state.getRightContext().setValue(defaultCtx); } updateSegmentations(segment, segmentations, updateStateFromConfig && !corpora.isEmpty()); if (resultsPerPage != null && updateStateFromConfig && corpusSelectionChanged) { state.getLimit().setValue(resultsPerPage); } // reset if corpus selection has changed if (corpusSelectionChanged) { updateStateFromConfig = true; } } }); } } private static class CustomContext implements AbstractSelect.NewItemHandler { private final AtomicInteger maxCtx; private final IndexedContainer container; private final Property<Integer> prop; public CustomContext(AtomicInteger maxCtx, IndexedContainer container, Property<Integer> prop) { this.maxCtx = maxCtx; this.container = container; this.prop = prop; } @Override public void addNewItem(String context) { try { int i = Integer.parseInt((String) context); if (i < 0) { throw new IllegalArgumentException("context has to be a positive number or 0"); } if (i > maxCtx.get()) { throw new IllegalArgumentException( "The context is greater than, than the max value defined in the corpus property file."); } // everything ok, add the value container.addItem(i); container.sort(null, null); prop.setValue(i); } catch (NumberFormatException ex) { Notification.show("invalid context input", "Please enter valid numbers [0-9]", Notification.Type.WARNING_MESSAGE); } catch (IllegalArgumentException ex) { Notification.show("invalid context input", ex.getMessage(), Notification.Type.WARNING_MESSAGE); } } } public static class IntegerIDSorter implements ItemSorter { @Override public void setSortProperties(Container.Sortable container, Object[] propertyId, boolean[] ascending) { // does nothing } @Override public int compare(Object itemId1, Object itemId2) { if (itemId1 instanceof Integer && itemId2 instanceof Integer) { return Integer.compare((Integer) itemId1, (Integer) itemId2); } return 0; } } }