From: Johannes Dahlström Date: Thu, 8 Sep 2016 18:17:37 +0000 (+0300) Subject: Add AbstractSingleSelection base class X-Git-Tag: 8.0.0.alpha2~36 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ea89e24646cead0eef80dd42a7426fae4e0a6092;p=vaadin-framework.git Add AbstractSingleSelection base class Uses RPC for client-to-server but leaves server-to-client to implementation. SimpleSingleSelection uses shared state; lazy-loading implementations to pass selection info along with item data. Change-Id: I97c1dfa28eee39aef43eabbfbac56cd83fa5747c --- diff --git a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java index 2169222c53..823650f443 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java +++ b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java @@ -41,32 +41,32 @@ import com.vaadin.util.ReflectTools; * @since */ public abstract class AbstractSingleSelect extends - AbstractListing.SimpleSingleSelection> { + AbstractListing.AbstractSingleSelection> { /** - * A simple single selection model using the {@code AbstractSingleSelect} - * RPC and state to communicate with the client. Has no client-side - * counterpart; the listing connector is expected to handle selection. - * Client-to-server selection is passed via {@link SelectionServerRpc} and - * server-to-client via {@link AbstractSingleSelectState#selectedItemKey}. + * A base class for single selection model implementations. Listens to + * {@code SelectionServerRpc} invocations to track selection requests by the + * client. Maintaining the selection state and communicating it from the + * server to the client is the responsibility of the implementing class. */ - public class SimpleSingleSelection implements SelectionModel.Single { + public abstract class AbstractSingleSelection implements + SelectionModel.Single { /** * Creates a new {@code SimpleSingleSelection} instance. */ - public SimpleSingleSelection() { + public AbstractSingleSelection() { registerRpc(new SelectionServerRpc() { @Override public void select(String key) { - setSelectedKey(key); + setSelectedFromClient(key); } @Override public void deselect(String key) { - if (Objects.equals(key, getState(false).selectedItemKey)) { - setSelectedKey(null); + if (isKeySelected(key)) { + setSelectedFromClient(null); } } }); @@ -75,51 +75,151 @@ public abstract class AbstractSingleSelect extends @Override public void deselect(T item) { Objects.requireNonNull(item, "deselected item cannot be null"); - // TODO creates a key if item not in data source - String key = getDataCommunicator().getKeyMapper().key(item); - if (Objects.equals(key, getState(false).selectedItemKey)) { - setSelectedItem(null); + if (isSelected(item)) { + setSelectedFromServer(null); } } @Override public void select(T item) { Objects.requireNonNull(item, "selected item cannot be null"); - setSelectedItem(item); + setSelectedFromServer(item); } - @Override - public Optional getSelectedItem() { - return Optional.ofNullable(getState(false).selectedItemKey).map( - getDataCommunicator().getKeyMapper()::get); - } + /** + * Returns the communication key of the selected item or {@code null} if + * no item is selected. + * + * @return the key of the selected item if any, {@code null} otherwise. + */ + protected abstract String getSelectedKey(); + + /** + * Sets the selected item based on the given communication key. If the + * key is {@code null}, clears the current selection if any. + * + * @param key + * the key of the selected item or {@code null} to clear + * selection + */ + protected abstract void doSetSelectedKey(String key); - private void setSelectedKey(String key) { + /** + * Sets the selection based on a client request. Does nothing if the + * select component is {@linkplain Component#isReadOnly()} or if the + * selection would not change. Otherwise updates the selection and fires + * a selection change event with {@code isUserOriginated == true}. + * + * @param key + * the key of the item to select or {@code null} to clear + * selection + */ + protected void setSelectedFromClient(String key) { if (isReadOnly()) { return; } - if (Objects.equals(key, getState(false).selectedItemKey)) { + if (isKeySelected(key)) { return; } - getState().selectedItemKey = key; + doSetSelectedKey(key); fireEvent(new SingleSelectionChange<>(AbstractSingleSelect.this, getSelectedItem().orElse(null), true)); } - private void setSelectedItem(T item) { + /** + * Sets the selection based on server API call. Does nothing if the + * selection would not change; otherwise updates the selection and fires + * a selection change event with {@code isUserOriginated == false}. + * + * @param item + * the item to select or {@code null} to clear selection + */ + protected void setSelectedFromServer(T item) { // TODO creates a key if item not in data source - String key = Optional.ofNullable(item).map(getDataCommunicator() - .getKeyMapper()::key).orElse(null); + String key = itemToKey(item); - if (Objects.equals(key, getState(false).selectedItemKey)) { + if (isKeySelected(key) || isSelected(item)) { return; } - getState().selectedItemKey = key; + doSetSelectedKey(key); fireEvent(new SingleSelectionChange<>(AbstractSingleSelect.this, item, false)); } + + /** + * Returns whether the given key maps to the currently selected item. + * + * @param key + * the key to test or {@code null} to test whether nothing is + * selected + * @return {@code true} if the key equals the key of the currently + * selected item (or {@code null} if no selection), + * {@code false} otherwise. + */ + protected boolean isKeySelected(String key) { + return Objects.equals(key, getSelectedKey()); + } + + /** + * Returns the communication key assigned to the given item. + * + * @param item + * the item whose key to return + * @return the assigned key + */ + protected String itemToKey(T item) { + if (item == null) { + return null; + } else { + // TODO creates a key if item not in data source + return getDataCommunicator().getKeyMapper().key(item); + } + } + + /** + * Returns the item that the given key is assigned to, or {@code null} + * if there is no such item. + * + * @param key + * the key whose item to return + * @return the associated item if any, {@code null} otherwise. + */ + protected T keyToItem(String key) { + return getDataCommunicator().getKeyMapper().get(key); + } + } + + /** + * A simple single selection model using the {@code AbstractSingleSelect} + * RPC and state to communicate with the client. Has no client-side + * counterpart; the listing connector is expected to handle selection. + * Client-to-server selection is passed via {@link SelectionServerRpc} and + * server-to-client via {@link AbstractSingleSelectState#selectedItemKey}. + */ + protected class SimpleSingleSelection extends AbstractSingleSelection { + + /** + * Creates a new {@code SimpleSingleSelection}. + */ + public SimpleSingleSelection() { + } + + @Override + public Optional getSelectedItem() { + return Optional.ofNullable(keyToItem(getSelectedKey())); + } + + @Override + protected String getSelectedKey() { + return getState(false).selectedItemKey; + } + + @Override + protected void doSetSelectedKey(String key) { + getState().selectedItemKey = key; + } } @Deprecated diff --git a/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java b/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java new file mode 100644 index 0000000000..82c80542c4 --- /dev/null +++ b/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java @@ -0,0 +1,175 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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 com.vaadin.ui; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.server.data.datasource.bov.Person; +import com.vaadin.shared.data.DataCommunicatorClientRpc; +import com.vaadin.ui.AbstractSingleSelect.AbstractSingleSelection; + +/** + * Test for {@link AbstractSingleSelect} and {@link AbstractSingleSelection} + * + * @author Vaadin Ltd + */ +public class AbstractSingleSelectTest { + + private PersonListing.AbstractSingleSelection selectionModel; + private List selectionChanges; + + private static class PersonListing extends + AbstractSingleSelect { + public PersonListing() { + setSelectionModel(new SimpleSingleSelection()); + } + } + + @Before + public void initListing() { + listing = new PersonListing(); + listing.setItems(PERSON_A, PERSON_B, PERSON_C); + selectionModel = listing.getSelectionModel(); + selectionChanges = new ArrayList<>(); + listing.addSelectionListener(e -> selectionChanges.add(e.getValue())); + } + + public static final Person PERSON_C = new Person("c", 3); + public static final Person PERSON_B = new Person("b", 2); + public static final Person PERSON_A = new Person("a", 1); + public static final String RPC_INTERFACE = DataCommunicatorClientRpc.class + .getName(); + private PersonListing listing; + + @Test + public void select() { + + selectionModel.select(PERSON_B); + + assertTrue(selectionModel.getSelectedItem().isPresent()); + + assertEquals(PERSON_B, selectionModel.getSelectedItem().orElse(null)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertEquals(Collections.singleton(PERSON_B), selectionModel + .getSelectedItems()); + + assertEquals(Arrays.asList(PERSON_B), selectionChanges); + } + + @Test + public void selectDeselect() { + + selectionModel.select(PERSON_B); + selectionModel.deselect(PERSON_B); + + assertFalse(selectionModel.getSelectedItem().isPresent()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertTrue(selectionModel.getSelectedItems().isEmpty()); + + assertEquals(Arrays.asList(PERSON_B, null), selectionChanges); + } + + @Test + public void reselect() { + + selectionModel.select(PERSON_B); + selectionModel.select(PERSON_C); + + assertEquals(PERSON_C, selectionModel.getSelectedItem().orElse(null)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Collections.singleton(PERSON_C), selectionModel + .getSelectedItems()); + + assertEquals(Arrays.asList(PERSON_B, PERSON_C), selectionChanges); + } + + @Test + public void deselectNoOp() { + + selectionModel.select(PERSON_C); + selectionModel.deselect(PERSON_B); + + assertEquals(PERSON_C, selectionModel.getSelectedItem().orElse(null)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Collections.singleton(PERSON_C), selectionModel + .getSelectedItems()); + + assertEquals(Arrays.asList(PERSON_C), selectionChanges); + } + + @Test + public void selectTwice() { + + selectionModel.select(PERSON_C); + selectionModel.select(PERSON_C); + + assertEquals(PERSON_C, selectionModel.getSelectedItem().orElse(null)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Collections.singleton(PERSON_C), selectionModel + .getSelectedItems()); + + assertEquals(Arrays.asList(PERSON_C), selectionChanges); + } + + @Test + public void deselectTwice() { + + selectionModel.select(PERSON_C); + selectionModel.deselect(PERSON_C); + selectionModel.deselect(PERSON_C); + + assertFalse(selectionModel.getSelectedItem().isPresent()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertTrue(selectionModel.getSelectedItems().isEmpty()); + + assertEquals(Arrays.asList(PERSON_C, null), selectionChanges); + } + +}