/*
* 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.ui.components.grid;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.Query;
import com.vaadin.event.selection.MultiSelectionEvent;
import com.vaadin.event.selection.MultiSelectionListener;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.selection.GridMultiSelectServerRpc;
import com.vaadin.shared.ui.grid.MultiSelectionModelState;
import com.vaadin.ui.MultiSelect;
/**
* Multiselection model for grid.
* <p>
* Shows a column of checkboxes as the first column of grid. Each checkbox
* triggers the selection for that row.
* <p>
* Implementation detail: The Grid selection is updated immediately after user
* selection on client side, without waiting for the server response.
*
* @author Vaadin Ltd.
* @since 8.0
*
* @param <T>
* the type of the selected item in grid.
*/
public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
implements MultiSelectionModel<T> {
private class GridMultiSelectServerRpcImpl
implements GridMultiSelectServerRpc {
@Override
public void select(String key) {
MultiSelectionModelImpl.this.updateSelection(
new LinkedHashSet<>(Arrays.asList(getData(key))),
Collections.emptySet(), true);
}
@Override
public void deselect(String key) {
if (getState(false).allSelected) {
// updated right away on client side
getState(false).allSelected = false;
getUI().getConnectorTracker()
.getDiffState(MultiSelectionModelImpl.this)
.put("allSelected", false);
}
MultiSelectionModelImpl.this.updateSelection(Collections.emptySet(),
new LinkedHashSet<>(Arrays.asList(getData(key))), true);
}
@Override
public void selectAll() {
onSelectAll(true);
}
@Override
public void deselectAll() {
onDeselectAll(true);
}
}
private List<T> selection = new ArrayList<>();
private SelectAllCheckBoxVisibility selectAllCheckBoxVisibility = SelectAllCheckBoxVisibility.DEFAULT;
@Override
protected void init() {
registerRpc(new GridMultiSelectServerRpcImpl());
}
@Override
protected MultiSelectionModelState getState() {
return (MultiSelectionModelState) super.getState();
}
@Override
protected MultiSelectionModelState getState(boolean markAsDirty) {
return (MultiSelectionModelState) super.getState(markAsDirty);
}
@Override
public void setSelectAllCheckBoxVisibility(
SelectAllCheckBoxVisibility selectAllCheckBoxVisibility) {
if (this.selectAllCheckBoxVisibility != selectAllCheckBoxVisibility) {
this.selectAllCheckBoxVisibility = selectAllCheckBoxVisibility;
markAsDirty();
}
}
@Override
public SelectAllCheckBoxVisibility getSelectAllCheckBoxVisibility() {
return selectAllCheckBoxVisibility;
}
@Override
public boolean isSelectAllCheckBoxVisible() {
updateCanSelectAll();
return getState(false).selectAllCheckBoxVisible;
}
/**
* Returns whether all items are selected or not.
* <p>
* This is only {@code true} if user has selected all rows with the select
* all checkbox on client side, or if {@link #selectAll()} has been used
* from server side.
*
* @return {@code true} if all selected, {@code false} if not
*/
public boolean isAllSelected() {
return getState(false).allSelected;
}
@Override
public boolean isSelected(T item) {
return isAllSelected()
|| selectionContainsId(getGrid().getDataProvider().getId(item));
}
/**
* Returns if the given id belongs to one of the selected items.
*
* @param id
* the id to check for
* @return {@code true} if id is selected, {@code false} if not
*/
protected boolean selectionContainsId(Object id) {
DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
return selection.stream().map(dataProvider::getId)
.anyMatch(i -> id.equals(i));
}
@Override
public void beforeClientResponse(boolean initial) {
super.beforeClientResponse(initial);
updateCanSelectAll();
}
/**
* Controls whether the select all checkbox is visible in the grid default
* header, or not.
* <p>
* This is updated as a part of {@link #beforeClientResponse(boolean)},
* since the data provider for grid can be changed on the fly.
*
* @see SelectAllCheckBoxVisibility
*/
protected void updateCanSelectAll() {
switch (selectAllCheckBoxVisibility) {
case VISIBLE:
getState(false).selectAllCheckBoxVisible = true;
break;
case HIDDEN:
getState(false).selectAllCheckBoxVisible = false;
break;
case DEFAULT:
getState(false).selectAllCheckBoxVisible = getGrid()
.getDataProvider().isInMemory();
break;
default:
break;
}
}
@Override
public Registration addMultiSelectionListener(
MultiSelectionListener<T> listener) {
return addListener(MultiSelectionEvent.class, listener,
MultiSelectionListener.SELECTION_CHANGE_METHOD);
}
@Override
public Set<T> getSelectedItems() {
return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
}
@Override
public void updateSelection(Set<T> addedItems, Set<T> removedItems) {
updateSelection(addedItems, removedItems, false);
}
@Override
public void selectAll() {
onSelectAll(false);
}
@Override
public void deselectAll() {
onDeselectAll(false);
}
/**
* Gets a wrapper for using this grid as a multiselect in a binder.
*
* @return a multiselect wrapper for grid
*/
@Override
public MultiSelect<T> asMultiSelect() {
return new MultiSelect<T>() {
@Override
public void setValue(Set<T> value) {
Objects.requireNonNull(value);
Set<T> copy = value.stream().map(Objects::requireNonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
updateSelection(copy, new LinkedHashSet<>(getSelectedItems()));
}
@Override
public Set<T> getValue() {
return getSelectedItems();
}
@Override
public Registration addValueChangeListener(
com.vaadin.data.HasValue.ValueChangeListener<Set<T>> listener) {
return addSelectionListener(
event -> listener.valueChange(event));
}
@Override
public void setRequiredIndicatorVisible(
boolean requiredIndicatorVisible) {
// TODO support required indicator for grid ?
throw new UnsupportedOperationException(
"Required indicator is not supported in grid.");
}
@Override
public boolean isRequiredIndicatorVisible() {
// TODO support required indicator for grid ?
throw new UnsupportedOperationException(
"Required indicator is not supported in grid.");
}
@Override
public void setReadOnly(boolean readOnly) {
setUserSelectionAllowed(!readOnly);
}
@Override
public boolean isReadOnly() {
return !isUserSelectionAllowed();
}
@Override
public void updateSelection(Set<T> addedItems,
Set<T> removedItems) {
MultiSelectionModelImpl.this.updateSelection(addedItems,
removedItems);
}
@Override
public Set<T> getSelectedItems() {
return MultiSelectionModelImpl.this.getSelectedItems();
}
@Override
public Registration addSelectionListener(
MultiSelectionListener<T> listener) {
return MultiSelectionModelImpl.this
.addMultiSelectionListener(listener);
}
};
}
/**
* Triggered when the user checks the select all checkbox.
*
* @param userOriginated
* {@code true} if originated from client side by user
*/
protected void onSelectAll(boolean userOriginated) {
if (userOriginated) {
verifyUserCanSelectAll();
// all selected state has been updated in client side already
getState(false).allSelected = true;
getUI().getConnectorTracker().getDiffState(this).put("allSelected",
true);
} else {
getState().allSelected = true;
}
DataProvider<T, ?> dataSource = getGrid().getDataProvider();
// this will fetch everything from backend
Stream<T> stream = dataSource.fetch(new Query<>());
LinkedHashSet<T> allItems = new LinkedHashSet<>();
stream.forEach(allItems::add);
updateSelection(allItems, Collections.emptySet(), userOriginated);
}
/**
* Triggered when the user unchecks the select all checkbox.
*
* @param userOriginated
* {@code true} if originated from client side by user
*/
protected void onDeselectAll(boolean userOriginated) {
if (userOriginated) {
verifyUserCanSelectAll();
// all selected state has been update in client side already
getState(false).allSelected = false;
getUI().getConnectorTracker().getDiffState(this).put("allSelected",
false);
} else {
getState().allSelected = false;
}
updateSelection(Collections.emptySet(), new LinkedHashSet<>(selection),
userOriginated);
}
private void verifyUserCanSelectAll() {
if (!getState(false).selectAllCheckBoxVisible) {
throw new IllegalStateException(
"Cannot select all from client since select all checkbox should not be visible");
}
}
/**
* Updates the selection by adding and removing the given items.
* <p>
* All selection updates should go through this method, since it handles
* incorrect parameters, removing duplicates, notifying data communicator
* and and firing events.
*
* @param addedItems
* the items added to selection, not {@code} null
* @param removedItems
* the items removed from selection, not {@code} null
* @param userOriginated
* {@code true} if this was used originated, {@code false} if not
*/
protected void updateSelection(Set<T> addedItems, Set<T> removedItems,
boolean userOriginated) {
Objects.requireNonNull(addedItems);
Objects.requireNonNull(removedItems);
if (userOriginated && !isUserSelectionAllowed()) {
throw new IllegalStateException("Client tried to update selection"
+ " although user selection is disallowed");
}
// if there are duplicates, some item is both added & removed, just
// discard that and leave things as was before
addedItems.removeIf(item -> removedItems.remove(item));
if (selection.containsAll(addedItems)
&& Collections.disjoint(selection, removedItems)) {
return;
}
// update allSelected for server side selection updates
if (getState(false).allSelected && !removedItems.isEmpty()
&& !userOriginated) {
getState().allSelected = false;
}
doUpdateSelection(set -> {
// order of add / remove does not matter since no duplicates
set.removeAll(removedItems);
set.addAll(addedItems);
// refresh method is NOOP for items that are not present client side
DataCommunicator<T> dataCommunicator = getGrid()
.getDataCommunicator();
removedItems.forEach(dataCommunicator::refresh);
addedItems.forEach(dataCommunicator::refresh);
}, userOriginated);
}
private void doUpdateSelection(Consumer<Collection<T>> handler,
boolean userOriginated) {
if (getParent() == null) {
throw new IllegalStateException(
"Trying to update selection for grid selection model that has been detached from the grid.");
}
LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
handler.accept(selection);
fireEvent(new MultiSelectionEvent<>(getGrid(), asMultiSelect(),
oldSelection, userOriginated));
}
@Override
public void refreshData(T item) {
DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
Object refreshId = dataProvider.getId(item);
for (int i = 0; i < selection.size(); ++i) {
if (dataProvider.getId(selection.get(i)).equals(refreshId)) {
selection.set(i, item);
return;
}
}
}
}