/*
* Copyright (c) 2016 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.wicket.markup.html.table;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.apache.wicket.Component;
import org.apache.wicket.IClusterable;
import org.apache.wicket.Resource;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.IBehavior;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.HeaderlessColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.html.panel.EmptyPanel;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.IItemReuseStrategy;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RefreshingView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.request.target.resource.ResourceStreamRequestTarget;
import org.apache.wicket.util.resource.IResourceStream;
import org.obiba.core.domain.IEntity;
import org.obiba.core.service.EntityQueryService;
import org.obiba.wicket.markup.html.ResourceGetter;
import org.obiba.wicket.markup.html.panel.ImageLabelLinkPanel;
import org.obiba.wicket.util.resource.CsvResourceStream;
@SuppressWarnings({ "UnusedDeclaration", "serial" })
public class EntityListTablePanel<T> extends Panel {
private static final long serialVersionUID = -5163898654558983434L;
public static final int DEFAULT_ROWS_PER_PAGE = 100;
private Boolean allSelected = false;
private final Map<Serializable, EntitySelection> selections = new HashMap<Serializable, EntitySelection>();
private IColumnProvider<T> columnProvider;
private IColumnProvider<T> csvColumnProvider;
private SortableDataProvider<T> dataProvider;
private AjaxDataTable<T> dataTable;
private ColumnSelectorPanel<T> selector;
private RowSelectionColumn<T> rowSelectionColumn;
private boolean displayRowSelectionColumn = false;
/**
* Constructor with the default title, rows per page and {@link SortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param columns
*/
public EntityListTablePanel(String id, EntityQueryService queryService, Class<T> type, IColumnProvider<T> columns) {
this(id, queryService, type, columns, new Model<String>(type.getSimpleName()), DEFAULT_ROWS_PER_PAGE);
}
/**
* Constructor with the default rows per page and {@link SortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param columns
* @param entityNameModel
*/
public EntityListTablePanel(String id, EntityQueryService queryService, Class<T> type, IColumnProvider<T> columns,
IModel<String> entityNameModel) {
this(id, queryService, type, columns, entityNameModel, DEFAULT_ROWS_PER_PAGE);
}
/**
* Constructor using a {@link SortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param columns
* @param entityNameModel
* @param rowsPerPage
*/
public EntityListTablePanel(String id, EntityQueryService queryService, Class<T> type, IColumnProvider<T> columns,
IModel<String> entityNameModel, int rowsPerPage) {
super(id);
internalConstruct(new SortableDataProviderEntityServiceImpl<T>(queryService, type), columns, entityNameModel,
rowsPerPage);
}
/**
* Constructor with the default title, rows per page and {@link FilteredSortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param template
* @param columns
*/
public EntityListTablePanel(String id, EntityQueryService queryService, T template, IColumnProvider<T> columns) {
this(id, queryService, template, columns, new Model<String>(template.getClass().getSimpleName()),
DEFAULT_ROWS_PER_PAGE);
}
/**
* Constructor with the default rows per page and {@link FilteredSortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param template
* @param columns
* @param entityNameModel
*/
public EntityListTablePanel(String id, EntityQueryService queryService, T template, IColumnProvider<T> columns,
IModel<String> entityNameModel) {
this(id, queryService, template, columns, entityNameModel, DEFAULT_ROWS_PER_PAGE);
}
/**
* Constructor using a {@link FilteredSortableDataProviderEntityServiceImpl}.
*
* @param id
* @param service
* @param template
* @param columns
* @param entityNameModel
* @param rowsPerPage
*/
public EntityListTablePanel(String id, EntityQueryService queryService, T template, IColumnProvider<T> columns,
IModel<String> entityNameModel, int rowsPerPage) {
super(id);
internalConstruct(new FilteredSortableDataProviderEntityServiceImpl<T>(queryService, template), columns,
entityNameModel, rowsPerPage);
}
/**
* Main constructor.
*
* @param id
* @param service
* @param dataProvider
* @param columns
* @param entityNameModel
* @param rowsPerPage
*/
public EntityListTablePanel(String id, SortableDataProvider<T> dataProvider, IColumnProvider<T> columns,
IModel<String> entityNameModel, int rowsPerPage) {
super(id);
internalConstruct(dataProvider, columns, entityNameModel, rowsPerPage);
}
private void internalConstruct(@SuppressWarnings("ParameterHidesMemberVariable") SortableDataProvider<T> dataProvider,
IColumnProvider<T> columns, IModel<String> entityNameModel, int rowsPerPage) {
setOutputMarkupId(true);
columnProvider = columns;
this.dataProvider = dataProvider;
List<IColumn<T>> displayableColumns = columnProvider.getDefaultColumns();
selector = new ColumnSelectorPanel<T>("commands", this);
add(dataTable = new AjaxDataTable<T>("list", entityNameModel, displayableColumns, dataProvider, selector,
rowsPerPage) {
private static final long serialVersionUID = 1L;
@Override
protected void onPageChanged() {
super.onPageChanged();
EntityListTablePanel.this.onPageChanged();
}
});
}
/**
* Sets the item reuse strategy. This strategy controls the creation of {@link Item}s.
*
* @param strategy item reuse strategy
* @return this for chaining
* @see RefreshingView#setItemReuseStrategy(IItemReuseStrategy)
* @see IItemReuseStrategy
*/
public final EntityListTablePanel<T> setItemReuseStrategy(IItemReuseStrategy strategy) {
dataTable.setItemReuseStrategy(strategy);
return this;
}
public IColumnProvider<T> getColumnProvider() {
return columnProvider;
}
@SuppressWarnings("unchecked")
void updateColumns(List<IColumn<T>> columns, @Nullable AjaxRequestTarget target) {
List<IColumn<T>> columnsWithSelector = null;
if(displayRowSelectionColumn) {
if(rowSelectionColumn == null) rowSelectionColumn = new RowSelectionColumn(this);
columnsWithSelector = new ArrayList<IColumn<T>>(columns);
columnsWithSelector.add(0, rowSelectionColumn);
} else {
columnsWithSelector = columns;
}
List<IBehavior> behaviours = dataTable.getBehaviors();
int currentPage = dataTable.getCurrentPage();
dataTable = new AjaxDataTable<T>("list", dataTable.getTitleModel(), columnsWithSelector, dataProvider, selector,
dataTable.getRowsPerPage()) {
@Override
protected void onPageChanged() {
super.onPageChanged();
EntityListTablePanel.this.onPageChanged();
}
};
if(behaviours != null) {
for(IBehavior behaviour : behaviours) {
dataTable.add(behaviour);
}
}
dataTable.setCurrentPage(currentPage);
replace(dataTable);
if(target != null) {
target.addComponent(this);
}
}
/**
* Default behaviour is to clean the selections made on previous page.
*/
protected void onPageChanged() {
clearSelections();
}
@Override
protected void onModelChanged() {
clearSelections();
super.onModelChanged();
}
/**
* Clear the selections and the select-all flag.
*/
public void clearSelections() {
getSelections().clear();
setAllSelected(false);
if(rowSelectionColumn != null) rowSelectionColumn.clearSelectionComponents();
}
/**
* Sets whether to display the column selection widget on the table's header row.
*
* @param selection true if the widget should be displayed.
*/
public void setAllowColumnSelection(boolean selection) {
selector.setVisible(selection);
}
public void setDisplayRowSelectionColumn(boolean displayRowSelectionColumn) {
this.displayRowSelectionColumn = displayRowSelectionColumn;
updateColumns(columnProvider.getDefaultColumns(), null);
}
/**
* Override this method to set your own command components in a panel.
*
* @param panelId
* @return
*/
public Panel getCommandPanel(String panelId) {
return new EmptyPanel(panelId);
}
public Panel getDefaultCommandPanel(String panelId) {
return getExportCommandPanel(panelId, ResourceGetter.getImage("document_out.gif"), new Model<String>("Export "));
}
/**
* Get the command that export table content to a csv file, with the given image.
*
* @param panelId
* @param image
* @return
*/
public Panel getExportCommandPanel(String panelId, Resource image, IModel<String> linkLabel) {
return new ImageLabelLinkPanel(panelId, image, linkLabel, ImageLabelLinkPanel.ImageLocation.left) {
private static final long serialVersionUID = 8347134007933182401L;
@Override
public void onClick() {
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(getReportStream()) {
@Override
public String getFileName() {
return getCsvFileName();
}
});
}
};
}
/**
* Returns the filename of the generated CSV file.
*
* @return
*/
public String getCsvFileName() {
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmm");
String name = formatter.format(new Date());
String header;
//Supporting the old way of defining the filename for a CSV.
Component entityNameComponent = get("entityName");
header = entityNameComponent == null
? dataTable.getTitleModel().toString()
: entityNameComponent.getDefaultModelObjectAsString();
name += "_" + header;
name = name.replace(' ', '_');
return name + "." + CsvResourceStream.FILE_SUFFIX;
}
/**
* Get the stream of list data in a csv file.
*
* @return
*/
public IResourceStream getReportStream() {
CsvResourceStream csv = getCsvResourceStream();
List<IColumn<T>> columns = csvColumnProvider == null
? columnProvider.getDefaultColumns()
: csvColumnProvider.getDefaultColumns();
for(String name : getColumnHeaderNames(columns)) {
csv.append(name);
}
csv.appendLine();
int size = dataProvider.size();
int from = 0;
int count = DEFAULT_ROWS_PER_PAGE > size ? size : DEFAULT_ROWS_PER_PAGE;
int idx = 0;
while(from < size) {
Iterator<? extends T> it = dataProvider.iterator(from, count);
while(it.hasNext()) {
IModel<T> model = dataProvider.model(it.next());
int pos = 0;
for(IColumn<T> col : columns) {
if(!(col instanceof HeaderlessColumn<?>)) {
Item<ICellPopulator<T>> cellItem = new Item<ICellPopulator<T>>("dummy", idx, null);
col.populateItem(cellItem, "dummy", model);
Component comp = cellItem.get("dummy");
String value = "";
if(comp != null) value = comp.getDefaultModelObjectAsString();
csv.append(value);
pos++;
}
}
idx++;
csv.appendLine();
}
from = idx;
count = from + DEFAULT_ROWS_PER_PAGE > size ? size - from : from + DEFAULT_ROWS_PER_PAGE;
}
csv.appendEnd();
return csv;
}
/**
* Gets the CsvResourceStream instance used to stream the CSV document. It is protected to allow overriding, for example to change the value separating character of the resulting CSV file.
*
* @return
*/
protected CsvResourceStream getCsvResourceStream() {
return new CsvResourceStream();
}
/**
* Get the (localized) column header names. Ignore headerless columns.
*
* @param columns
* @return
*/
protected List<String> getColumnHeaderNames(Iterable<IColumn<T>> columns) {
List<String> names = new ArrayList<String>();
for(IColumn<T> col : columns) {
if(col instanceof AbstractColumn<?> && !(col instanceof HeaderlessColumn<?>)) names.add(getColumnHeaderName(col));
}
return names;
}
/**
* Get the (localized) column header name from column display model.
*
* @param column
* @return
*/
String getColumnHeaderName(IColumn<T> column) {
String n = null;
if(column instanceof AbstractColumn<?> && !(column instanceof HeaderlessColumn<?>)) {
IModel<String> displayModel = ((AbstractColumn<T>) column).getDisplayModel();
if(displayModel != null) {
if(displayModel instanceof StringResourceModel) {
n = ((StringResourceModel) displayModel).getString();
} else if(displayModel.getObject() != null) {
n = displayModel.getObject();
}
}
}
return n;
}
public void setCellSpacing(int value) {
for(IBehavior behaviour : dataTable.getBehaviors()) {
if(behaviour instanceof SimpleAttributeModifier) {
SimpleAttributeModifier modifier = (SimpleAttributeModifier) behaviour;
if("cellspacing".equalsIgnoreCase(modifier.getAttribute())) {
dataTable.remove(behaviour);
break;
}
}
}
dataTable.add(new SimpleAttributeModifier("cellspacing", Integer.toString(value)));
}
public boolean isAllSelected() {
return allSelected == null ? false : allSelected;
}
public Boolean getAllSelected() {
return allSelected;
}
public void setAllSelected(Boolean allSelected) {
this.allSelected = allSelected;
}
/**
* Get a map with entity ids and their selection.
*
* @return
*/
public Map<Serializable, EntitySelection> getSelections() {
return selections;
}
/**
* Get the selection object for entity model. Create it if needed.
*
* @param model
* @return
*/
protected EntitySelection getSelection(IModel<T> model) {
if(model == null) return null;
EntitySelection selection;
IEntity entity = (IEntity) model.getObject();
if(selections.containsKey(entity.getId())) {
selection = selections.get(entity.getId());
} else {
selection = new EntitySelection(model);
selections.put(selection.getEntityId(), selection);
}
return selection;
}
/**
* Sets the checkbox of the row corresponding to the given model to "selected".
*
* @param model the targeted row's model
*/
public void setSelected(IModel<T> model) {
EntitySelection es = getSelection(model);
es.setSelected(true);
}
/**
* Sets the checkbox of the row corresponding to the given model to "deselected".
*
* @param model the targeted row's model
*/
public void setDeselected(IModel<T> model) {
EntitySelection es = getSelection(model);
es.setSelected(false);
}
/**
* "Deselects" all rows on the table. (Resets the list of selections to an empty list).
*
* @param model the targeted row's model
*/
public void emptySelections() {
setAllSelected(false);
selections.clear();
}
/**
* Get the ids that where selected in the selection column.
*
* @return
*/
public List<IEntity> getSelectedEntities() {
List<IEntity> selected = new ArrayList<IEntity>();
for(Map.Entry<Serializable, EntitySelection> entry : selections.entrySet()) {
EntitySelection selection = entry.getValue();
if(selection.getSelected()) {
selected.add(selection.getEntity());
}
}
return selected;
}
public void setPageSize(int rowsPerPage) {
dataTable.setRowsPerPage(rowsPerPage);
}
public Integer getCount() {
return dataProvider.size();
}
/**
* Stores the id of the entity and whether it is selected or not.
*/
protected class EntitySelection implements IClusterable {
private static final long serialVersionUID = 1L;
private final IModel<T> model;
private boolean selected = false;
public EntitySelection(IModel<T> model, boolean selected) {
this.model = model;
this.selected = selected;
}
public EntitySelection(IModel<T> model) {
this(model, allSelected);
}
public IModel<T> getModel() {
return model;
}
public Serializable getEntityId() {
return getEntity().getId();
}
public IEntity getEntity() {
return (IEntity) model.getObject();
}
public boolean getSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
}
public IColumnProvider<T> getCsvColumnProvider() {
return csvColumnProvider;
}
public void setCsvColumnProvider(IColumnProvider<T> csvColumnProvider) {
this.csvColumnProvider = csvColumnProvider;
}
protected AjaxDataTable<T> getDataTable() {
return dataTable;
}
}