aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/main/java')
-rw-r--r--server/src/main/java/com/vaadin/data/HasItems.java4
-rw-r--r--server/src/main/java/com/vaadin/data/HierarchyData.java263
-rw-r--r--server/src/main/java/com/vaadin/data/provider/AbstractHierarchicalDataProvider.java34
-rw-r--r--server/src/main/java/com/vaadin/data/provider/DataCommunicator.java135
-rw-r--r--server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java390
-rw-r--r--server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java82
-rw-r--r--server/src/main/java/com/vaadin/data/provider/HierarchicalQuery.java93
-rw-r--r--server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java445
-rw-r--r--server/src/main/java/com/vaadin/data/provider/InMemoryHierarchicalDataProvider.java235
-rw-r--r--server/src/main/java/com/vaadin/data/provider/ListDataProvider.java3
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java72
-rw-r--r--server/src/main/java/com/vaadin/ui/TreeGrid.java265
12 files changed, 1923 insertions, 98 deletions
diff --git a/server/src/main/java/com/vaadin/data/HasItems.java b/server/src/main/java/com/vaadin/data/HasItems.java
index 18dd9fc99d..c5b5382c9e 100644
--- a/server/src/main/java/com/vaadin/data/HasItems.java
+++ b/server/src/main/java/com/vaadin/data/HasItems.java
@@ -87,7 +87,7 @@ public interface HasItems<T> extends Component, Serializable {
* <pre>
* <code>
* HasDataProvider<String> listing = new CheckBoxGroup<>();
- * listing.setItems(Arrays.asList("a","b"));
+ * listing.setItems("a","b");
* ...
*
* Collection<String> collection = ((ListDataProvider<String>)listing.getDataProvider()).getItems();
@@ -122,7 +122,7 @@ public interface HasItems<T> extends Component, Serializable {
* <pre>
* <code>
* HasDataProvider<String> listing = new CheckBoxGroup<>();
- * listing.setItems(Arrays.asList("a","b"));
+ * listing.setItems(Stream.of("a","b"));
* ...
*
* Collection<String> collection = ((ListDataProvider<String>)listing.getDataProvider()).getItems();
diff --git a/server/src/main/java/com/vaadin/data/HierarchyData.java b/server/src/main/java/com/vaadin/data/HierarchyData.java
new file mode 100644
index 0000000000..df5fbbbbf5
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/HierarchyData.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2000-2016 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.data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * Class for representing hierarchical data.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ *
+ * @param <T>
+ * data type
+ */
+public class HierarchyData<T> implements Serializable {
+
+ private static class HierarchyWrapper<T> implements Serializable {
+ private T item;
+ private T parent;
+ private List<T> children;
+
+ public HierarchyWrapper(T item, T parent) {
+ this.item = item;
+ this.parent = parent;
+ children = new ArrayList<>();
+ }
+
+ public T getItem() {
+ return item;
+ }
+
+ public void setItem(T item) {
+ this.item = item;
+ }
+
+ public T getParent() {
+ return parent;
+ }
+
+ public void setParent(T parent) {
+ this.parent = parent;
+ }
+
+ public List<T> getChildren() {
+ return children;
+ }
+
+ public void setChildren(List<T> children) {
+ this.children = children;
+ }
+
+ public void addChild(T child) {
+ children.add(child);
+ }
+
+ public void removeChild(T child) {
+ children.remove(child);
+ }
+ }
+
+ private final Map<T, HierarchyWrapper<T>> itemToWrapperMap;
+
+ /**
+ * Creates an initially empty hierarchical data representation to which
+ * items can be added or removed.
+ */
+ public HierarchyData() {
+ itemToWrapperMap = new LinkedHashMap<>();
+ itemToWrapperMap.put(null, new HierarchyWrapper<>(null, null));
+ }
+
+ /**
+ * Adds a data item as a child of {@code parent}. Call with {@code null} as
+ * parent to add a root level item. The given parent item must already exist
+ * in this structure, and an item can only be added to this structure once.
+ *
+ * @param parent
+ * the parent item for which the items are added as children
+ * @param item
+ * the item to add
+ * @return this
+ *
+ * @throws IllegalArgumentException
+ * if parent is not null and not already added to this structure
+ * @throws IllegalArgumentException
+ * if the item has already been added to this structure
+ * @throws NullPointerException
+ * if item is null
+ */
+ public HierarchyData<T> addItem(T parent, T item) {
+ Objects.requireNonNull(item, "Item cannot be null");
+ if (parent != null && !contains(parent)) {
+ throw new IllegalArgumentException(
+ "Parent needs to be added before children. "
+ + "To add root items, call with parent as null");
+ }
+ if (contains(item)) {
+ throw new IllegalArgumentException(
+ "Cannot add the same item multiple times: " + item);
+ }
+ putItem(item, parent);
+ return this;
+ }
+
+ /**
+ * Adds a list of data items as children of {@code parent}. Call with
+ * {@code null} as parent to add root level items. The given parent item
+ * must already exist in this structure, and an item can only be added to
+ * this structure once.
+ *
+ * @param parent
+ * the parent item for which the items are added as children
+ * @param items
+ * the list of items to add
+ * @return this
+ *
+ * @throws IllegalArgumentException
+ * if parent is not null and not already added to this structure
+ * @throws IllegalArgumentException
+ * if any of the given items have already been added to this
+ * structure
+ * @throws NullPointerException
+ * if any of the items are null
+ */
+ public HierarchyData<T> addItems(T parent,
+ @SuppressWarnings("unchecked") T... items) {
+ Arrays.asList(items).stream().forEach(item -> addItem(parent, item));
+ return this;
+ }
+
+ /**
+ * Adds a list of data items as children of {@code parent}. Call with
+ * {@code null} as parent to add root level items. The given parent item
+ * must already exist in this structure, and an item can only be added to
+ * this structure once.
+ *
+ * @param parent
+ * the parent item for which the items are added as children
+ * @param items
+ * the collection of items to add
+ * @return this
+ *
+ * @throws IllegalArgumentException
+ * if parent is not null and not already added to this structure
+ * @throws IllegalArgumentException
+ * if any of the given items have already been added to this
+ * structure
+ * @throws NullPointerException
+ * if any of the items are null
+ */
+ public HierarchyData<T> addItems(T parent, Collection<T> items) {
+ items.stream().forEach(item -> addItem(parent, item));
+ return this;
+ }
+
+ /**
+ * Adds data items contained in a stream as children of {@code parent}. Call
+ * with {@code null} as parent to add root level items. The given parent
+ * item must already exist in this structure, and an item can only be added
+ * to this structure once.
+ *
+ * @param parent
+ * the parent item for which the items are added as children
+ * @param items
+ * stream of items to add
+ * @return this
+ *
+ * @throws IllegalArgumentException
+ * if parent is not null and not already added to this structure
+ * @throws IllegalArgumentException
+ * if any of the given items have already been added to this
+ * structure
+ * @throws NullPointerException
+ * if any of the items are null
+ */
+ public HierarchyData<T> addItems(T parent, Stream<T> items) {
+ items.forEach(item -> addItem(parent, item));
+ return this;
+ }
+
+ /**
+ * Remove a given item from this structure. Additionally, this will
+ * recursively remove any descendants of the item.
+ *
+ * @param item
+ * the item to remove, or null to clear all data
+ * @return this
+ *
+ * @throws IllegalArgumentException
+ * if the item does not exist in this structure
+ */
+ public HierarchyData<T> removeItem(T item) {
+ if (!contains(item)) {
+ throw new IllegalArgumentException(
+ "Item '" + item + "' not in the hierarchy");
+ }
+ new ArrayList<>(getChildren(item)).forEach(child -> removeItem(child));
+ itemToWrapperMap.get(itemToWrapperMap.get(item).getParent())
+ .removeChild(item);
+ return this;
+ }
+
+ /**
+ * Clear all items from this structure. Shorthand for calling
+ * {@link #removeItem(Object)} with null.
+ */
+ public void clear() {
+ removeItem(null);
+ }
+
+ /**
+ * Get the immediate child items for the given item.
+ *
+ * @param item
+ * the item for which to retrieve child items for, null to
+ * retrieve all root items
+ * @return a list of child items for the given item
+ *
+ * @throws IllegalArgumentException
+ * if the item does not exist in this structure
+ */
+ public List<T> getChildren(T item) {
+ if (!contains(item)) {
+ throw new IllegalArgumentException(
+ "Item '" + item + "' not in the hierarchy");
+ }
+ return itemToWrapperMap.get(item).getChildren();
+ }
+
+ private boolean contains(T item) {
+ return itemToWrapperMap.containsKey(item);
+ }
+
+ private void putItem(T item, T parent) {
+ HierarchyWrapper<T> wrappedItem = new HierarchyWrapper<>(item, parent);
+ if (itemToWrapperMap.containsKey(parent)) {
+ itemToWrapperMap.get(parent).addChild(item);
+ }
+ itemToWrapperMap.put(item, wrappedItem);
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/AbstractHierarchicalDataProvider.java b/server/src/main/java/com/vaadin/data/provider/AbstractHierarchicalDataProvider.java
new file mode 100644
index 0000000000..4b9825dcf7
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/AbstractHierarchicalDataProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2016 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.data.provider;
+
+/**
+ * Abstract hierarchical data provider implementation which takes care of item
+ * refreshes and associated events.
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ *
+ * @param <T>
+ * data type
+ * @param <F>
+ * filter type
+ */
+public abstract class AbstractHierarchicalDataProvider<T, F>
+ extends AbstractDataProvider<T, F>
+ implements HierarchicalDataProvider<T, F> {
+
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java b/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java
index 9080ad3442..ffa461e19f 100644
--- a/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java
+++ b/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java
@@ -66,15 +66,13 @@ public class DataCommunicator<T> extends AbstractExtension {
@Override
public void requestRows(int firstRowIndex, int numberOfRows,
int firstCachedRowIndex, int cacheSize) {
- pushRows = Range.withLength(firstRowIndex, numberOfRows);
- markAsDirty();
+ onRequestRows(firstRowIndex, numberOfRows, firstCachedRowIndex,
+ cacheSize);
}
@Override
public void dropRows(JsonArray keys) {
- for (int i = 0; i < keys.length(); ++i) {
- handler.dropActiveData(keys.getString(i));
- }
+ onDropRows(keys);
}
}
@@ -190,11 +188,11 @@ public class DataCommunicator<T> extends AbstractExtension {
private final ActiveDataHandler handler = new ActiveDataHandler();
/** Empty default data provider */
- private DataProvider<T, ?> dataProvider = new CallbackDataProvider<>(
+ protected DataProvider<T, ?> dataProvider = new CallbackDataProvider<>(
q -> Stream.empty(), q -> 0);
private final DataKeyMapper<T> keyMapper;
- private boolean reset = false;
+ protected boolean reset = false;
private final Set<T> updatedData = new HashSet<>();
private int minPushSize = 40;
private Range pushRows = Range.withLength(0, minPushSize);
@@ -224,6 +222,72 @@ public class DataCommunicator<T> extends AbstractExtension {
}
/**
+ * Set the range of rows to push for next response.
+ *
+ * @param pushRows
+ */
+ protected void setPushRows(Range pushRows) {
+ this.pushRows = pushRows;
+ }
+
+ /**
+ * Get the current range of rows to push in the next response.
+ *
+ * @return the range of rows to push
+ */
+ protected Range getPushRows() {
+ return pushRows;
+ }
+
+ /**
+ * Get the object used for filtering in this data communicator.
+ *
+ * @return the filter object of this data communicator
+ */
+ protected Object getFilter() {
+ return filter;
+ }
+
+ /**
+ * Get the client rpc interface for this data communicator.
+ *
+ * @return the client rpc interface for this data communicator
+ */
+ protected DataCommunicatorClientRpc getClientRpc() {
+ return rpc;
+ }
+
+ /**
+ * Request the given rows to be available on the client side.
+ *
+ * @param firstRowIndex
+ * the index of the first requested row
+ * @param numberOfRows
+ * the number of requested rows
+ * @param firstCachedRowIndex
+ * the index of the first cached row
+ * @param cacheSize
+ * the number of cached rows
+ */
+ protected void onRequestRows(int firstRowIndex, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+ setPushRows(Range.withLength(firstRowIndex, numberOfRows));
+ markAsDirty();
+ }
+
+ /**
+ * Triggered when rows have been dropped from the client side cache.
+ *
+ * @param keys
+ * the keys of the rows that have been dropped
+ */
+ protected void onDropRows(JsonArray keys) {
+ for (int i = 0; i < keys.length(); ++i) {
+ handler.dropActiveData(keys.getString(i));
+ }
+ }
+
+ /**
* Initially and in the case of a reset all data should be pushed to the
* client.
*/
@@ -231,6 +295,16 @@ public class DataCommunicator<T> extends AbstractExtension {
public void beforeClientResponse(boolean initial) {
super.beforeClientResponse(initial);
+ sendDataToClient(initial);
+ }
+
+ /**
+ * Send the needed data and updates to the client side.
+ *
+ * @param initial
+ * {@code true} if initial data load, {@code false} if not
+ */
+ protected void sendDataToClient(boolean initial) {
if (getDataProvider() == null) {
return;
}
@@ -241,9 +315,10 @@ public class DataCommunicator<T> extends AbstractExtension {
rpc.reset(dataProviderSize);
}
- if (!pushRows.isEmpty()) {
- int offset = pushRows.getStart();
- int limit = pushRows.length();
+ Range requestedRows = getPushRows();
+ if (!requestedRows.isEmpty()) {
+ int offset = requestedRows.getStart();
+ int limit = requestedRows.length();
@SuppressWarnings({ "rawtypes", "unchecked" })
Stream<T> rowsToPush = getDataProvider().fetch(new Query(offset,
@@ -261,7 +336,7 @@ public class DataCommunicator<T> extends AbstractExtension {
rpc.updateData(dataArray);
}
- pushRows = Range.withLength(0, 0);
+ setPushRows(Range.withLength(0, 0));
reset = false;
updatedData.clear();
}
@@ -343,6 +418,15 @@ public class DataCommunicator<T> extends AbstractExtension {
}
/**
+ * Returns the active data handler.
+ *
+ * @return the active data handler
+ */
+ protected ActiveDataHandler getActiveDataHandler() {
+ return handler;
+ }
+
+ /**
* Drops data objects identified by given keys from memory. This will invoke
* {@link DataGenerator#destroyData} for each of those objects.
*
@@ -401,6 +485,15 @@ public class DataCommunicator<T> extends AbstractExtension {
}
/**
+ * Returns the currently set updated data.
+ *
+ * @return the set of data that should be updated on the next response
+ */
+ protected Set<T> getUpdatedData() {
+ return updatedData;
+ }
+
+ /**
* Sets the {@link Comparator} to use with in-memory sorting.
*
* @param comparator
@@ -412,6 +505,15 @@ public class DataCommunicator<T> extends AbstractExtension {
}
/**
+ * Returns the {@link Comparator} to use with in-memory sorting.
+ *
+ * @return comparator used to sort data
+ */
+ public Comparator<T> getInMemorySorting() {
+ return inMemorySorting;
+ }
+
+ /**
* Sets the {@link QuerySortOrder}s to use with backend sorting.
*
* @param sortOrder
@@ -424,6 +526,15 @@ public class DataCommunicator<T> extends AbstractExtension {
}
/**
+ * Returns the {@link QuerySortOrder} to use with backend sorting.
+ *
+ * @return list of sort order information to pass to a query
+ */
+ public List<QuerySortOrder> getBackEndSorting() {
+ return backEndSorting;
+ }
+
+ /**
* Creates a {@link DataKeyMapper} to use with this DataCommunicator.
* <p>
* This method is called from the constructor.
@@ -492,7 +603,7 @@ public class DataCommunicator<T> extends AbstractExtension {
* (and theoretically allows to the client doesn't request more data in
* a happy path).
*/
- pushRows = Range.between(0, getMinPushSize());
+ setPushRows(Range.between(0, getMinPushSize()));
if (isAttached()) {
attachDataProviderListener();
}
diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java
new file mode 100644
index 0000000000..75e131df57
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataCommunicator.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2000-2016 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.data.provider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.vaadin.data.HierarchyData;
+import com.vaadin.data.provider.HierarchyMapper.TreeLevelQuery;
+import com.vaadin.server.SerializableConsumer;
+import com.vaadin.shared.Range;
+import com.vaadin.shared.extension.datacommunicator.HierarchicalDataCommunicatorState;
+import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+
+/**
+ * Data communicator that handles requesting hierarchical data from
+ * {@link HierarchicalDataProvider} and sending it to client side.
+ *
+ * @param <T>
+ * the bean type
+ * @author Vaadin Ltd
+ * @since
+ */
+public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> {
+
+ private static final Logger LOGGER = Logger
+ .getLogger(HierarchicalDataCommunicator.class.getName());
+
+ /**
+ * The amount of root level nodes to fetch and push to the client.
+ */
+ private static final int INITIAL_FETCH_SIZE = 100;
+
+ private HierarchyMapper mapper = new HierarchyMapper();
+
+ /**
+ * The captured client side cache size.
+ */
+ private int latestCacheSize = INITIAL_FETCH_SIZE;
+
+ /**
+ * Construct a new hierarchical data communicator backed by a
+ * {@link InMemoryHierarchicalDataProvider}.
+ */
+ public HierarchicalDataCommunicator() {
+ super();
+ dataProvider = new InMemoryHierarchicalDataProvider<>(
+ new HierarchyData<>());
+ }
+
+ @Override
+ protected HierarchicalDataCommunicatorState getState() {
+ return (HierarchicalDataCommunicatorState) super.getState();
+ }
+
+ @Override
+ protected HierarchicalDataCommunicatorState getState(boolean markAsDirty) {
+ return (HierarchicalDataCommunicatorState) super.getState(markAsDirty);
+ }
+
+ @Override
+ protected void sendDataToClient(boolean initial) {
+ // on purpose do not call super
+ if (getDataProvider() == null) {
+ return;
+ }
+
+ if (initial || reset) {
+ loadInitialData();
+ } else {
+ loadRequestedRows();
+ }
+
+ if (!getUpdatedData().isEmpty()) {
+ JsonArray dataArray = Json.createArray();
+ int i = 0;
+ for (T data : getUpdatedData()) {
+ dataArray.set(i++, createDataObject(data, -1));
+ }
+ getClientRpc().updateData(dataArray);
+ getUpdatedData().clear();
+ }
+ }
+
+ private void loadInitialData() {
+ int rootSize = doSizeQuery(null);
+ mapper.reset(rootSize);
+
+ if (rootSize != 0) {
+ Range initialRange = getInitialRowsToPush(rootSize);
+ assert !initialRange
+ .isEmpty() : "Initial range should never be empty.";
+ Stream<T> rootItems = doFetchQuery(initialRange.getStart(),
+ initialRange.length(), null);
+
+ // for now just fetching data for the root level as everything is
+ // collapsed by default
+ List<T> items = rootItems.collect(Collectors.toList());
+ List<JsonObject> dataObjects = items.stream()
+ .map(item -> createDataObject(item, 0))
+ .collect(Collectors.toList());
+
+ getClientRpc().reset(rootSize);
+ sendData(0, dataObjects);
+ getActiveDataHandler().addActiveData(items.stream());
+ getActiveDataHandler().cleanUp(items.stream());
+ }
+
+ setPushRows(Range.withLength(0, 0));
+ // any updated data is ignored at this point
+ getUpdatedData().clear();
+ reset = false;
+ }
+
+ private void loadRequestedRows() {
+ final Range requestedRows = getPushRows();
+ if (!requestedRows.isEmpty()) {
+ Stream<TreeLevelQuery> levelQueries = mapper
+ .splitRangeToLevelQueries(requestedRows.getStart(),
+ requestedRows.getEnd() - 1);
+
+ JsonObject[] dataObjects = new JsonObject[requestedRows.length()];
+ BiConsumer<JsonObject, Integer> rowDataMapper = (object,
+ index) -> dataObjects[index
+ - requestedRows.getStart()] = object;
+ List<T> fetchedItems = new ArrayList<>(dataObjects.length);
+
+ levelQueries.forEach(query -> {
+ List<T> results = doFetchQuery(query.startIndex, query.size,
+ getKeyMapper().get(query.node.getParentKey()))
+ .collect(Collectors.toList());
+ // TODO if the size differers from expected, all goes to hell
+ fetchedItems.addAll(results);
+ List<JsonObject> rowData = results.stream()
+ .map(item -> createDataObject(item, query.depth))
+ .collect(Collectors.toList());
+ mapper.reorderLevelQueryResultsToFlatOrdering(rowDataMapper,
+ query, rowData);
+ });
+ verifyNoNullItems(dataObjects, requestedRows);
+
+ sendData(requestedRows.getStart(), Arrays.asList(dataObjects));
+ getActiveDataHandler().addActiveData(fetchedItems.stream());
+ getActiveDataHandler().cleanUp(fetchedItems.stream());
+ }
+
+ setPushRows(Range.withLength(0, 0));
+ }
+
+ /*
+ * Verify that there are no null objects in the array, to fail eagerly and
+ * not just on the client side.
+ */
+ private void verifyNoNullItems(JsonObject[] dataObjects,
+ Range requestedRange) {
+ List<Integer> nullItems = new ArrayList<>(0);
+ AtomicInteger indexCounter = new AtomicInteger();
+ Stream.of(dataObjects).forEach(object -> {
+ int index = indexCounter.getAndIncrement();
+ if (object == null) {
+ nullItems.add(index);
+ }
+ });
+ if (!nullItems.isEmpty()) {
+ throw new IllegalStateException("For requested rows "
+ + requestedRange + ", there was null items for indexes "
+ + nullItems.stream().map(Object::toString)
+ .collect(Collectors.joining(", ")));
+ }
+ }
+
+ private JsonObject createDataObject(T item, int depth) {
+ JsonObject dataObject = getDataObject(item);
+
+ JsonObject hierarchyData = Json.createObject();
+ if (depth != -1) {
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_DEPTH, depth);
+ }
+
+ boolean isLeaf = !getDataProvider().hasChildren(item);
+ if (isLeaf) {
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF, true);
+ } else {
+ String key = getKeyMapper().key(item);
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_COLLAPSED,
+ mapper.isCollapsed(key));
+ hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF, false);
+ }
+
+ // add hierarchy information to row as metadata
+ dataObject.put(TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION,
+ hierarchyData);
+
+ return dataObject;
+ }
+
+ private void sendData(int startIndex, List<JsonObject> dataObjects) {
+ JsonArray dataArray = Json.createArray();
+ int i = 0;
+ for (JsonObject dataObject : dataObjects) {
+ dataArray.set(i++, dataObject);
+ }
+
+ getClientRpc().setData(startIndex, dataArray);
+ }
+
+ /**
+ * Returns the range of rows to push on initial response.
+ *
+ * @param rootLevelSize
+ * the amount of rows on the root level
+ * @return the range of rows to push initially
+ */
+ private Range getInitialRowsToPush(int rootLevelSize) {
+ // TODO optimize initial level to avoid unnecessary requests
+ return Range.between(0, Math.min(rootLevelSize, latestCacheSize));
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private Stream<T> doFetchQuery(int start, int length, T parentItem) {
+ return getDataProvider()
+ .fetch(new HierarchicalQuery(start, length, getBackEndSorting(),
+ getInMemorySorting(), getFilter(), parentItem));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private int doSizeQuery(T parentItem) {
+ return getDataProvider()
+ .getChildCount(new HierarchicalQuery(getFilter(), parentItem));
+ }
+
+ @Override
+ protected void onRequestRows(int firstRowIndex, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+ super.onRequestRows(firstRowIndex, numberOfRows, firstCachedRowIndex,
+ cacheSize);
+ }
+
+ @Override
+ protected void onDropRows(JsonArray keys) {
+ for (int i = 0; i < keys.length(); i++) {
+ // cannot drop expanded rows since the parent item is needed always
+ // when fetching more rows
+ String itemKey = keys.getString(i);
+ if (mapper.isCollapsed(itemKey)) {
+ getActiveDataHandler().dropActiveData(itemKey);
+ }
+ }
+ }
+
+ @Override
+ public HierarchicalDataProvider<T, ?> getDataProvider() {
+ return (HierarchicalDataProvider<T, ?>) super.getDataProvider();
+ }
+
+ /**
+ * Set the current hierarchical data provider for this communicator.
+ *
+ * @param dataProvider
+ * the data provider to set, not <code>null</code>
+ * @param initialFilter
+ * the initial filter value to use, or <code>null</code> to not
+ * use any initial filter value
+ *
+ * @param <F>
+ * the filter type
+ *
+ * @return a consumer that accepts a new filter value to use
+ */
+ public <F> SerializableConsumer<F> setDataProvider(
+ HierarchicalDataProvider<T, F> dataProvider, F initialFilter) {
+ return super.setDataProvider(dataProvider, initialFilter);
+ }
+
+ /**
+ * Set the current hierarchical data provider for this communicator.
+ *
+ * @param dataProvider
+ * the data provider to set, must extend
+ * {@link HierarchicalDataProvider}, not <code>null</code>
+ * @param initialFilter
+ * the initial filter value to use, or <code>null</code> to not
+ * use any initial filter value
+ *
+ * @param <F>
+ * the filter type
+ *
+ * @return a consumer that accepts a new filter value to use
+ */
+ @Override
+ public <F> SerializableConsumer<F> setDataProvider(
+ DataProvider<T, F> dataProvider, F initialFilter) {
+ if (dataProvider instanceof HierarchicalDataProvider) {
+ return super.setDataProvider(dataProvider, initialFilter);
+ }
+ throw new IllegalArgumentException(
+ "Only " + HierarchicalDataProvider.class.getName()
+ + " and subtypes supported.");
+ }
+
+ /**
+ * Collapses given row, removing all its subtrees.
+ *
+ * @param collapsedRowKey
+ * the key of the row, not {@code null}
+ * @param collapsedRowIndex
+ * the index of row to collapse
+ */
+ public void doCollapse(String collapsedRowKey, int collapsedRowIndex) {
+ if (collapsedRowIndex < 0 | collapsedRowIndex >= mapper.getTreeSize()) {
+ throw new IllegalArgumentException("Invalid row index "
+ + collapsedRowIndex + " when tree grid size of "
+ + mapper.getTreeSize());
+ }
+ Objects.requireNonNull(collapsedRowKey, "Row key cannot be null");
+ T collapsedItem = getKeyMapper().get(collapsedRowKey);
+ Objects.requireNonNull(collapsedItem,
+ "Cannot find item for given key " + collapsedItem);
+
+ int collapsedSubTreeSize = mapper.collapse(collapsedRowKey,
+ collapsedRowIndex);
+
+ getClientRpc().removeRows(collapsedRowIndex + 1,
+ collapsedSubTreeSize);
+ // FIXME seems like a slight overkill to do this just for refreshing
+ // expanded status
+ refresh(collapsedItem);
+ }
+
+ /**
+ * Expands the given row.
+ *
+ * @param expandedRowKey
+ * the key of the row, not {@code null}
+ * @param expandedRowIndex
+ * the index of the row to expand
+ */
+ public void doExpand(String expandedRowKey, final int expandedRowIndex) {
+ if (expandedRowIndex < 0 | expandedRowIndex >= mapper.getTreeSize()) {
+ throw new IllegalArgumentException("Invalid row index "
+ + expandedRowIndex + " when tree grid size of "
+ + mapper.getTreeSize());
+ }
+ Objects.requireNonNull(expandedRowKey, "Row key cannot be null");
+ final T expandedItem = getKeyMapper().get(expandedRowKey);
+ Objects.requireNonNull(expandedItem,
+ "Cannot find item for given key " + expandedRowKey);
+
+ final int expandedNodeSize = doSizeQuery(expandedItem);
+ if (expandedNodeSize == 0) {
+ // TODO handle 0 size -> not expandable
+ throw new IllegalStateException("Row with index " + expandedRowIndex
+ + " returned no child nodes.");
+ }
+
+ mapper.expand(expandedRowKey, expandedRowIndex, expandedNodeSize);
+
+ // TODO optimize by sending "enough" of the expanded items directly
+ getClientRpc().insertRows(expandedRowIndex + 1, expandedNodeSize);
+ // expanded node needs to be updated to be marked as expanded
+ // FIXME seems like a slight overkill to do this just for refreshing
+ // expanded status
+ refresh(expandedItem);
+ }
+
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java
index ec54a3a138..8dabdce2cc 100644
--- a/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java
+++ b/server/src/main/java/com/vaadin/data/provider/HierarchicalDataProvider.java
@@ -15,25 +15,93 @@
*/
package com.vaadin.data.provider;
+import java.util.stream.Stream;
+
/**
- *
+ * A common interface for fetching hierarchical data from a data source, such as
+ * an in-memory collection or a backend database.
+ *
* @author Vaadin Ltd
* @since 8.1
- *
+ *
* @param <T>
+ * data type
* @param <F>
+ * filter type
*/
public interface HierarchicalDataProvider<T, F> extends DataProvider<T, F> {
- public int getDepth(T item);
+ /**
+ * Get the number of immediate child data items for the parent item returned
+ * by a given query.
+ *
+ * @param query
+ * given query to request the count for
+ * @return the count of child data items for the data item
+ * {@link HierarchicalQuery#getParent()}
+ *
+ * @throws IllegalArgumentException
+ * if the query is not of type HierarchicalQuery
+ */
+ @Override
+ public default int size(Query<T, F> query) {
+ if (query instanceof HierarchicalQuery<?, ?>) {
+ return getChildCount((HierarchicalQuery<T, F>) query);
+ }
+ throw new IllegalArgumentException(
+ "Hierarchical data provider doesn't support non-hierarchical queries");
+ }
- public boolean isRoot(T item);
+ /**
+ * Fetches data from this HierarchicalDataProvider using given
+ * {@code query}. Only the immediate children of
+ * {@link HierarchicalQuery#getParent()} will be returned.
+ *
+ * @param query
+ * given query to request data with
+ * @return a stream of data objects resulting from the query
+ *
+ * @throws IllegalArgumentException
+ * if the query is not of type HierarchicalQuery
+ */
+ @Override
+ public default Stream<T> fetch(Query<T, F> query) {
+ if (query instanceof HierarchicalQuery<?, ?>) {
+ return fetchChildren((HierarchicalQuery<T, F>) query);
+ }
+ throw new IllegalArgumentException(
+ "Hierarchical data provider doesn't support non-hierarchical queries");
+ }
- public T getParent(T item);
+ /**
+ * Get the number of immediate child data items for the parent item returned
+ * by a given query.
+ *
+ * @param query
+ * given query to request the count for
+ * @return the count of child data items for the data item
+ * {@link HierarchicalQuery#getParent()}
+ */
+ public int getChildCount(HierarchicalQuery<T, F> query);
- public boolean isCollapsed(T item);
+ /**
+ * Fetches data from this HierarchicalDataProvider using given
+ * {@code query}. Only the immediate children of
+ * {@link HierarchicalQuery#getParent()} will be returned.
+ *
+ * @param query
+ * given query to request data with
+ * @return a stream of data objects resulting from the query
+ */
+ public Stream<T> fetchChildren(HierarchicalQuery<T, F> query);
+ /**
+ * Check whether a given item has any children associated with it.
+ *
+ * @param item
+ * the item to check for children
+ * @return whether the given item has children
+ */
public boolean hasChildren(T item);
- public void setCollapsed(T item, boolean b);
}
diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchicalQuery.java b/server/src/main/java/com/vaadin/data/provider/HierarchicalQuery.java
new file mode 100644
index 0000000000..6671464b31
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/HierarchicalQuery.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2016 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.data.provider;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Immutable hierarchical query object used to request data from a backend.
+ * Contains the parent node, index limits, sorting and filtering information.
+ *
+ * @param <T>
+ * bean type
+ * @param <F>
+ * filter type
+ *
+ * @since 8.0
+ */
+public class HierarchicalQuery<T, F> extends Query<T, F> {
+
+ private final T parent;
+
+ /**
+ * Constructs a new hierarchical query object with given filter and parent
+ * node.
+ *
+ * @param filter
+ * filtering for fetching; can be <code>null</code>
+ * @param parent
+ * the hierarchical parent object, can be <code>null</code>
+ */
+ public HierarchicalQuery(F filter, T parent) {
+ super(filter);
+ this.parent = parent;
+ }
+
+ /**
+ * Constructs a new hierarchical query object with given offset, limit,
+ * sorting and filtering.
+ *
+ * @param offset
+ * first index to fetch
+ * @param limit
+ * fetched item count
+ * @param sortOrders
+ * sorting order for fetching; used for sorting backends
+ * @param inMemorySorting
+ * comparator for sorting in-memory data
+ * @param filter
+ * filtering for fetching; can be <code>null</code>
+ * @param parent
+ * the hierarchical parent object, can be <code>null</code>
+ */
+ public HierarchicalQuery(int offset, int limit,
+ List<QuerySortOrder> sortOrders, Comparator<T> inMemorySorting,
+ F filter, T parent) {
+ super(offset, limit, sortOrders, inMemorySorting, filter);
+ this.parent = parent;
+ }
+
+ /**
+ * Get the hierarchical parent object, can be <code>null</code>.
+ *
+ * @return the hierarchical parent object, can be <code>null</code>
+ */
+ public T getParent() {
+ return parent;
+ }
+
+ /**
+ * Get an Optional of the hierarchical parent object.
+ *
+ * @see #getParent()
+ * @return the result of {@link #getParent()} wrapped by an Optional
+ */
+ public Optional<T> getParentOptional() {
+ return Optional.ofNullable(parent);
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java b/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java
new file mode 100644
index 0000000000..7f423b39c9
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/HierarchyMapper.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2000-2016 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.data.provider;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Mapper for hierarchical data.
+ * <p>
+ * Keeps track of the expanded nodes, and size of of the subtrees for each
+ * expanded node.
+ * <p>
+ * This class is framework internal implementation details, and can be changed /
+ * moved at any point. This means that you should not directly use this for
+ * anything.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+class HierarchyMapper implements Serializable {
+
+ private static final Logger LOGGER = Logger
+ .getLogger(HierarchyMapper.class.getName());
+
+ /**
+ * A POJO that represents a query data for a certain tree level.
+ */
+ static class TreeLevelQuery { // not serializable since not stored
+ /**
+ * The tree node that the query is for. Only used for fetching parent
+ * key.
+ */
+ final TreeNode node;
+ /** The start index of the query, from 0 to level's size - 1. */
+ final int startIndex;
+ /** The number of rows to fetch. s */
+ final int size;
+ /** The depth of this node. */
+ final int depth;
+ /** The first row index in grid, including all the nodes. */
+ final int firstRowIndex;
+ /** The direct subtrees for the node that effect the indexing. */
+ final List<TreeNode> subTrees;
+
+ TreeLevelQuery(TreeNode node, int startIndex, int size, int depth,
+ int firstRowIndex, List<TreeNode> subTrees) {
+ this.node = node;
+ this.startIndex = startIndex;
+ this.size = size;
+ this.depth = depth;
+ this.firstRowIndex = firstRowIndex;
+ this.subTrees = subTrees;
+ }
+ }
+
+ /**
+ * A level in the tree, either the root level or an expanded subtree level.
+ * <p>
+ * Comparable based on the {@link #startIndex}, which is flat from 0 to data
+ * size - 1.
+ */
+ static class TreeNode implements Serializable, Comparable<TreeNode> {
+
+ /** The key for the expanded item that this is a subtree of. */
+ private final String parentKey;
+ /** The first index on this level. */
+ private int startIndex;
+ /** The last index on this level, INCLUDING subtrees. */
+ private int endIndex;
+
+ TreeNode(String parentKey, int startIndex, int size) {
+ this.parentKey = parentKey;
+ this.startIndex = startIndex;
+ endIndex = startIndex + size - 1;
+ }
+
+ TreeNode(int startIndex) {
+ parentKey = "INVALID";
+ this.startIndex = startIndex;
+ }
+
+ int getStartIndex() {
+ return startIndex;
+ }
+
+ int getEndIndex() {
+ return endIndex;
+ }
+
+ String getParentKey() {
+ return parentKey;
+ }
+
+ private void push(int offset) {
+ startIndex += offset;
+ endIndex += offset;
+ }
+
+ private void pushEnd(int offset) {
+ endIndex += offset;
+ }
+
+ @Override
+ public int compareTo(TreeNode other) {
+ return Integer.valueOf(startIndex).compareTo(other.startIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "TreeNode [parent=" + parentKey + ", start=" + startIndex
+ + ", end=" + getEndIndex() + "]";
+ }
+
+ }
+
+ /** The expanded nodes in the tree. */
+ private final TreeSet<TreeNode> nodes = new TreeSet<>();
+
+ /**
+ * Resets the tree, sets given the root level size.
+ *
+ * @param rootLevelSize
+ * the number of items in the root level
+ */
+ public void reset(int rootLevelSize) {
+ nodes.clear();
+ nodes.add(new TreeNode(null, 0, rootLevelSize));
+ }
+
+ /**
+ * Returns the complete size of the tree, including all expanded subtrees.
+ *
+ * @return the size of the tree
+ */
+ public int getTreeSize() {
+ TreeNode rootNode = getNodeForKey(null)
+ .orElse(new TreeNode(null, 0, 0));
+ return rootNode.endIndex + 1;
+ }
+
+ /**
+ * Returns whether the node with the given is collapsed or not.
+ *
+ * @param itemKey
+ * the key of node to check
+ * @return {@code true} if collapsed, {@code false} if expanded
+ */
+ public boolean isCollapsed(String itemKey) {
+ return !getNodeForKey(itemKey).isPresent();
+ }
+
+ /**
+ * Return the depth of expanded node's subtree.
+ * <p>
+ * The root node depth is 0.
+ *
+ * @param expandedNodeKey
+ * the item key of the expanded node
+ * @return the depth of the expanded node
+ * @throws IllegalArgumentException
+ * if the node was not expanded
+ */
+ protected int getDepth(String expandedNodeKey) {
+ Optional<TreeNode> node = getNodeForKey(expandedNodeKey);
+ if (!node.isPresent()) {
+ throw new IllegalArgumentException("No node with given key "
+ + expandedNodeKey + " was expanded.");
+ }
+ TreeNode treeNode = node.get();
+ AtomicInteger start = new AtomicInteger(treeNode.startIndex);
+ AtomicInteger end = new AtomicInteger(treeNode.getEndIndex());
+ AtomicInteger depth = new AtomicInteger();
+ nodes.headSet(treeNode, false).descendingSet().forEach(higherNode -> {
+ if (higherNode.startIndex < start.get()
+ && higherNode.getEndIndex() >= end.get()) {
+ start.set(higherNode.startIndex);
+ depth.incrementAndGet();
+ }
+ });
+
+ return depth.get();
+ }
+
+ /**
+ * Returns the tree node for the given expanded item key, or an empty
+ * optional if the item was not expanded.
+ *
+ * @param expandedNodeKey
+ * the key of the item
+ * @return the tree node for the expanded item, or an empty optional if not
+ * expanded
+ */
+ protected Optional<TreeNode> getNodeForKey(String expandedNodeKey) {
+ return nodes.stream()
+ .filter(node -> Objects.equals(node.parentKey, expandedNodeKey))
+ .findAny();
+ }
+
+ /**
+ * Expands the node in the given index and with the given key.
+ *
+ * @param expanedRowKey
+ * the key of the expanded item
+ * @param expandedRowIndex
+ * the index of the expanded item
+ * @param expandedNodeSize
+ * the size of the subtree of the expanded node
+ * @throws IllegalStateException
+ * if the node was expanded already
+ */
+ protected void expand(String expanedRowKey, int expandedRowIndex,
+ int expandedNodeSize) {
+ if (expandedNodeSize < 1) {
+ throw new IllegalArgumentException(
+ "The expanded node's size cannot be less than 1, was "
+ + expandedNodeSize);
+ }
+ TreeNode newNode = new TreeNode(expanedRowKey, expandedRowIndex + 1,
+ expandedNodeSize);
+
+ boolean added = nodes.add(newNode);
+ if (!added) {
+ throw new IllegalStateException("Node in index " + expandedRowIndex
+ + " was expanded already.");
+ }
+
+ // push end indexes for parent nodes
+ List<TreeNode> updated = nodes.headSet(newNode, false).stream()
+ .filter(node -> node.getEndIndex() >= expandedRowIndex)
+ .collect(Collectors.toList());
+ nodes.removeAll(updated);
+ updated.stream().forEach(node -> node.pushEnd(expandedNodeSize));
+ nodes.addAll(updated);
+
+ // push start and end indexes for later nodes
+ updated = nodes.tailSet(newNode, false).stream()
+ .collect(Collectors.toList());
+ nodes.removeAll(updated);
+ updated.stream().forEach(node -> node.push(expandedNodeSize));
+ nodes.addAll(updated);
+ }
+
+ /**
+ * Collapses the node in the given index.
+ *
+ * @param key
+ * the key of the collapsed item
+ * @param collapsedRowIndex
+ * the index of the collapsed item
+ * @return the size of the complete subtree that was collapsed
+ * @throws IllegalStateException
+ * if the node was not collapsed, or if the given key is not the
+ * same as it was when the node has been expanded
+ */
+ protected int collapse(String key, int collapsedRowIndex) {
+ Objects.requireNonNull(key,
+ "The key for the item to collapse cannot be null.");
+ TreeNode collapsedNode = nodes
+ .ceiling(new TreeNode(collapsedRowIndex + 1));
+ if (collapsedNode == null
+ || collapsedNode.startIndex != collapsedRowIndex + 1) {
+ throw new IllegalStateException(
+ "Could not find expanded node for index "
+ + collapsedRowIndex + ", node was not collapsed");
+ }
+ if (!Objects.equals(key, collapsedNode.parentKey)) {
+ throw new IllegalStateException("The expected parent key " + key
+ + " is different for the collapsed node " + collapsedNode);
+ }
+
+ // remove complete subtree
+ AtomicInteger removedSubTreeSize = new AtomicInteger(
+ collapsedNode.getEndIndex() - collapsedNode.startIndex + 1);
+ nodes.tailSet(collapsedNode, false).removeIf(
+ node -> node.startIndex <= collapsedNode.getEndIndex());
+
+ final int offset = -1 * removedSubTreeSize.get();
+ // adjust parent end indexes
+ List<TreeNode> updated = nodes.headSet(collapsedNode, false).stream()
+ .filter(node -> node.getEndIndex() >= collapsedRowIndex)
+ .collect(Collectors.toList());
+ nodes.removeAll(updated);
+ updated.stream().forEach(node -> node.pushEnd(offset));
+ nodes.addAll(updated);
+
+ // adjust start and end indexes for latter nodes
+ updated = nodes.tailSet(collapsedNode, false).stream()
+ .collect(Collectors.toList());
+ nodes.removeAll(updated);
+ updated.stream().forEach(node -> node.push(offset));
+ nodes.addAll(updated);
+
+ nodes.remove(collapsedNode);
+
+ return removedSubTreeSize.get();
+ }
+
+ /**
+ * Splits the given range into queries per tree level.
+ *
+ * @param firstRow
+ * the first row to fetch
+ * @param lastRow
+ * the last row to fetch
+ * @return a stream of query data per level
+ * @see #reorderLevelQueryResultsToFlatOrdering(BiConsumer, TreeLevelQuery,
+ * List)
+ */
+ protected Stream<TreeLevelQuery> splitRangeToLevelQueries(
+ final int firstRow, final int lastRow) {
+ return nodes.stream()
+ // filter to parts intersecting with the range
+ .filter(node -> node.startIndex <= lastRow
+ && firstRow <= node.getEndIndex())
+ // split into queries per level with level based indexing
+ .map(node -> {
+
+ // calculate how subtrees effect indexing and size
+ int depth = getDepth(node.parentKey);
+ List<TreeNode> directSubTrees = nodes.tailSet(node, false)
+ .stream()
+ // find subtrees
+ .filter(subTree -> node.startIndex < subTree
+ .getEndIndex()
+ && subTree.startIndex < node.getEndIndex())
+ // filter to direct subtrees
+ .filter(subTree -> getDepth(
+ subTree.parentKey) == (depth + 1))
+ .collect(Collectors.toList());
+ // first intersecting index in flat order
+ AtomicInteger firstIntersectingRowIndex = new AtomicInteger(
+ Math.max(node.startIndex, firstRow));
+ // last intersecting index in flat order
+ final int lastIntersectingRowIndex = Math
+ .min(node.getEndIndex(), lastRow);
+ // start index for this level
+ AtomicInteger start = new AtomicInteger(
+ firstIntersectingRowIndex.get() - node.startIndex);
+ // how many nodes should be fetched for this level
+ AtomicInteger size = new AtomicInteger(
+ lastIntersectingRowIndex
+ - firstIntersectingRowIndex.get() + 1);
+
+ // reduce subtrees before requested index
+ directSubTrees.stream().filter(subtree -> subtree
+ .getEndIndex() < firstIntersectingRowIndex.get())
+ .forEachOrdered(subtree -> {
+ start.addAndGet(-1 * (subtree.getEndIndex()
+ - subtree.startIndex + 1));
+ });
+ // if requested start index is in the middle of a
+ // subtree, start is after that
+ List<TreeNode> intersectingSubTrees = new ArrayList<>();
+ directSubTrees.stream()
+ .filter(subtree -> subtree.startIndex <= firstIntersectingRowIndex
+ .get() && firstIntersectingRowIndex
+ .get() <= subtree.getEndIndex())
+ .findFirst().ifPresent(subtree -> {
+ int previous = firstIntersectingRowIndex
+ .getAndSet(subtree.getEndIndex() + 1);
+ int delta = previous
+ - firstIntersectingRowIndex.get();
+ start.addAndGet(subtree.startIndex - previous);
+ size.addAndGet(delta);
+ intersectingSubTrees.add(subtree);
+ });
+ // reduce size of subtrees after first row that intersect
+ // with requested range
+ directSubTrees.stream()
+ .filter(subtree -> firstIntersectingRowIndex
+ .get() < subtree.startIndex
+ && subtree.endIndex <= lastIntersectingRowIndex)
+ .forEachOrdered(subtree -> {
+ // reduce subtree size that is part of the
+ // requested range from query size
+ size.addAndGet(
+ -1 * (Math.min(subtree.getEndIndex(),
+ lastIntersectingRowIndex)
+ - subtree.startIndex + 1));
+ intersectingSubTrees.add(subtree);
+ });
+ return new TreeLevelQuery(node, start.get(), size.get(),
+ depth, firstIntersectingRowIndex.get(),
+ intersectingSubTrees);
+
+ }).filter(query -> query.size > 0);
+
+ }
+
+ /**
+ * Merges the tree level query results into flat grid ordering.
+ *
+ * @param rangePositionCallback
+ * the callback to place the results into
+ * @param query
+ * the query data for the results
+ * @param results
+ * the results to reorder
+ * @param <T>
+ * the type of the results
+ */
+ protected <T> void reorderLevelQueryResultsToFlatOrdering(
+ BiConsumer<T, Integer> rangePositionCallback, TreeLevelQuery query,
+ List<T> results) {
+ AtomicInteger nextPossibleIndex = new AtomicInteger(
+ query.firstRowIndex);
+ for (T item : results) {
+ // search for any intersecting subtrees and push index if necessary
+ query.subTrees.stream().filter(
+ subTree -> subTree.startIndex <= nextPossibleIndex.get()
+ && nextPossibleIndex.get() <= subTree.getEndIndex())
+ .findAny().ifPresent(intersecting -> {
+ nextPossibleIndex.addAndGet(intersecting.getEndIndex()
+ - intersecting.startIndex + 1);
+ query.subTrees.remove(intersecting);
+ });
+ rangePositionCallback.accept(item,
+ nextPossibleIndex.getAndIncrement());
+ }
+ }
+
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/InMemoryHierarchicalDataProvider.java b/server/src/main/java/com/vaadin/data/provider/InMemoryHierarchicalDataProvider.java
new file mode 100644
index 0000000000..b86c3186c6
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/provider/InMemoryHierarchicalDataProvider.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2000-2016 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.data.provider;
+
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import com.vaadin.data.HierarchyData;
+import com.vaadin.data.ValueProvider;
+import com.vaadin.server.SerializableComparator;
+import com.vaadin.server.SerializableFunction;
+import com.vaadin.server.SerializablePredicate;
+import com.vaadin.shared.data.sort.SortDirection;
+
+/**
+ * A {@link DataProvider} for in-memory hierarchical data.
+ *
+ * @see HierarchyData
+ *
+ * @author Vaadin Ltd
+ * @since 8.1
+ *
+ * @param <T>
+ * data type
+ */
+public class InMemoryHierarchicalDataProvider<T> extends
+ AbstractHierarchicalDataProvider<T, SerializablePredicate<T>> implements
+ ConfigurableFilterDataProvider<T, SerializablePredicate<T>, SerializablePredicate<T>> {
+
+ private final HierarchyData<T> hierarchyData;
+
+ private SerializablePredicate<T> filter = null;
+
+ private SerializableComparator<T> sortOrder = null;
+
+ /**
+ * Constructs a new InMemoryHierarchicalDataProvider.
+ * <p>
+ * All changes made to the given HierarchyData object will also be visible
+ * through this data provider.
+ *
+ * @param hierarchyData
+ * the backing HierarchyData for this provider
+ */
+ public InMemoryHierarchicalDataProvider(HierarchyData<T> hierarchyData) {
+ this.hierarchyData = hierarchyData;
+ }
+
+ /**
+ * Return the underlying hierarchical data of this provider.
+ *
+ * @return the underlying data of this provider
+ */
+ public HierarchyData<T> getData() {
+ return hierarchyData;
+ }
+
+ @Override
+ public boolean isInMemory() {
+ return true;
+ }
+
+ @Override
+ public boolean hasChildren(T item) {
+ return !hierarchyData.getChildren(item).isEmpty();
+ }
+
+ @Override
+ public int getChildCount(
+ HierarchicalQuery<T, SerializablePredicate<T>> query) {
+ return (int) fetchChildren(query).count();
+ }
+
+ @Override
+ public Stream<T> fetchChildren(
+ HierarchicalQuery<T, SerializablePredicate<T>> query) {
+ Stream<T> childStream = getFilteredStream(
+ hierarchyData.getChildren(query.getParent()).stream(),
+ query.getFilter());
+
+ Optional<Comparator<T>> comparing = Stream
+ .of(query.getInMemorySorting(), sortOrder)
+ .filter(c -> c != null)
+ .reduce((c1, c2) -> c1.thenComparing(c2));
+
+ if (comparing.isPresent()) {
+ childStream = childStream.sorted(comparing.get());
+ }
+
+ return childStream.skip(query.getOffset()).limit(query.getLimit());
+ }
+
+ @Override
+ public void setFilter(SerializablePredicate<T> filter) {
+ this.filter = filter;
+ refreshAll();
+ }
+
+ /**
+ * Adds a filter to be applied to all queries. The filter will be used in
+ * addition to any filter that has been set or added previously.
+ *
+ * @see #addFilter(ValueProvider, SerializablePredicate)
+ * @see #addFilterByValue(ValueProvider, Object)
+ * @see #setFilter(SerializablePredicate)
+ *
+ * @param filter
+ * the filter to add, not <code>null</code>
+ */
+ public void addFilter(SerializablePredicate<T> filter) {
+ Objects.requireNonNull(filter, "Filter cannot be null");
+
+ if (this.filter == null) {
+ setFilter(filter);
+ } else {
+ SerializablePredicate<T> oldFilter = this.filter;
+ setFilter(item -> oldFilter.test(item) && filter.test(item));
+ }
+ }
+
+ /**
+ * Sets the comparator to use as the default sorting for this data provider.
+ * This overrides the sorting set by any other method that manipulates the
+ * default sorting of this data provider.
+ * <p>
+ * The default sorting is used if the query defines no sorting. The default
+ * sorting is also used to determine the ordering of items that are
+ * considered equal by the sorting defined in the query.
+ *
+ * @see #setSortOrder(ValueProvider, SortDirection)
+ * @see #addSortComparator(SerializableComparator)
+ *
+ * @param comparator
+ * a comparator to use, or <code>null</code> to clear any
+ * previously set sort order
+ */
+ public void setSortComparator(SerializableComparator<T> comparator) {
+ sortOrder = comparator;
+ refreshAll();
+ }
+
+ /**
+ * Adds a comparator to the default sorting for this data provider. If no
+ * default sorting has been defined, then the provided comparator will be
+ * used as the default sorting. If a default sorting has been defined, then
+ * the provided comparator will be used to determine the ordering of items
+ * that are considered equal by the previously defined default sorting.
+ * <p>
+ * The default sorting is used if the query defines no sorting. The default
+ * sorting is also used to determine the ordering of items that are
+ * considered equal by the sorting defined in the query.
+ *
+ * @see #setSortComparator(SerializableComparator)
+ * @see #addSortOrder(ValueProvider, SortDirection)
+ *
+ * @param comparator
+ * a comparator to add, not <code>null</code>
+ */
+ public void addSortComparator(SerializableComparator<T> comparator) {
+ Objects.requireNonNull(comparator, "Sort order to add cannot be null");
+ SerializableComparator<T> originalComparator = sortOrder;
+ if (originalComparator == null) {
+ setSortComparator(comparator);
+ } else {
+ setSortComparator((a, b) -> {
+ int result = originalComparator.compare(a, b);
+ if (result == 0) {
+ result = comparator.compare(a, b);
+ }
+ return result;
+ });
+ }
+ }
+
+ @Override
+ public <C> DataProvider<T, C> withConvertedFilter(
+ SerializableFunction<C, SerializablePredicate<T>> filterConverter) {
+ Objects.requireNonNull(filterConverter,
+ "Filter converter can't be null");
+ return new DataProviderWrapper<T, C, SerializablePredicate<T>>(this) {
+
+ @Override
+ protected SerializablePredicate<T> getFilter(Query<T, C> query) {
+ return query.getFilter().map(filterConverter).orElse(null);
+ }
+
+ @Override
+ public int size(Query<T, C> t) {
+ if (t instanceof HierarchicalQuery<?, ?>) {
+ return dataProvider.size(new HierarchicalQuery<>(
+ t.getOffset(), t.getLimit(), t.getSortOrders(),
+ t.getInMemorySorting(), getFilter(t),
+ ((HierarchicalQuery<T, C>) t).getParent()));
+ }
+ throw new IllegalArgumentException(
+ "Hierarchical data provider doesn't support non-hierarchical queries");
+ }
+
+ @Override
+ public Stream<T> fetch(Query<T, C> t) {
+ if (t instanceof HierarchicalQuery<?, ?>) {
+ return dataProvider.fetch(new HierarchicalQuery<>(
+ t.getOffset(), t.getLimit(), t.getSortOrders(),
+ t.getInMemorySorting(), getFilter(t),
+ ((HierarchicalQuery<T, C>) t).getParent()));
+ }
+ throw new IllegalArgumentException(
+ "Hierarchical data provider doesn't support non-hierarchical queries");
+ }
+ };
+ }
+
+ private Stream<T> getFilteredStream(Stream<T> stream,
+ Optional<SerializablePredicate<T>> queryFilter) {
+ if (filter != null) {
+ stream = stream.filter(filter);
+ }
+ return queryFilter.map(stream::filter).orElse(stream);
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/provider/ListDataProvider.java b/server/src/main/java/com/vaadin/data/provider/ListDataProvider.java
index 8fc6d4a364..fba2f94d9d 100644
--- a/server/src/main/java/com/vaadin/data/provider/ListDataProvider.java
+++ b/server/src/main/java/com/vaadin/data/provider/ListDataProvider.java
@@ -31,8 +31,7 @@ import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.ui.UI;
/**
- * {@link DataProvider} wrapper for {@link Collection}s. This class does not
- * actually handle the {@link Query} parameters.
+ * {@link DataProvider} wrapper for {@link Collection}s.
*
* @param <T>
* data type
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index 19222d8c85..175b7d1ae5 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -2084,20 +2084,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
* @see #withPropertySet(PropertySet)
*/
public Grid() {
- this(new PropertySet<T>() {
- @Override
- public Stream<PropertyDefinition<T, ?>> getProperties() {
- // No columns configured by default
- return Stream.empty();
- }
-
- @Override
- public Optional<PropertyDefinition<T, ?>> getProperty(String name) {
- throw new IllegalStateException(
- "A Grid created without a bean type class literal or a custom property set"
- + " doesn't support finding properties by name.");
- }
- });
+ this(new DataCommunicator<>());
}
/**
@@ -2117,6 +2104,32 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
}
/**
+ * Creates a new grid with the given data communicator and without support
+ * for creating columns based on property names.
+ *
+ * @param dataCommunicator
+ * the custom data communicator to set
+ * @see #Grid()
+ * @see #Grid(PropertySet, DataCommunicator)
+ */
+ protected Grid(DataCommunicator<T> dataCommunicator) {
+ this(new PropertySet<T>() {
+ @Override
+ public Stream<PropertyDefinition<T, ?>> getProperties() {
+ // No columns configured by default
+ return Stream.empty();
+ }
+
+ @Override
+ public Optional<PropertyDefinition<T, ?>> getProperty(String name) {
+ throw new IllegalStateException(
+ "A Grid created without a bean type class literal or a custom property set"
+ + " doesn't support finding properties by name.");
+ }
+ }, dataCommunicator);
+ }
+
+ /**
* Creates a grid using a custom {@link PropertySet} implementation for
* configuring the initial columns and resolving property names for
* {@link #addColumn(String)} and
@@ -2128,6 +2141,27 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
* the property set implementation to use, not <code>null</code>.
*/
protected Grid(PropertySet<T> propertySet) {
+ this(propertySet, new DataCommunicator<>());
+ }
+
+ /**
+ * Creates a grid using a custom {@link PropertySet} implementation and
+ * custom data communicator.
+ * <p>
+ * Property set is used for configuring the initial columns and resolving
+ * property names for {@link #addColumn(String)} and
+ * {@link Column#setEditorComponent(HasValue)}.
+ *
+ * @see #withPropertySet(PropertySet)
+ *
+ * @param propertySet
+ * the property set implementation to use, not <code>null</code>.
+ * @param dataCommunicator
+ * the data communicator to use, not<code>null</code>
+ */
+ protected Grid(PropertySet<T> propertySet,
+ DataCommunicator<T> dataCommunicator) {
+ super(dataCommunicator);
registerRpc(new GridServerRpcImpl());
setDefaultHeaderRow(appendHeaderRow());
setSelectionModel(new SingleSelectionModelImpl<>());
@@ -3844,7 +3878,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
}
}
- private void readData(Element body,
+ protected void readData(Element body,
List<DeclarativeValueProvider<T>> providers) {
getSelectionModel().deselectAll();
List<T> items = new ArrayList<>();
@@ -3883,8 +3917,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
if (designContext.shouldWriteData(this)) {
Element bodyElement = tableElement.appendElement("tbody");
- getDataProvider().fetch(new Query<>()).forEach(
- item -> writeRow(bodyElement, item, designContext));
+ writeData(bodyElement, designContext);
}
if (getFooter().getRowCount() > 0) {
@@ -3893,6 +3926,11 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
}
}
+ protected void writeData(Element body, DesignContext designContext) {
+ getDataProvider().fetch(new Query<>())
+ .forEach(item -> writeRow(body, item, designContext));
+ }
+
private void writeRow(Element container, T item, DesignContext context) {
Element tableRow = container.appendElement("tr");
tableRow.attr("item", serializeDeclarativeRepresentation(item));
diff --git a/server/src/main/java/com/vaadin/ui/TreeGrid.java b/server/src/main/java/com/vaadin/ui/TreeGrid.java
index 46e6c99b59..82c857ac86 100644
--- a/server/src/main/java/com/vaadin/ui/TreeGrid.java
+++ b/server/src/main/java/com/vaadin/ui/TreeGrid.java
@@ -15,21 +15,32 @@
*/
package com.vaadin.ui;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Stream;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.data.HierarchyData;
import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.DataProvider;
+import com.vaadin.data.provider.HierarchicalDataCommunicator;
import com.vaadin.data.provider.HierarchicalDataProvider;
+import com.vaadin.data.provider.HierarchicalQuery;
+import com.vaadin.data.provider.InMemoryHierarchicalDataProvider;
import com.vaadin.shared.ui.treegrid.NodeCollapseRpc;
-import com.vaadin.shared.ui.treegrid.TreeGridCommunicationConstants;
import com.vaadin.shared.ui.treegrid.TreeGridState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignFormatter;
import com.vaadin.ui.renderers.AbstractRenderer;
import com.vaadin.ui.renderers.Renderer;
-import elemental.json.Json;
-import elemental.json.JsonObject;
/**
* A grid component for displaying hierarchical tabular data.
@@ -43,55 +54,130 @@ import elemental.json.JsonObject;
public class TreeGrid<T> extends Grid<T> {
public TreeGrid() {
- super();
-
- // Attaches hierarchy data to the row
- addDataGenerator((item, rowData) -> {
-
- JsonObject hierarchyData = Json.createObject();
- hierarchyData.put(TreeGridCommunicationConstants.ROW_DEPTH,
- getDataProvider().getDepth(item));
-
- boolean isLeaf = !getDataProvider().hasChildren(item);
- if (isLeaf) {
- hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF,
- true);
- } else {
- hierarchyData.put(TreeGridCommunicationConstants.ROW_COLLAPSED,
- getDataProvider().isCollapsed(item));
- hierarchyData.put(TreeGridCommunicationConstants.ROW_LEAF,
- false);
- }
-
- // add hierarchy information to row as metadata
- rowData.put(
- TreeGridCommunicationConstants.ROW_HIERARCHY_DESCRIPTION,
- hierarchyData);
- });
+ super(new HierarchicalDataCommunicator<>());
registerRpc(new NodeCollapseRpc() {
@Override
- public void toggleCollapse(String rowKey) {
- T item = getDataCommunicator().getKeyMapper().get(rowKey);
- TreeGrid.this.toggleCollapse(item);
+ public void setNodeCollapsed(String rowKey, int rowIndex,
+ boolean collapse) {
+ if (collapse) {
+ getDataCommunicator().doCollapse(rowKey, rowIndex);
+ } else {
+ getDataCommunicator().doExpand(rowKey, rowIndex);
+ }
}
});
}
- // TODO: construct a "flat" in memory hierarchical data provider?
+ /**
+ * Sets the data items of this component provided as a collection.
+ * <p>
+ * The provided items are wrapped into a
+ * {@link InMemoryHierarchicalDataProvider} backed by a flat
+ * {@link HierarchyData} structure. The data provider instance is used as a
+ * parameter for the {@link #setDataProvider(DataProvider)} method. It means
+ * that the items collection can be accessed later on via
+ * {@link InMemoryHierarchicalDataProvider#getData()}:
+ *
+ * <pre>
+ * <code>
+ * TreeGrid<String> treeGrid = new TreeGrid<>();
+ * treeGrid.setItems(Arrays.asList("a","b"));
+ * ...
+ *
+ * HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
+ * </code>
+ * </pre>
+ * <p>
+ * The returned HierarchyData instance may be used as-is to add, remove or
+ * modify items in the hierarchy. These modifications to the object are not
+ * automatically reflected back to the TreeGrid. Items modified should be
+ * refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
+ * when adding or removing items
+ * {@link HierarchicalDataProvider#refreshAll()} should be called.
+ *
+ * @param items
+ * the data items to display, not null
+ */
@Override
public void setItems(Collection<T> items) {
- throw new UnsupportedOperationException("Not implemented");
+ Objects.requireNonNull(items, "Given collection may not be null");
+ setDataProvider(new InMemoryHierarchicalDataProvider<>(
+ new HierarchyData<T>().addItems(null, items)));
}
+ /**
+ * Sets the data items of this component provided as a stream.
+ * <p>
+ * The provided items are wrapped into a
+ * {@link InMemoryHierarchicalDataProvider} backed by a flat
+ * {@link HierarchyData} structure. The data provider instance is used as a
+ * parameter for the {@link #setDataProvider(DataProvider)} method. It means
+ * that the items collection can be accessed later on via
+ * {@link InMemoryHierarchicalDataProvider#getData()}:
+ *
+ * <pre>
+ * <code>
+ * TreeGrid<String> treeGrid = new TreeGrid<>();
+ * treeGrid.setItems(Stream.of("a","b"));
+ * ...
+ *
+ * HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
+ * </code>
+ * </pre>
+ * <p>
+ * The returned HierarchyData instance may be used as-is to add, remove or
+ * modify items in the hierarchy. These modifications to the object are not
+ * automatically reflected back to the TreeGrid. Items modified should be
+ * refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
+ * when adding or removing items
+ * {@link HierarchicalDataProvider#refreshAll()} should be called.
+ *
+ * @param items
+ * the data items to display, not null
+ */
@Override
public void setItems(Stream<T> items) {
- throw new UnsupportedOperationException("Not implemented");
+ Objects.requireNonNull(items, "Given stream may not be null");
+ setDataProvider(new InMemoryHierarchicalDataProvider<>(
+ new HierarchyData<T>().addItems(null, items)));
}
+ /**
+ * Sets the data items of this listing.
+ * <p>
+ * The provided items are wrapped into a
+ * {@link InMemoryHierarchicalDataProvider} backed by a flat
+ * {@link HierarchyData} structure. The data provider instance is used as a
+ * parameter for the {@link #setDataProvider(DataProvider)} method. It means
+ * that the items collection can be accessed later on via
+ * {@link InMemoryHierarchicalDataProvider#getData()}:
+ *
+ * <pre>
+ * <code>
+ * TreeGrid<String> treeGrid = new TreeGrid<>();
+ * treeGrid.setItems("a","b");
+ * ...
+ *
+ * HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
+ * </code>
+ * </pre>
+ * <p>
+ * The returned HierarchyData instance may be used as-is to add, remove or
+ * modify items in the hierarchy. These modifications to the object are not
+ * automatically reflected back to the TreeGrid. Items modified should be
+ * refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
+ * when adding or removing items
+ * {@link HierarchicalDataProvider#refreshAll()} should be called.
+ *
+ * @param items
+ * the data items to display, not null
+ */
@Override
- public void setItems(T... items) {
- throw new UnsupportedOperationException("Not implemented");
+ public void setItems(@SuppressWarnings("unchecked") T... items) {
+ Objects.requireNonNull(items, "Given items may not be null");
+ setDataProvider(new InMemoryHierarchicalDataProvider<>(
+ new HierarchyData<T>().addItems(null, items)));
}
@Override
@@ -138,34 +224,97 @@ public class TreeGrid<T> extends Grid<T> {
return (TreeGridState) super.getState(markAsDirty);
}
- /**
- * Toggle the expansion of an item in this grid. If the item is already
- * expanded, it will be collapsed.
- * <p>
- * Toggling expansion on a leaf item in the hierarchy will have no effect.
- *
- * @param item
- * the item to toggle expansion for
- */
- public void toggleCollapse(T item) {
- getDataProvider().setCollapsed(item,
- !getDataProvider().isCollapsed(item));
- getDataCommunicator().reset();
+ @Override
+ public HierarchicalDataCommunicator<T> getDataCommunicator() {
+ return (HierarchicalDataCommunicator<T>) super.getDataCommunicator();
}
@Override
public HierarchicalDataProvider<T, ?> getDataProvider() {
- DataProvider<T, ?> dataProvider = super.getDataProvider();
- // FIXME DataCommunicator by default has a CallbackDataProvider if no
- // DataProvider is set, resulting in a class cast exception if we don't
- // check it here.
+ if (!(super.getDataProvider() instanceof HierarchicalDataProvider)) {
+ return null;
+ }
+ return (HierarchicalDataProvider<T, ?>) super.getDataProvider();
+ }
- // Once fixed, remove this method from the exclude list in
- // StateGetDoesNotMarkDirtyTest
- if (!(dataProvider instanceof HierarchicalDataProvider)) {
- throw new IllegalStateException("No data provider has been set.");
+ @Override
+ protected void doReadDesign(Element design, DesignContext context) {
+ super.doReadDesign(design, context);
+ Attributes attrs = design.attributes();
+ if (attrs.hasKey("hierarchy-column")) {
+ setHierarchyColumn(DesignAttributeHandler
+ .readAttribute("hierarchy-column", attrs, String.class));
+ }
+ }
+
+ @Override
+ protected void readData(Element body,
+ List<DeclarativeValueProvider<T>> providers) {
+ getSelectionModel().deselectAll();
+ List<T> selectedItems = new ArrayList<>();
+ HierarchyData<T> data = new HierarchyData<T>();
+
+ for (Element row : body.children()) {
+ 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);
+ }
+ Elements cells = row.children();
+ int i = 0;
+ for (Element cell : cells) {
+ providers.get(i).addValue(item, cell.html());
+ i++;
+ }
+ }
+
+ setDataProvider(new InMemoryHierarchicalDataProvider<>(data));
+ selectedItems.forEach(getSelectionModel()::select);
+ }
+
+ @Override
+ protected void doWriteDesign(Element design, DesignContext designContext) {
+ super.doWriteDesign(design, designContext);
+ if (getColumnByInternalId(getState(false).hierarchyColumnId) != null) {
+ String hierarchyColumn = getColumnByInternalId(
+ getState(false).hierarchyColumnId).getId();
+ DesignAttributeHandler.writeAttribute("hierarchy-column",
+ design.attributes(), hierarchyColumn, null, String.class,
+ designContext);
+ }
+ }
+
+ @Override
+ protected void writeData(Element body, DesignContext designContext) {
+ getDataProvider().fetch(new HierarchicalQuery<>(null, null))
+ .forEach(item -> writeRow(body, item, null, designContext));
+ }
+
+ private void writeRow(Element container, T item, T parent,
+ DesignContext context) {
+ Element tableRow = container.appendElement("tr");
+ tableRow.attr("item", serializeDeclarativeRepresentation(item));
+ if (parent != null) {
+ tableRow.attr("parent", serializeDeclarativeRepresentation(parent));
+ }
+ if (getSelectionModel().isSelected(item)) {
+ tableRow.attr("selected", "");
+ }
+ for (Column<T, ?> column : getColumns()) {
+ Object value = column.getValueProvider().apply(item);
+ tableRow.appendElement("td")
+ .append(Optional.ofNullable(value).map(Object::toString)
+ .map(DesignFormatter::encodeForTextNode)
+ .orElse(""));
}
- return (HierarchicalDataProvider<T, ?>) dataProvider;
+ getDataProvider().fetch(new HierarchicalQuery<>(null, item))
+ .forEach(childItem -> writeRow(container, childItem, item,
+ context));
}
@Override