/*
* Copyright 2014 mattitahvonenitmill.
*
* 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 org.vaadin.viritin.v7.fields;
import com.vaadin.v7.event.ItemClickEvent;
import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
import com.vaadin.event.MouseEvents;
import com.vaadin.server.Resource;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.ui.Component;
import com.vaadin.v7.ui.Table;
import com.vaadin.util.ReflectTools;
import org.apache.commons.lang3.StringUtils;
import org.vaadin.viritin.LazyList;
import org.vaadin.viritin.v7.ListContainer;
import org.vaadin.viritin.MSize;
import org.vaadin.viritin.v7.SortableLazyList;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.commons.beanutils.DynaClass;
import static org.vaadin.viritin.LazyList.DEFAULT_PAGE_SIZE;
/**
* A better typed version of the Table component in Vaadin. Expects that users
* are always listing POJOs, which is most often the case in modern Java
* development. Uses ListContainer to bind data due to its superior performance
* compared to BeanItemContainer.
* <p>
* Note, that MTable don't support "multiselection mode". It is also very little
* tested in "editable mode".
* <p>
* If your "list" of entities is too large to load into memory, there are also
* constructors for typical "service layers". Then paged requests are used to
* fetch entities that are visible (or almost visible) in the UI. Behind the
* scenes LazyList is used to "wrap" your service into list.
*
* @param <T> the type of the POJO listed in this Table.
*/
public class MTable<T> extends Table {
private static final long serialVersionUID = 3330985834015680723L;
private ListContainer<T> bic;
private String[] pendingProperties;
private String[] pendingHeaders;
private Collection sortableProperties;
// Cached last sort properties, used to maintain sorting when re-setting
// lazy load strategy
private String sortProperty;
private boolean sortAscending;
public MTable() {
}
/**
* Constructs a Table with explicit bean type. Handy for example if your
* beans are JPA proxies or the table is empty when showing it initially.
*
* @param type the type of beans that are listed in this table
*/
public MTable(Class<? extends T> type) {
bic = createContainer(type);
setContainerDataSource(bic);
}
public MTable(T... beans) {
this(new ArrayList<>(Arrays.asList(beans)));
}
/**
* Constructs a Table with explicit bean type. Handy for example if your
* beans are JPA proxies or the table is empty when showing it initially.
*
* @param type the type of beans that are listed in this table
*/
public MTable(DynaClass type) {
bic = createContainer(type);
setContainerDataSource(bic);
}
/**
* A shorthand to create MTable using LazyList. By default page size of
* LazyList.DEFAULT_PAGE_SIZE (30) is used.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
*/
public MTable(LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
this(new LazyList(pageProvider, countProvider, DEFAULT_PAGE_SIZE));
}
/**
* A shorthand to create MTable using LazyList.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
*/
public MTable(LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
this(new LazyList(pageProvider, countProvider, pageSize));
}
/**
* A shorthand to create MTable using SortableLazyList. By default page size
* of LazyList.DEFAULT_PAGE_SIZE (30) is used.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
*/
public MTable(SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
this(new SortableLazyList(pageProvider, countProvider, DEFAULT_PAGE_SIZE));
}
/**
* A shorthand to create MTable using SortableLazyList.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
*/
public MTable(SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
this(new SortableLazyList(pageProvider, countProvider, pageSize));
}
/**
* A shorthand to create MTable using LazyList. By default page size of
* LazyList.DEFAULT_PAGE_SIZE (30) is used.
*
* @param rowType the type of entities listed in the table
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
*/
public MTable(Class<T> rowType, LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
this(rowType, pageProvider, countProvider, DEFAULT_PAGE_SIZE);
}
/**
* A shorthand to create MTable using LazyList.
*
* @param rowType the type of entities listed in the table
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
*/
public MTable(Class<T> rowType, LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
this(rowType);
lazyLoadFrom(pageProvider, countProvider, pageSize);
}
/**
* A shorthand to create MTable using SortableLazyList. By default page size
* of LazyList.DEFAULT_PAGE_SIZE (30) is used.
*
* @param rowType the type of entities listed in the table
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
*/
public MTable(Class<T> rowType,
SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
this(rowType, pageProvider, countProvider, DEFAULT_PAGE_SIZE);
}
/**
* A shorthand to create MTable using SortableLazyList.
*
* @param rowType the type of entities listed in the table
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
*/
public MTable(Class<T> rowType,
SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
this(rowType);
lazyLoadFrom(pageProvider, countProvider, pageSize);
}
public MTable(Collection<T> beans) {
this();
if (beans != null) {
bic = createContainer(beans);
setContainerDataSource(bic);
}
}
/**
* Makes the table lazy load its content with given strategy.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @return this MTable object
*/
public MTable<T> lazyLoadFrom(LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
setBeans(new LazyList(pageProvider, countProvider, DEFAULT_PAGE_SIZE));
return this;
}
/**
* Makes the table lazy load its content with given strategy.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
* @return this MTable object
*/
public MTable<T> lazyLoadFrom(LazyList.PagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
setBeans(new LazyList(pageProvider, countProvider, pageSize));
return this;
}
/**
* Makes the table lazy load its content with given strategy.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @return this MTable object
*/
public MTable<T> lazyLoadFrom(
SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider) {
setBeans(new SortableLazyList(pageProvider, countProvider,
DEFAULT_PAGE_SIZE));
return this;
}
/**
* Makes the table lazy load its content with given strategy.
*
* @param pageProvider the interface via entities are fetched
* @param countProvider the interface via the count of items is detected
* @param pageSize the page size (aka maxResults) that is used in paging.
* @return this MTable object
*/
public MTable<T> lazyLoadFrom(
SortableLazyList.SortablePagingProvider<T> pageProvider,
LazyList.CountProvider countProvider, int pageSize) {
setBeans(new SortableLazyList(pageProvider, countProvider, pageSize));
return this;
}
protected ListContainer<T> createContainer(Class<? extends T> type) {
return new ListContainer<T>(type); // Type parameter just to keep NB happy
}
private ListContainer<T> createContainer(DynaClass type) {
return new ListContainer<>(type);
}
protected ListContainer<T> createContainer(Collection<T> beans) {
return new ListContainer<>(beans);
}
protected ListContainer<T> getContainer() {
return bic;
}
public MTable<T> withProperties(String... visibleProperties) {
if (isContainerInitialized()) {
bic.setContainerPropertyIds(visibleProperties);
setVisibleColumns((Object[]) visibleProperties);
} else {
pendingProperties = visibleProperties;
for (String string : visibleProperties) {
addContainerProperty(string, String.class, "");
}
}
for (String visibleProperty : visibleProperties) {
String[] parts = StringUtils.splitByCharacterTypeCamelCase(
visibleProperty);
parts[0] = StringUtils.capitalize(parts[0]);
for (int i = 1; i < parts.length; i++) {
parts[i] = parts[i].toLowerCase();
}
String saneCaption = StringUtils.join(parts, " ");
setColumnHeader(visibleProperty, saneCaption);
}
return this;
}
protected boolean isContainerInitialized() {
return bic != null;
}
public MTable<T> withColumnHeaders(String... columnNamesForVisibleProperties) {
if (isContainerInitialized()) {
setColumnHeaders(columnNamesForVisibleProperties);
} else {
pendingHeaders = columnNamesForVisibleProperties;
// Add headers to temporary indexed container, in case table is initially
// empty
for (String prop : columnNamesForVisibleProperties) {
addContainerProperty(prop, String.class, "");
}
}
return this;
}
/**
* the propertyId has to been added before!
*
* @param propertyId columns property id
* @param width width to be reserved for columns content
* @return MTable
*/
public MTable<T> withColumnWidth(String propertyId, int width) {
setColumnWidth(propertyId, width);
return this;
}
/**
* Explicitly sets which properties are sortable in the UI.
*
* @param sortableProperties the collection of property identifiers/names
* that should be sortable
* @return the MTable instance
*/
public MTable<T> setSortableProperties(Collection sortableProperties) {
this.sortableProperties = sortableProperties;
return this;
}
/**
* Explicitly sets which properties are sortable in the UI.
*
* @param sortableProperties the collection of property identifiers/names
* that should be sortable
* @return the MTable instance
*/
public MTable<T> setSortableProperties(String... sortableProperties) {
this.sortableProperties = Arrays.asList(sortableProperties);
return this;
}
public Collection getSortableProperties() {
return sortableProperties;
}
@Override
public Collection<?> getSortableContainerPropertyIds() {
if (getSortableProperties() != null) {
return Collections.unmodifiableCollection(sortableProperties);
}
return super.getSortableContainerPropertyIds();
}
public void addMValueChangeListener(MValueChangeListener<T> listener) {
addListener(MValueChangeEvent.class, listener,
MValueChangeEventImpl.VALUE_CHANGE_METHOD);
// implicitly consider the table should be selectable
setSelectable(true);
// Needed as client side checks only for "real value change listener"
setImmediate(true);
}
public void removeMValueChangeListener(MValueChangeListener<T> listener) {
removeListener(MValueChangeEvent.class, listener,
MValueChangeEventImpl.VALUE_CHANGE_METHOD);
setSelectable(hasListeners(MValueChangeEvent.class));
}
@Override
protected void fireValueChange(boolean repaintIsNotNeeded) {
super.fireValueChange(repaintIsNotNeeded);
fireEvent(new MValueChangeEventImpl(this));
}
protected void ensureBeanItemContainer(Collection<T> beans) {
if (!isContainerInitialized()) {
bic = createContainer(beans);
if (pendingProperties != null) {
bic.setContainerPropertyIds(pendingProperties);
setContainerDataSource(bic, Arrays.asList(pendingProperties));
pendingProperties = null;
} else {
setContainerDataSource(bic);
}
if (pendingHeaders != null) {
setColumnHeaders(pendingHeaders);
pendingHeaders = null;
}
}
}
@Override
public T getValue() {
return (T) super.getValue();
}
@Override
@Deprecated
public void setMultiSelect(boolean multiSelect) {
super.setMultiSelect(multiSelect);
}
public MTable<T> addBeans(T... beans) {
addBeans(Arrays.asList(beans));
return this;
}
public MTable<T> addBeans(Collection<T> beans) {
if (!beans.isEmpty()) {
if (isContainerInitialized()) {
bic.addAll(beans);
} else {
ensureBeanItemContainer(beans);
}
}
return this;
}
public MTable<T> setBeans(T... beans) {
setBeans(new ArrayList<>(Arrays.asList(beans)));
return this;
}
public MTable<T> setRows(T... beansForRows) {
return setBeans(beansForRows);
}
public MTable<T> setBeans(Collection<T> beans) {
if (sortProperty != null && beans instanceof SortableLazyList) {
final SortableLazyList sll = (SortableLazyList) beans;
sll.setSortProperty(new String[]{sortProperty});
sll.setSortAscending(new boolean[]{sortAscending});
}
if (!isContainerInitialized() && !beans.isEmpty()) {
ensureBeanItemContainer(beans);
} else if (isContainerInitialized()) {
bic.setCollection(beans);
}
return this;
}
public MTable<T> setRows(Collection<T> beansForRows) {
return setBeans(beansForRows);
}
/**
* Makes the first column of the table a primary column, for which all space
* left out from other columns is given. The method also makes sure the
* Table has a width defined (otherwise the setting makes no sense).
*
*
* @return {@link MTable}
*/
public MTable<T> expandFirstColumn() {
expand(getContainerPropertyIds().iterator().next().toString());
if (getWidth() == -1) {
return withFullWidth();
}
return this;
}
public MTable<T> withFullWidth() {
setWidth(100, Unit.PERCENTAGE);
return this;
}
public MTable<T> withHeight(String height) {
setHeight(height);
return this;
}
public MTable<T> withFullHeight() {
return withHeight("100%");
}
public MTable<T> withWidth(String width) {
setWidth(width);
return this;
}
public MTable<T> withSize(MSize mSize) {
setWidth(mSize.getWidth(), mSize.getWidthUnit());
setHeight(mSize.getHeight(), mSize.getHeightUnit());
return this;
}
public MTable<T> withCaption(String caption) {
setCaption(caption);
return this;
}
public MTable<T> withStyleName(String... styleNames) {
for (String styleName : styleNames) {
addStyleName(styleName);
}
return this;
}
public MTable<T> withIcon(Resource icon) {
setIcon(icon);
return this;
}
public MTable<T> withId(String id) {
setId(id);
return this;
}
public MTable<T> expand(String... propertiesToExpand) {
for (String property : propertiesToExpand) {
setColumnExpandRatio(property, 1);
}
return this;
}
/**
* the propertyId has to been added before!
*
* @param propertyId columns property id
* @param ratio the expandRatio used to divide excess space for this column
* @return MTable
*/
public MTable<T> withColumnExpand(String propertyId, float ratio) {
setColumnExpandRatio(propertyId, ratio);
return this;
}
private ItemClickListener itemClickPiggyback;
private void ensureTypedItemClickPiggybackListener() {
if (itemClickPiggyback == null) {
itemClickPiggyback = new ItemClickListener() {
private static final long serialVersionUID = -2318797984292753676L;
@Override
public void itemClick(ItemClickEvent event) {
fireEvent(new RowClickEvent<T>(event));
}
};
addItemClickListener(itemClickPiggyback);
}
}
public MTable<T> withProperties(List<String> a) {
return withProperties(a.toArray(new String[a.size()]));
}
public static interface SimpleColumnGenerator<T> {
public Object generate(T entity);
}
public MTable<T> withGeneratedColumn(String columnId,
final SimpleColumnGenerator<T> columnGenerator) {
addGeneratedColumn(columnId, new ColumnGenerator() {
private static final long serialVersionUID = 2855441121974230973L;
@Override
public Object generateCell(Table source, Object itemId,
Object columnId) {
return columnGenerator.generate((T) itemId);
}
});
return this;
}
public static class SortEvent extends Component.Event {
private static final long serialVersionUID = 267382182533317834L;
private boolean preventContainerSort = false;
private final boolean sortAscending;
private final String sortProperty;
public SortEvent(Component source, boolean sortAscending,
String property) {
super(source);
this.sortAscending = sortAscending;
this.sortProperty = property;
}
public String getSortProperty() {
return sortProperty;
}
public boolean isSortAscending() {
return sortAscending;
}
/**
* By calling this method you can prevent the sort call to the container
* used by MTable. In this case you most most probably you want to
* manually sort the container instead.
*/
public void preventContainerSort() {
preventContainerSort = true;
}
public boolean isPreventContainerSort() {
return preventContainerSort;
}
private final static Method method = ReflectTools.findMethod(
SortListener.class, "onSort",
SortEvent.class);
}
/**
* A listener that can be used to track when user sorts table on a column.
*
* Via the event user can also prevent the "container sort" done by the
* Table and implement own sorting logic instead (e.g. get a sorted list of
* entities from the backend).
*
*/
public interface SortListener {
public void onSort(SortEvent event);
}
public MTable addSortListener(SortListener listener) {
addListener(SortEvent.class, listener, SortEvent.method);
return this;
}
public MTable removeSortListener(SortListener listener) {
removeListener(SortEvent.class, listener, SortEvent.method);
return this;
}
private boolean isSorting = false;
@Override
public void sort(Object[] propertyId, boolean[] ascending) throws UnsupportedOperationException {
if (isSorting) {
// hack to avoid recursion
return;
}
boolean refreshingPreviouslyEnabled = disableContentRefreshing();
boolean defaultTableSortingMethod = false;
try {
isSorting = true;
// create sort event and fire it, allow user to prevent default
// operation
sortAscending = ascending != null && ascending.length > 0 ? ascending[0] : true;
sortProperty = propertyId != null && propertyId.length > 0 ? propertyId[0].
toString() : null;
final SortEvent sortEvent = new SortEvent(this, sortAscending,
sortProperty);
fireEvent(sortEvent);
if (!sortEvent.isPreventContainerSort()) {
// if not prevented, do sorting
if (bic != null && bic.getItemIds() instanceof SortableLazyList) {
// Explicit support for SortableLazyList, set sort parameters
// it uses to backend services and clear internal buffers
SortableLazyList<T> sll = (SortableLazyList) bic.
getItemIds();
if (ascending == null || ascending.length == 0) {
sll.sort(true, null);
} else {
sll.sort(ascending[0], propertyId[0].toString());
}
resetPageBuffer();
} else {
super.sort(propertyId, ascending);
defaultTableSortingMethod = true;
}
}
if (!defaultTableSortingMethod) {
// Ensure the values used in UI are set as this method is public
// and can be called by both UI event and app logic
setSortAscending(sortAscending);
setSortContainerPropertyId(sortProperty);
}
} catch (UnsupportedOperationException e) {
throw new RuntimeException(e);
} finally {
isSorting = false;
if (refreshingPreviouslyEnabled) {
enableContentRefreshing(true);
}
}
}
/**
* A version of ItemClickEvent that is properly typed and named.
*
* @param <T> the type of the row
*/
public static class RowClickEvent<T> extends MouseEvents.ClickEvent {
private static final long serialVersionUID = -73902815731458960L;
public static final Method TYPED_ITEM_CLICK_METHOD;
static {
try {
TYPED_ITEM_CLICK_METHOD = RowClickListener.class.
getDeclaredMethod("rowClick",
new Class[]{RowClickEvent.class});
} catch (final java.lang.NoSuchMethodException e) {
// This should never happen
throw new java.lang.RuntimeException();
}
}
private final ItemClickEvent orig;
public RowClickEvent(ItemClickEvent orig) {
super(orig.getComponent(), null);
this.orig = orig;
}
/**
* @return the entity(~row) that was clicked.
*/
public T getEntity() {
return (T) orig.getItemId();
}
/**
* @return the entity(~row) that was clicked.
*/
public T getRow() {
return getEntity();
}
/**
* @return the identifier of the column on which the row click happened.
*/
public String getColumnId() {
return orig.getPropertyId().toString();
}
@Override
public MouseEventDetails.MouseButton getButton() {
return orig.getButton();
}
@Override
public int getClientX() {
return orig.getClientX();
}
@Override
public int getClientY() {
return orig.getClientY();
}
@Override
public int getRelativeX() {
return orig.getRelativeX();
}
@Override
public int getRelativeY() {
return orig.getRelativeY();
}
@Override
public boolean isAltKey() {
return orig.isAltKey();
}
@Override
public boolean isCtrlKey() {
return orig.isCtrlKey();
}
@Override
public boolean isDoubleClick() {
return orig.isDoubleClick();
}
@Override
public boolean isMetaKey() {
return orig.isMetaKey();
}
@Override
public boolean isShiftKey() {
return orig.isShiftKey();
}
}
/**
* A better typed version of ItemClickEvent.
*
* @param <T> the type of entities listed in the table
*/
public interface RowClickListener<T> extends Serializable {
public void rowClick(RowClickEvent<T> event);
}
public void addRowClickListener(RowClickListener<T> listener) {
ensureTypedItemClickPiggybackListener();
addListener(RowClickEvent.class, listener,
RowClickEvent.TYPED_ITEM_CLICK_METHOD);
}
public void removeRowClickListener(RowClickListener<T> listener) {
removeListener(RowClickEvent.class, listener,
RowClickEvent.TYPED_ITEM_CLICK_METHOD);
}
/**
* Clears caches in case the Table is backed by a LazyList implementation.
* Also resets "pageBuffer" used by table. If you know you have changes in
* the listing, you can call this method to ensure the UI gets updated.
*
* @deprecated use refreshRows instead
*/
@Deprecated
public void resetLazyList() {
refreshRows();
}
/**
* Clears caches in case the Table is backed by a LazyList implementation.
* Also resets "pageBuffer" used by table. If you know you have changes in
* the listing, you can call this method to ensure the UI gets updated.
*/
public void refreshRows() {
if (bic != null && bic.getItemIds() instanceof LazyList) {
((LazyList) bic.getItemIds()).reset();
}
resetPageBuffer();
}
/**
* Sets the row of given entity as selected. This is practically a better
* typed version for select(Object) and setValue(Object) methods.
*
* @param entity the entity whose row should be selected
* @return the MTable instance
*/
public MTable<T> setSelected(T entity) {
setValue(entity);
return this;
}
public MTable<T> withValueChangeListener(MValueChangeListener<T> listener){
addMValueChangeListener(listener);
return this;
}
public MTable<T> withRowClickListener(RowClickListener<T> listener){
addRowClickListener(listener);
return this;
}
public MTable<T> withSortListener(SortListener listener){
addSortListener(listener);
return this;
}
}