summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java31
-rw-r--r--server/src/com/vaadin/ui/Grid.java230
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java2
3 files changed, 260 insertions, 3 deletions
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
index cf2284a62e..62b8214cbd 100644
--- a/server/src/com/vaadin/data/RpcDataProviderExtension.java
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -51,6 +51,7 @@ import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.CellReference;
import com.vaadin.ui.Grid.CellStyleGenerator;
import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.DetailComponentManager;
import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.renderers.Renderer;
@@ -113,6 +114,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
final Object itemId = indexToItemId.get(ii);
if (!isPinned(itemId)) {
+ detailComponentManager.destroyDetails(itemId);
itemIdToKey.remove(itemId);
indexToItemId.remove(ii);
}
@@ -154,6 +156,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
indexToItemId.forcePut(index, itemId);
+ detailComponentManager.createDetails(itemId, index);
}
index++;
}
@@ -747,14 +750,18 @@ public class RpcDataProviderExtension extends AbstractExtension {
*/
private Set<Object> visibleDetails = new HashSet<Object>();
+ private DetailComponentManager detailComponentManager;
+
/**
* Creates a new data provider using the given container.
*
* @param container
* the container to make available
*/
- public RpcDataProviderExtension(Indexed container) {
+ public RpcDataProviderExtension(Indexed container,
+ DetailComponentManager detailComponentManager) {
this.container = container;
+ this.detailComponentManager = detailComponentManager;
rpc = getRpcProxy(DataProviderRpc.class);
registerRpc(new DataRequestRpc() {
@@ -1018,6 +1025,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
JsonArray rowArray = Json.createArray();
rowArray.set(0, row);
rpc.setRowData(index, rowArray);
+
+ if (isDetailsVisible(itemId)) {
+ detailComponentManager.createDetails(itemId, index);
+ }
}
}
@@ -1155,10 +1166,28 @@ public class RpcDataProviderExtension extends AbstractExtension {
*/
public void setDetailsVisible(Object itemId, boolean visible) {
final boolean modified;
+
if (visible) {
modified = visibleDetails.add(itemId);
+
+ /*
+ * We don't want to create the component here, since the component
+ * might be out of view, and thus we don't know where the details
+ * should end up on the client side. This is also a great thing to
+ * optimize away, so that in case a lot of things would be opened at
+ * once, a huge chunk of data doesn't get sent over immediately.
+ */
+
} else {
modified = visibleDetails.remove(itemId);
+
+ /*
+ * Here we can try to destroy the component no matter what. The
+ * component has been removed and should be detached from the
+ * component hierarchy. The details row will be closed on the client
+ * side automatically.
+ */
+ detailComponentManager.destroyDetails(itemId);
}
int rowIndex = keyMapper.getIndex(itemId);
diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java
index b56bb0d036..da5cedd999 100644
--- a/server/src/com/vaadin/ui/Grid.java
+++ b/server/src/com/vaadin/ui/Grid.java
@@ -36,6 +36,8 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
import com.vaadin.data.Container;
@@ -75,6 +77,7 @@ import com.vaadin.server.KeyMapper;
import com.vaadin.server.VaadinSession;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.ConnectorIndexChange;
import com.vaadin.shared.ui.grid.EditorClientRpc;
import com.vaadin.shared.ui.grid.EditorServerRpc;
import com.vaadin.shared.ui.grid.GridClientRpc;
@@ -183,6 +186,11 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
/**
* This method is called for whenever a new details row needs to be
* generated.
+ * <p>
+ * <em>Note:</em> If a component gets generated, it may not be manually
+ * attached anywhere, nor may it be a reused instance &ndash; each
+ * invocation of this method should produce a unique and isolated
+ * component instance.
*
* @param rowReference
* the reference for the row for which to generate details
@@ -2810,6 +2818,208 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * A class that makes detail component related internal communication
+ * possible between {@link RpcDataProviderExtension} and grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ public final class DetailComponentManager implements Serializable {
+ /**
+ * This map represents all the components that have been requested for
+ * each item id.
+ * <p>
+ * Normally this map is consistent with what is displayed in the
+ * component hierarchy (and thus the DOM). The only time this map is out
+ * of sync with the DOM is between the any calls to
+ * {@link #createDetails(Object, int)} or
+ * {@link #destroyDetails(Object)}, and
+ * {@link GridClientRpc#setDetailsConnectorChanges(Set)}.
+ * <p>
+ * This is easily checked: if {@link #unattachedComponents} is
+ * {@link Collection#isEmpty() empty}, then this field is consistent
+ * with the connector hierarchy.
+ */
+ private final Map<Object, Component> visibleDetailsComponents = new HashMap<Object, Component>();
+
+ /** A lookup map for which row contains which details component. */
+ private BiMap<Integer, Component> rowIndexToDetails = HashBiMap
+ .create();
+
+ /**
+ * A copy of {@link #rowIndexToDetails} from its last stable state. Used
+ * for creating a diff against {@link #rowIndexToDetails}.
+ *
+ * @see #getAndResetConnectorChanges()
+ */
+ private BiMap<Integer, Component> prevRowIndexToDetails = HashBiMap
+ .create();
+
+ /**
+ * A set keeping track on components that have been created, but not
+ * attached. They should be attached at some later point in time.
+ * <p>
+ * This isn't strictly requried, but it's a handy explicit log. You
+ * could find out the same thing by taking out all the other components
+ * and checking whether Grid is their parent or not.
+ */
+ private final Set<Component> unattachedComponents = new HashSet<Component>();
+
+ /**
+ * Creates a details component by the request of the client side, with
+ * the help of the user-defined {@link DetailsGenerator}.
+ * <p>
+ * Also keeps internal bookkeeping up to date.
+ *
+ * @param itemId
+ * the item id for which to create the details component.
+ * Assumed not <code>null</code> and that a component is not
+ * currently present for this item previously
+ * @param rowIndex
+ * the row index for {@code itemId}
+ * @throws IllegalStateException
+ * if the current details generator provides a component
+ * that was manually attached, or if the same instance has
+ * already been provided
+ */
+ public void createDetails(Object itemId, int rowIndex)
+ throws IllegalStateException {
+ assert itemId != null : "itemId was null";
+ Integer newRowIndex = Integer.valueOf(rowIndex);
+
+ assert !visibleDetailsComponents.containsKey(itemId) : "itemId already has a component. Should be destroyed first.";
+
+ RowReference rowReference = new RowReference(Grid.this);
+ rowReference.set(itemId);
+
+ Component details = getDetailsGenerator().getDetails(rowReference);
+ if (details != null) {
+
+ if (details.getParent() != null) {
+ String generatorName = getDetailsGenerator().getClass()
+ .getName();
+ throw new IllegalStateException(generatorName
+ + " generated a details component that already "
+ + "was attached. (itemId: " + itemId + ", row: "
+ + rowIndex + ", component: " + details);
+ }
+
+ if (rowIndexToDetails.containsValue(details)) {
+ String generatorName = getDetailsGenerator().getClass()
+ .getName();
+ throw new IllegalStateException(generatorName
+ + " provided a details component that already "
+ + "exists in Grid. (itemId: " + itemId + ", row: "
+ + rowIndex + ", component: " + details);
+ }
+
+ visibleDetailsComponents.put(itemId, details);
+ rowIndexToDetails.put(newRowIndex, details);
+ unattachedComponents.add(details);
+ }
+
+ /*
+ * Don't attach the components here. It's done by
+ * GridServerRpc.sendDetailsComponents in a separate roundtrip.
+ */
+ }
+
+ /**
+ * Destroys correctly a details component, by the request of the client
+ * side.
+ * <p>
+ * Also keeps internal bookkeeping up to date.
+ *
+ * @param itemId
+ * the item id for which to destroy the details component
+ */
+ public void destroyDetails(Object itemId) {
+ Component removedComponent = visibleDetailsComponents
+ .remove(itemId);
+ if (removedComponent == null) {
+ return;
+ }
+
+ rowIndexToDetails.inverse().remove(removedComponent);
+
+ removedComponent.setParent(null);
+ markAsDirty();
+ }
+
+ /**
+ * Gets all details components that are currently attached to the grid.
+ * <p>
+ * Used internally by the Grid object.
+ *
+ * @return all details components that are currently attached to the
+ * grid
+ */
+ Collection<Component> getComponents() {
+ Set<Component> components = new HashSet<Component>(
+ visibleDetailsComponents.values());
+ components.removeAll(unattachedComponents);
+ return components;
+ }
+
+ /**
+ * Gets information on how the connectors have changed.
+ * <p>
+ * This method only returns the changes that have been made between two
+ * calls of this method. I.e. Calling this method once will reset the
+ * state for the next state.
+ * <p>
+ * Used internally by the Grid object.
+ *
+ * @return information on how the connectors have changed
+ */
+ Set<ConnectorIndexChange> getAndResetConnectorChanges() {
+ Set<ConnectorIndexChange> changes = new HashSet<ConnectorIndexChange>();
+
+ // populate diff with added/changed
+ for (Entry<Integer, Component> entry : rowIndexToDetails.entrySet()) {
+ Component component = entry.getValue();
+ assert component != null : "rowIndexToDetails contains a null component";
+
+ Integer newIndex = entry.getKey();
+ Integer oldIndex = prevRowIndexToDetails.inverse().get(
+ component);
+
+ /*
+ * only attach components. Detaching already happened in
+ * destroyDetails.
+ */
+ if (newIndex != null && oldIndex == null) {
+ assert unattachedComponents.contains(component) : "unattachedComponents does not contain component for index "
+ + newIndex + " (" + component + ")";
+ component.setParent(Grid.this);
+ unattachedComponents.remove(component);
+ }
+
+ if (!SharedUtil.equals(oldIndex, newIndex)) {
+ changes.add(new ConnectorIndexChange(component, oldIndex,
+ newIndex));
+ }
+ }
+
+ // populate diff with removed
+ for (Entry<Integer, Component> entry : prevRowIndexToDetails
+ .entrySet()) {
+ Integer oldIndex = entry.getKey();
+ Component component = entry.getValue();
+ Integer newIndex = rowIndexToDetails.inverse().get(component);
+ if (newIndex == null) {
+ changes.add(new ConnectorIndexChange(null, oldIndex, null));
+ }
+ }
+
+ // reset diff map
+ prevRowIndexToDetails = HashBiMap.create(rowIndexToDetails);
+
+ return changes;
+ }
+ }
+
+ /**
* The data source attached to the grid
*/
private Container.Indexed datasource;
@@ -2916,8 +3126,15 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+ /**
+ * The user-defined details generator.
+ *
+ * @see #setDetailsGenerator(DetailsGenerator)
+ */
private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;
+ private final DetailComponentManager detailComponentManager = new DetailComponentManager();
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
@@ -3118,6 +3335,13 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
fireEvent(new ItemClickEvent(Grid.this, item, itemId,
propertyId, details));
}
+
+ @Override
+ public void sendDetailsComponents(int fetchId) {
+ getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
+ detailComponentManager.getAndResetConnectorChanges(),
+ fetchId);
+ }
});
registerRpc(new EditorServerRpc() {
@@ -3278,7 +3502,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
sortOrder.clear();
}
- datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension = new RpcDataProviderExtension(container,
+ detailComponentManager);
datasourceExtension.extend(this, columnKeys);
/*
@@ -4607,6 +4832,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
componentList.addAll(getEditorFields());
+
+ componentList.addAll(detailComponentManager.getComponents());
+
return componentList.iterator();
}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
index 9ecf131c5b..54f5dcdbc7 100644
--- a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
@@ -47,7 +47,7 @@ public class DataProviderExtension {
container = new IndexedContainer();
populate(container);
- dataProvider = new RpcDataProviderExtension(container);
+ dataProvider = new RpcDataProviderExtension(container, null);
keyMapper = dataProvider.getKeyMapper();
}