]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add AbstractSingleSelection base class
authorJohannes Dahlström <johannesd@vaadin.com>
Thu, 8 Sep 2016 18:17:37 +0000 (21:17 +0300)
committerVaadin Code Review <review@vaadin.com>
Mon, 12 Sep 2016 07:56:38 +0000 (07:56 +0000)
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

server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java [new file with mode: 0644]

index 2169222c53a31a422e4c56be676d07808f2aea98..823650f443d48f6e0b5226b864f127285adc79f5 100644 (file)
@@ -41,32 +41,32 @@ import com.vaadin.util.ReflectTools;
  * @since
  */
 public abstract class AbstractSingleSelect<T> extends
-        AbstractListing<T, AbstractSingleSelect<T>.SimpleSingleSelection> {
+        AbstractListing<T, AbstractSingleSelect<T>.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<T> {
+    public abstract class AbstractSingleSelection implements
+            SelectionModel.Single<T> {
 
         /**
          * 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<T> 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<T> 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<T> 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 (file)
index 0000000..82c8054
--- /dev/null
@@ -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<Person> selectionChanges;
+
+    private static class PersonListing extends
+            AbstractSingleSelect<Person> {
+        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);
+    }
+
+}