/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) 1999-2006 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.core.gui.components.table;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.olat.core.gui.ShortName;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.choice.Choice;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.link.LinkFactory;
import org.olat.core.gui.components.velocity.VelocityContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.ControllerEventListener;
import org.olat.core.gui.control.DefaultController;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.ajax.autocompletion.AutoCompleterController;
import org.olat.core.gui.control.generic.ajax.autocompletion.EntriesChosenEvent;
import org.olat.core.gui.control.generic.ajax.autocompletion.ListProvider;
import org.olat.core.gui.control.generic.ajax.autocompletion.ListReceiver;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.media.ExcelMediaResource;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.translator.PackageTranslator;
import org.olat.core.gui.translator.Translator;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
/**
* <!--**************-->
* <h3>Responsability:</h3>
* This controller wraps a table component and offers additional features like
* column selection. Two constructors are supported: regular table and table
* with a table filter. Use the TableGuiConfiguration object to configure the
* various rendering options.
* <p>
* <!--**************-->
* <h3>Events fired:</h3>
* <ul>
* <li><i>{@link #EVENT_FILTER_SELECTED}</i>:<br>
* After succesfully activation of the selected filter. </li>
* <li><i>{@link #EVENT_NOFILTER_SELECTED}</i>:<br>
* After deactivation of the last filter.</li>
* <li><i>{@link org.olat.core.gui.components.table.Table Table component events}</i>:<br>
* Forwards all events from the table component.</li>
* </ul>
* <p>
* <!--**************-->
* <h3>Workflow:</h3>
* <ul>
* <li><i>Change table columns:</i><br>
* Show a modal dialog for choosing visible/invisible table columns.<br>
* Save table columns settings in the users preferences.</li>
* <li><i>Download table content:</i><br>
* Formats table content as CSV.<br>
* Creates an asynchronously delivered
* {@link org.olat.core.gui.media.ExcelMediaResource excel media resource}.</li>
* <li><i>Apply table filter:</i><br>
* Activates a defined table filter.<br>
* Deactivates a table filter. </li>
* </ul>
* <p>
* <!--**************-->
* <h3>Special translators:</h3>
* Uses a translator provided in the constructor as <i>fallback</i>.
* <p>
* <!--**************-->
* <h3>Hints:</h3>
* Opens a modal dialog for choosing which columns to hide or show.
* <p>
*
* @author Felix Jost, Florian Gnägi
*/
public class TableController extends DefaultController implements ControllerEventListener {
private OLog log = Tracing.createLoggerFor(this.getClass());
private static final String PACKAGE = Util.getPackageName(TableController.class);
private static final String VELOCITY_ROOT = Util.getPackageVelocityRoot(TableController.class);
private static final String CMD_FILTER = "cmd.filter.";
private static final String CMD_FILTER_NOFILTER = "cmd.filter.nofilter";
/** Event is fired when the 'apply no filter' is selected * */
public static final Event EVENT_NOFILTER_SELECTED = new Event("nofilter.selected");
/**
* Event is fired when a specific filter is selected. Use getActiveFilter to
* retrieve the selected filter
*/
public static final Event EVENT_FILTER_SELECTED = new Event("filter.selected");
private VelocityContainer contentVc;
private Table table;
private Translator trans;
private Choice colsChoice;
private TablePrefs prefs;
private TableGuiConfiguration tableConfig;
private List filters;
private ShortName activeFilter;
private boolean tablePrefsInitialized = false;
private CloseableModalController cmc;
private AutoCompleterController tableSearchController;
private Link resetLink;
/**
* Constructor for the table controller using the table filter.
*
* @param tableConfig The table gui configuration determines the tables
* behaviour, may be <code>null</code> to use default table config.
* @param ureq The user request
* @param wControl The window control
* @param filters A list of filter objects ({@link ShortName})
* @param activeFilter The initially activated filter object
* @param filterTitle The translated title of the filter
* @param noFilterOption The translated key for the no-filter filter or
* <code>null</code> if not used
* @param tableTrans The translator that is used to translate the table
* @param tableEventListener The listener for the table and table controller
* events
*/
public TableController(TableGuiConfiguration tableConfig, UserRequest ureq, WindowControl wControl, List filters, ShortName activeFilter,
String filterTitle, String noFilterOption, Translator tableTrans, ControllerEventListener tableEventListener) {
// init using regular constructor
this(tableConfig, ureq, wControl, tableTrans, tableEventListener);
// push filter to velocity page
setFilters(filters, activeFilter);
this.contentVc.contextPut("filterTitle", filterTitle);
if (noFilterOption != null) {
this.contentVc.contextPut("noFilterOption", noFilterOption);
this.contentVc.contextPut("useNoFilterOption", Boolean.TRUE);
} else {
this.contentVc.contextPut("useNoFilterOption", Boolean.FALSE);
}
}
/**
* Constructor for the table controller
*
* @param tableConfig The table gui configuration determines the tables
* behaviour, may be <code>null</code> to use default table config.
* @param ureq The user request
* @param wControl The window control
* @param tableTrans The translator that is used to translate the table
* @param tableEventListener The listener for the table and table controller
* events
*/
public TableController(TableGuiConfiguration tableConfig, UserRequest ureq, WindowControl wControl, Translator tableTrans,
ControllerEventListener tableEventListener) {
super(wControl);
if (tableConfig == null) tableConfig = new TableGuiConfiguration();
this.tableConfig = tableConfig;
if (tableEventListener != null) addControllerListener(tableEventListener);
// FIXME fj: the other way round: allow table translator to override package
// translator!
this.trans = new PackageTranslator(PACKAGE, ureq.getLocale(), tableTrans);
this.table = new Table("table", trans);
this.table.addListener(this);
// propagate table specific configuration to table,
// rest of configuration is handled by this controller
this.table.setColumnMovingOffered(tableConfig.isColumnMovingOffered());
this.table.setDisplayTableHeader(tableConfig.isDisplayTableHeader());
this.table.setSelectedRowUnselectable(tableConfig.isSelectedRowUnselectable());
this.table.setSortingEnabled(tableConfig.isSortingEnabled());
this.table.setPageingEnabled(tableConfig.isPageingEnabled());
this.table.setResultsPerPage(tableConfig.getResultsPerPage());
this.table.setMultiSelect(tableConfig.isMultiSelect());
this.table.enableShowAllLink(tableConfig.isShowAllLinkEnabled());
// table is embedded in a velocity page that renders the surrounding layout
contentVc = new VelocityContainer("tablevc", VELOCITY_ROOT + "/tablelayout.html", trans, this);
contentVc.put("table", table);
// fetch prefs (which were loaded at login time
String preferencesKey = tableConfig.getPreferencesKey();
if (tableConfig.isPreferencesOffered() && preferencesKey != null) {
this.prefs = (TablePrefs) ureq.getUserSession().getGuiPreferences().get(TableController.class, preferencesKey);
}
// empty table message
String tableEmptyMessage = tableConfig.getTableEmptyMessage();
if (tableEmptyMessage == null) tableEmptyMessage = trans.translate("default.tableEmptyMessage");
contentVc.contextPut("tableEmptyMessage", tableEmptyMessage);
contentVc.contextPut("tableConfig", this.tableConfig);
tableSearchController = createTableSearchController(ureq, wControl);
contentVc.put("tableSearch", tableSearchController.getInitialComponent());
contentVc.contextPut("hasTableSearch", Boolean.FALSE);
setInitialComponent(contentVc);
}
public TableController(TableGuiConfiguration tableConfig, UserRequest ureq, WindowControl wControl, Translator tableTrans,
ControllerEventListener tableEventListener, boolean enableTableSearch ) {
this(tableConfig, ureq, wControl, tableTrans, tableEventListener);
if (enableTableSearch) {
contentVc.contextPut("hasTableSearch", Boolean.TRUE);
} else {
contentVc.contextPut("hasTableSearch", Boolean.FALSE);
}
}
private AutoCompleterController createTableSearchController(UserRequest ureq, WindowControl wControl) {
ListProvider genericProvider = new ListProvider() {
public void getResult(String searchValue, ListReceiver receiver) {
Set<String> searchEntries = new TreeSet();
// loop over hole data-model
for (int rowIndex=0; rowIndex < table.getUnfilteredTableDataModel().getRowCount(); rowIndex++) {
for (int colIndex=0; colIndex < table.getUnfilteredTableDataModel().getColumnCount(); colIndex++) {
Object obj = table.getUnfilteredTableDataModel().getValueAt(rowIndex, colIndex);
if (obj instanceof String) {
String valueString = (String)obj;
if (valueString.toLowerCase().indexOf(searchValue.toLowerCase()) != -1) {
if (searchEntries.add(valueString) ) {
// Add to receiver list same entries only once
if (searchEntries.size() == 1) {
// before first entry, add searchValue. But add only when one search match
receiver.addEntry( searchValue, null );
}
receiver.addEntry(valueString, null);
}
}
}
}
}
}};
AutoCompleterController tableSearchController = new AutoCompleterController(ureq, wControl, genericProvider, "-", false, trans.translate("table.filter.label"));
tableSearchController.addControllerListener(this);
return tableSearchController;
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.components.Component, org.olat.core.gui.control.Event)
*/
public void event(UserRequest ureq, Component source, Event event) {
if (source == table) {
if (event.getCommand().equalsIgnoreCase(Table.COMMAND_SHOW_PAGES)) {
// nothing to do
} else if (event.getCommand().equalsIgnoreCase(Table.COMMAND_PAGEACTION_SHOWALL)) {
// nothing to do
} else {
// forward to table controller listener
fireEvent(ureq, event);
}
} else if (source == contentVc) {
// links of this vc container coming in
String cmd = event.getCommand();
if (cmd.equals("cmd.changecols") && tableConfig.getPreferencesKey() != null) {
// get the list of columns and if they are visible or not
colsChoice = new Choice("colchoice", trans);
colsChoice.setTableDataModel(table.createChoiceTableDataModel());
colsChoice.addListener(this);
colsChoice.setCancelKey("cancel");
colsChoice.setSubmitKey("save");
cmc = new CloseableModalController(getWindowControl(), "close", colsChoice,true,trans.translate("title.changecols"));
cmc.addControllerListener(this);
cmc.activate();
} else if (cmd.equals("cmd.download") && tableConfig.isDownloadOffered()) {
int cdcnt = table.getColumnCount();
int rcnt = table.getRowCount();
StringBuilder sb = new StringBuilder();
// header
for (int c = 0; c < cdcnt; c++) {
ColumnDescriptor cd = table.getColumnDescriptor(c);
if (cd instanceof StaticColumnDescriptor) {
// ignore static column descriptors - of no value in excel download!
continue;
}
String headerKey = cd.getHeaderKey();
String headerVal = cd.translateHeaderKey() ? trans.translate(headerKey) : headerKey;
sb.append('\t').append(headerVal);
}
sb.append('\n');
// data
for (int r = 0; r < rcnt; r++) {
for (int c = 0; c < cdcnt; c++) {
ColumnDescriptor cd = table.getColumnDescriptor(c);
if (cd instanceof StaticColumnDescriptor) {
// ignore static column descriptors - of no value in excel download!
continue;
}
StringOutput so = new StringOutput();
cd.renderValue(so, r, null);
String cellValue = so.toString();
cellValue = StringHelper.stripLineBreaks(cellValue);
sb.append('\t').append(cellValue);
}
sb.append('\n');
}
String res = sb.toString();
// FIXME:FG:6.2:OLAT-3736 a use a charsetprovider interface which can deliver the selected charset of a user
//String charset = UserManager.getInstance().getUserCharset(ureq.getIdentity());
// As a temporary workaround use the default charset
String charset = WebappHelper.getDefaultCharset();
ExcelMediaResource emr = new ExcelMediaResource(res, charset);
ureq.getDispatchResult().setResultingMediaResource(emr);
} else if (cmd.equals(CMD_FILTER_NOFILTER)) {
// update new filter value
setActiveFilter(null);
fireEvent(ureq, EVENT_NOFILTER_SELECTED);
} else if (cmd.indexOf(CMD_FILTER) == 0) {
String areafilter = cmd.substring(CMD_FILTER.length());
int filterPosition = Integer.parseInt(areafilter);
// security check
if (filters.size() < (filterPosition + 1)) throw new AssertException("Filter size was ::" + filters.size()
+ " but requested filter was ::" + filterPosition);
// update new filter value
setActiveFilter((ShortName) this.filters.get(filterPosition));
fireEvent(ureq, EVENT_FILTER_SELECTED);
}
} else if (source == colsChoice) {
if (event == Choice.EVNT_VALIDATION_OK) {
List selRows = colsChoice.getSelectedRows();
if (selRows.size() == 0) {
getWindowControl().setError(trans.translate("error.selectatleastonecolumn"));
} else {
// check that there is at least one data column (because of sorting
// (technical) and information (usability))
if (table.isSortableColumnIn(selRows)) {
// ok
table.updateConfiguredRows(selRows);
// update user preferences, use the given preferences key
if (prefs == null) prefs = new TablePrefs();
prefs.setActiveColumnsRef(selRows);
ureq.getUserSession().getGuiPreferences().putAndSave(TableController.class, tableConfig.getPreferencesKey(), prefs);
// pop configuration dialog
cmc.deactivate();
} else {
getWindowControl().setError(trans.translate("error.atleastonedatacolumn"));
}
}
} else { // cancelled
cmc.deactivate();
}
} else if (source == resetLink) {
this.table.setSearchString(null);
this.modelChanged();
}
}
public void dispatchEvent(UserRequest ureq, Controller source, Event event) {
log.debug("dispatchEvent event=" + event + " source=" + source);
if (event instanceof EntriesChosenEvent) {
EntriesChosenEvent ece = (EntriesChosenEvent)event;
List filterList = ece.getEntries();
if (!filterList.isEmpty()) {
this.table.setSearchString((String)filterList.get(0));
this.modelChanged(false);
} else {
// reset filter search filter in modelChanged
this.modelChanged();
}
}
}
/**
* @return The currently active filter object or <code>null</code> if no
* filter is applied
*/
public ShortName getActiveFilter() {
return this.activeFilter;
}
/**
* @param activeFilter The currently applied filter or <code>null</code> if
* no filter is applied
*/
public void setActiveFilter(ShortName activeFilter) {
this.activeFilter = activeFilter;
if (this.activeFilter == null) {
this.contentVc.contextPut("selectedFilterValue", CMD_FILTER_NOFILTER);
} else {
this.contentVc.contextPut("selectedFilterValue", this.activeFilter.getShortName());
}
}
/**
* Sets the list of filters and the currently active filter
*
* @param filters List of TableFilter
* @param activeFilter active TableFilter
*/
public void setFilters(List filters, ShortName activeFilter) {
this.filters = filters;
this.contentVc.contextPut("hasFilters", filters == null ? Boolean.FALSE : Boolean.TRUE);
this.contentVc.contextPut("filters", filters);
setActiveFilter(activeFilter);
}
public void modelChanged() {
modelChanged(true);
}
/**
* Notifies the controller about a changed table data model. This will check
* if the table data model has any values and show a message instead of the
* table when the model has no rows.
*/
public void modelChanged(boolean resetSearchString) {
if (resetSearchString) {
table.setSearchString(null);
}
table.modelChanged();
TableDataModel tableModel = table.getTableDataModel();
if (tableModel != null) {
this.contentVc.contextPut("tableEmpty", tableModel.getRowCount() == 0 ? Boolean.TRUE : Boolean.FALSE);
this.contentVc.contextPut("numberOfElements", String.valueOf(table.getUnfilteredRowCount()));
if (table.isTableFiltered()) {
this.contentVc.contextPut("numberFilteredElements", String.valueOf(table.getRowCount()));
this.contentVc.contextPut("isFiltered", Boolean.TRUE);
this.contentVc.contextPut("filter", table.getSearchString());
resetLink = LinkFactory.createCustomLink("link.numberOfElements", "link.numberOfElements", String.valueOf(table.getUnfilteredRowCount()), Link.NONTRANSLATED, contentVc, this);
} else {
this.contentVc.contextPut("isFiltered", Boolean.FALSE);
}
}
// else do nothing. The table might have no table data model during
// constructing time of
// this controller.
}
/**
* Sets the tableDataModel. IMPORTANT: Once a tableDataModel is set, it is
* assumed to remain constant in its data & row & colcount. Otherwise a
* modelChanged has to be called
*
* @param tableDataModel The tableDataModel to set
*/
public void setTableDataModel(TableDataModel tableDataModel) {
table.setTableDataModel(tableDataModel);
if (!tablePrefsInitialized) { // first time
if (prefs != null) {
try {
List acolRefs = prefs.getActiveColumnsRef();
table.updateConfiguredRows(acolRefs);
} catch(IndexOutOfBoundsException ex) {
// GUI prefs match not to table data model => reset prefs
prefs = null;
}
}
tablePrefsInitialized = true;
}
modelChanged();
}
/**
* Add a table column descriptor
*
* @param visible true: is visible; false: is not visible
* @param cd column descriptor
*/
public void addColumnDescriptor(boolean visible, ColumnDescriptor cd) {
table.addColumnDescriptor(cd, -1, visible);
}
/**
* Add a visible table column descriptor
*
* @param cd column descriptor
*/
public void addColumnDescriptor(ColumnDescriptor cd) {
table.addColumnDescriptor(cd, -1, true);
}
/**
* Get the table column descriptor.
* @param row
* @return ColumnDescriptor
*/
public ColumnDescriptor getColumnDescriptor(int row) {
return table.getColumnDescriptor(row);
}
/**
* Get the current table data model from the table
*
* @return TableDataModel
*/
public TableDataModel getTableDataModel() {
return table.getTableDataModel();
}
/**
* Sorts the selected table row indexes according with the table Comparator,
* and then retrieves the rows from the input defaultTableDataModel.
* It is assumed that the defaultTableDataModel IS THE MODEL for the table.
* @param objectMarkers
* @return the List with the sorted selected objects in this table.
*/
public List getSelectedSortedObjects(BitSet objectMarkers, DefaultTableDataModel defaultTableDataModel) {
List results = new ArrayList();
List<Integer> sortedIndexes = new ArrayList<Integer>();
if(objectMarkers.isEmpty()) {
sortedIndexes.clear();
}
for (int i = objectMarkers.nextSetBit(0); i >= 0; i = objectMarkers.nextSetBit(i + 1)) {
sortedIndexes.add(i);
}
Collections.sort(sortedIndexes, table);
Iterator<Integer> indexesIterator = sortedIndexes.iterator();
while (indexesIterator.hasNext()) {
results.add(defaultTableDataModel.getObject(indexesIterator.next()));
}
return results;
}
/**
* Sets the selectedRowId to a specific row id. Make sure that this is valid,
* the table does not check for out of bound exception.
*
* @param selectedRowId The selectedRowId to set
*/
public void setSelectedRowId(int selectedRowId) {
table.setSelectedRowId(selectedRowId);
}
/**
* Sets the sortColumn to a specific colun id. Check if the column can be accessed
* and if it is sortable.
*
* @param sortColumn The sortColumn to set
* @param isSortAscending true: sorting is ascending
*/
public void setSortColumn(int sortColumn, boolean isSortAscending) {
if ((table.getColumnCount() > sortColumn)
&& table.getColumnDescriptor(sortColumn).isSortingAllowed()) {
table.setSortColumn(sortColumn, isSortAscending);
table.resort();
}
}
/**
* Sets wether user is able to select multiple rows via checkboxes.
*
* @param isMultiSelect
*/
public void setMultiSelect(boolean isMultiSelect) {
table.setMultiSelect(isMultiSelect);
}
public void setMultiSelectSelectedAt(int row, boolean selected) {
table.setMultiSelectSelectedAt(row, selected);
}
public void setMultiSelectReadonlyAt(int row, boolean readonly) {
table.setMultiSelectReadonlyAt(row, readonly);
}
/**
* Add a multiselect action.
*
* @param actionKeyi18n
* @param actionIdentifier
*/
public void addMultiSelectAction(String actionKeyi18n, String actionIdentifier) {
table.addMultiSelectAction(actionKeyi18n, actionIdentifier);
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose(boolean)
*/
protected void doDispose() {
if (cmc != null) {
cmc.dispose();
cmc = null;
}
}
}