From 204c5571af75c752623dd1278661c551f7e22200 Mon Sep 17 00:00:00 2001 From: Aleksi Hietanen Date: Tue, 29 Aug 2017 11:15:33 +0300 Subject: [PATCH] Add declarative support to Tree (#9881) Closes #9838 --- server/src/main/java/com/vaadin/ui/Tree.java | 174 ++++++++++++++++++ .../AbstractComponentDeclarativeTestBase.java | 4 +- .../component/tree/TreeDeclarativeTest.java | 172 +++++++++++++++++ 3 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java diff --git a/server/src/main/java/com/vaadin/ui/Tree.java b/server/src/main/java/com/vaadin/ui/Tree.java index 77d7d2300a..f5d24c716c 100644 --- a/server/src/main/java/com/vaadin/ui/Tree.java +++ b/server/src/main/java/com/vaadin/ui/Tree.java @@ -16,12 +16,20 @@ package com.vaadin.ui; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.UUID; + +import org.jsoup.nodes.Attributes; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import com.vaadin.data.Binder; import com.vaadin.data.HasHierarchicalDataProvider; @@ -30,6 +38,7 @@ import com.vaadin.data.TreeData; import com.vaadin.data.provider.DataGenerator; import com.vaadin.data.provider.DataProvider; import com.vaadin.data.provider.HierarchicalDataProvider; +import com.vaadin.data.provider.HierarchicalQuery; import com.vaadin.data.provider.TreeDataProvider; import com.vaadin.event.CollapseEvent; import com.vaadin.event.CollapseEvent.CollapseListener; @@ -51,6 +60,10 @@ import com.vaadin.shared.ui.tree.TreeRendererState; import com.vaadin.ui.Grid.SelectionMode; import com.vaadin.ui.components.grid.DescriptionGenerator; import com.vaadin.ui.components.grid.MultiSelectionModelImpl; +import com.vaadin.ui.components.grid.NoSelectionModel; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.renderers.AbstractRenderer; import com.vaadin.util.ReflectTools; @@ -702,6 +715,20 @@ public class Tree extends Composite } } + private SelectionMode getSelectionMode() { + SelectionModel selectionModel = getSelectionModel(); + SelectionMode mode = null; + if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) { + mode = SelectionMode.SINGLE; + } else if (selectionModel.getClass() + .equals(TreeMultiSelectionModel.class)) { + mode = SelectionMode.MULTI; + } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { + mode = SelectionMode.NONE; + } + return mode; + } + @Override public void setCaption(String caption) { treeGrid.setCaption(caption); @@ -886,6 +913,153 @@ public class Tree extends Composite setupContextClickListener(); } + @Override + public void writeDesign(Element design, DesignContext designContext) { + super.writeDesign(design, designContext); + Attributes attrs = design.attributes(); + + SelectionMode mode = getSelectionMode(); + if (mode != null) { + DesignAttributeHandler.writeAttribute("selection-mode", attrs, mode, + SelectionMode.SINGLE, SelectionMode.class, designContext); + } + DesignAttributeHandler.writeAttribute("content-mode", attrs, + renderer.getState(false).mode, ContentMode.TEXT, + ContentMode.class, designContext); + + if (designContext.shouldWriteData(this)) { + writeItems(design, designContext); + } + } + + private void writeItems(Element design, DesignContext designContext) { + getDataProvider().fetch(new HierarchicalQuery<>(null, null)) + .forEach(item -> writeItem(design, designContext, item, null)); + } + + private void writeItem(Element design, DesignContext designContext, T item, + T parent) { + + Element itemElement = design.appendElement("node"); + itemElement.attr("item", serializeDeclarativeRepresentation(item)); + + if (parent != null) { + itemElement.attr("parent", + serializeDeclarativeRepresentation(parent)); + } + + if (getSelectionModel().isSelected(item)) { + itemElement.attr("selected", ""); + } + + Resource icon = getItemIconGenerator().apply(item); + DesignAttributeHandler.writeAttribute("icon", itemElement.attributes(), + icon, null, Resource.class, designContext); + + String text = getItemCaptionGenerator().apply(item); + itemElement.html( + Optional.ofNullable(text).map(Object::toString).orElse("")); + + getDataProvider().fetch(new HierarchicalQuery<>(null, item)).forEach( + childItem -> writeItem(design, designContext, childItem, item)); + } + + @Override + public void readDesign(Element design, DesignContext designContext) { + super.readDesign(design, designContext); + Attributes attrs = design.attributes(); + if (attrs.hasKey("selection-mode")) { + setSelectionMode(DesignAttributeHandler.readAttribute( + "selection-mode", attrs, SelectionMode.class)); + } + if (attrs.hasKey("content-mode")) { + setContentMode(DesignAttributeHandler.readAttribute("content-mode", + attrs, ContentMode.class)); + } + readItems(design.children()); + } + + private void readItems(Elements bodyItems) { + if (bodyItems.isEmpty()) { + return; + } + + DeclarativeValueProvider valueProvider = new DeclarativeValueProvider<>(); + setItemCaptionGenerator(item -> valueProvider.apply(item)); + + DeclarativeIconGenerator iconGenerator = new DeclarativeIconGenerator<>( + item -> null); + setItemIconGenerator(iconGenerator); + + getSelectionModel().deselectAll(); + List selectedItems = new ArrayList<>(); + TreeData data = new TreeData(); + + for (Element row : bodyItems) { + T item = deserializeDeclarativeRepresentation(row.attr("item")); + T parent = null; + if (row.hasAttr("parent")) { + parent = deserializeDeclarativeRepresentation( + row.attr("parent")); + } + data.addItem(parent, item); + if (row.hasAttr("selected")) { + selectedItems.add(item); + } + + valueProvider.addValue(item, row.html()); + iconGenerator.setIcon(item, DesignAttributeHandler + .readAttribute("icon", row.attributes(), Resource.class)); + } + + setDataProvider(new TreeDataProvider<>(data)); + selectedItems.forEach(getSelectionModel()::select); + } + + /** + * Deserializes a string to a data item. Used when reading from the + * declarative format of this Tree. + *

+ * Default implementation is able to handle only {@link String} as an item + * type. There will be a {@link ClassCastException} if {@code T } is not a + * {@link String}. + * + * @since + * + * @see #serializeDeclarativeRepresentation(Object) + * + * @param item + * string to deserialize + * @throws ClassCastException + * if type {@code T} is not a {@link String} + * @return deserialized item + */ + @SuppressWarnings("unchecked") + protected T deserializeDeclarativeRepresentation(String item) { + if (item == null) { + return (T) new String(UUID.randomUUID().toString()); + } + return (T) new String(item); + } + + /** + * Serializes an {@code item} to a string. Used when saving this Tree to its + * declarative format. + *

+ * Default implementation delegates a call to {@code item.toString()}. + * + * @since + * + * @see #deserializeDeclarativeRepresentation(String) + * + * @param item + * a data item + * @return string representation of the {@code item}. + */ + protected String serializeDeclarativeRepresentation(T item) { + return item.toString(); + } + private void setupContextClickListener() { if (hasListeners(ContextClickEvent.class)) { if (contextClickRegistration == null) { diff --git a/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java b/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java index ea7e9c77a3..84c2534f84 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java +++ b/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java @@ -212,7 +212,7 @@ public abstract class AbstractComponentDeclarativeTestBase", getComponentTag()); @@ -237,7 +237,7 @@ public abstract class AbstractComponentDeclarativeTestBase", getComponentTag()); diff --git a/server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java b/server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java new file mode 100644 index 0000000000..a8f7741f1b --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java @@ -0,0 +1,172 @@ +package com.vaadin.tests.server.component.tree; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.TreeData; +import com.vaadin.data.provider.HierarchicalQuery; +import com.vaadin.shared.ui.ContentMode; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.tests.server.component.abstractcomponent.AbstractComponentDeclarativeTestBase; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.IconGenerator; +import com.vaadin.ui.ItemCaptionGenerator; +import com.vaadin.ui.Tree; + +public class TreeDeclarativeTest + extends AbstractComponentDeclarativeTestBase { + + @Test + public void dataSerialization() { + Person person1 = createPerson("a"); + Person person2 = createPerson("a/a"); + Person person3 = createPerson("a/b"); + Person person4 = createPerson("a/b/c"); + Person person5 = createPerson("b"); + + TreeData data = new TreeData<>(); + data.addItems(null, person1, person5); + data.addItems(person1, person2, person3); + data.addItem(person3, person4); + + Tree tree = new Tree<>(); + tree.setTreeData(data); + tree.setItemCaptionGenerator(item -> item.getFirstName()); + + String designString = String.format("<%s>" + + "%s" + + "%s" + + "%s" + + "%s" + + "%s" + + "", getComponentTag(), + person1.toString(), person1.getFirstName(), + person2.toString(), person1.toString(), person2.getFirstName(), + person3.toString(), person1.toString(), person3.getFirstName(), + person4.toString(), person3.toString(), person4.getFirstName(), + person5.toString(), person5.getFirstName(), + getComponentTag()); + + testWrite(designString, tree, true); + Tree readTree = testRead(designString, tree); + Assert.assertEquals(2, readTree.getDataProvider() + .getChildCount(new HierarchicalQuery<>(null, null))); + Assert.assertEquals(2, readTree.getDataProvider().getChildCount( + new HierarchicalQuery<>(null, person1.toString()))); + Assert.assertEquals(1, readTree.getDataProvider().getChildCount( + new HierarchicalQuery<>(null, person3.toString()))); + } + + @Test + public void htmlContentMode() { + Person person = createPerson("A Person"); + Tree tree = new Tree<>(); + tree.setItems(person); + tree.setItemCaptionGenerator( + item -> String.format("%s", item.getFirstName())); + tree.setContentMode(ContentMode.HTML); + + String designString = String.format( + "<%s content-mode='html'>%s", + getComponentTag(), person.toString(), person.getFirstName(), + getComponentTag()); + + testWrite(designString, tree, true); + testRead(designString, tree); + } + + @Test + public void selectionMode() { + Tree tree = new Tree<>(); + tree.setSelectionMode(SelectionMode.MULTI); + + String designString = String.format("<%s selection-mode='multi'>", + getComponentTag(), getComponentTag()); + + testRead(designString, tree); + testWrite(designString, tree, false); + } + + @Test + @Override + public void heightFullDeserialization() + throws InstantiationException, IllegalAccessException { + // width is full by default + String design = String.format("<%s size-full/>", getComponentTag()); + + Tree tree = new Tree<>(); + + tree.setHeight("100%"); + testRead(design, tree); + testWrite(design, tree); + } + + @Test + @Override + public void sizeUndefinedDeserialization() + throws InstantiationException, IllegalAccessException { + String design = String.format("<%s size-auto/>", getComponentTag()); + + Tree tree = new Tree<>(); + + tree.setSizeUndefined(); + testRead(design, tree); + testWrite(design, tree); + } + + @Test + @Override + public void widthFullDeserialization() + throws InstantiationException, IllegalAccessException { + // width is full by default + String design = String.format("<%s/>", getComponentTag()); + + Tree tree = new Tree<>(); + + tree.setWidth("100%"); + testRead(design, tree); + testWrite(design, tree); + } + + @Test + @Override + public void widthUndefinedDeserialization() + throws InstantiationException, IllegalAccessException { + String design = String.format("<%s size-auto/>", getComponentTag()); + + Tree tree = new Tree<>(); + + tree.setWidthUndefined(); + testRead(design, tree); + testWrite(design, tree); + } + + @Override + protected String getComponentTag() { + return "vaadin-tree"; + } + + @Override + protected Class getComponentClass() { + return Tree.class; + } + + @Override + protected void assertEquals(String message, Object o1, Object o2) { + if (o1 instanceof ItemCaptionGenerator) { + Assert.assertTrue(o2 instanceof ItemCaptionGenerator); + return; + } + if (o1 instanceof IconGenerator) { + Assert.assertTrue(o2 instanceof IconGenerator); + return; + } + super.assertEquals(message, o1, o2); + } + + private Person createPerson(String name) { + Person person = new Person(); + person.setFirstName(name); + return person; + } +} -- 2.39.5