/** * 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) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <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 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.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.controller.BasicController; import org.olat.core.gui.control.generic.ajax.autocompletion.AutoCompleterController; import org.olat.core.gui.control.generic.ajax.autocompletion.EmptyChosenEvent; 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.closablewrapper.CloseableCalloutWindowController; import org.olat.core.gui.media.MediaResource; 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.Util; /** * <!--**************--> * <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 BasicController { private static final String VC_VAR_USE_NO_FILTER_OPTION = "useNoFilterOption"; private static final String COMPONENT_TABLE_NAME = "table"; private static final String VC_VAR_SELECTED_FILTER_VALUE = "selectedFilterValue"; private static final String LINK_NUMBER_OF_ELEMENTS = "link.numberOfElements"; private static final String VC_VAR_IS_FILTERED = "isFiltered"; private static final String VC_VAR_HAS_TABLE_SEARCH = "hasTableSearch"; private OLog log = Tracing.createLoggerFor(this.getClass()); 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 Choice colsChoice; private TablePrefs prefs; private TableGuiConfiguration tableConfig; private List<ShortName> filters; private String filterTitle; private ShortName activeFilter; private boolean tablePrefsInitialized = false; private CloseableCalloutWindowController cmc; private AutoCompleterController tableSearchController; private TableSort tableSort; private Link resetLink; private Link preferenceLink; private Link downloadLink; /** * Constructor for the table controller using the table filter. * * @param tableConfig The table GUI configuration determines the tables * behavior, may be <code>null</code> to use default table configuration. * @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 */ public TableController(final TableGuiConfiguration tableConfig, final UserRequest ureq, final WindowControl wControl, final List<ShortName> filters, final ShortName activeFilter, final String filterTitle, final String noFilterOption, final Translator tableTrans) { // init using regular constructor this(tableConfig, ureq, wControl, filters, activeFilter, filterTitle, noFilterOption, false, tableTrans); } /** * Constructor for the table controller using the table filter. * * @param tableConfig The table GUI configuration determines the tables * behavior, may be <code>null</code> to use default table configuration. * @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 enableTableSearch Enable the auto completter for search within the table * @param tableTrans The translator that is used to translate the table */ public TableController(final TableGuiConfiguration tableConfig, final UserRequest ureq, final WindowControl wControl, final List<ShortName> filters, final ShortName activeFilter, final String filterTitle, final String noFilterOption, final boolean enableTableSearch, final Translator tableTrans) { // init using regular constructor this(tableConfig, ureq, wControl, tableTrans); // push filter to velocity page this.filterTitle = filterTitle; setFilters(filters, activeFilter); if (noFilterOption != null) { contentVc.contextPut("noFilterOption", noFilterOption); contentVc.contextPut(VC_VAR_USE_NO_FILTER_OPTION, Boolean.TRUE); } else { contentVc.contextPut(VC_VAR_USE_NO_FILTER_OPTION, Boolean.FALSE); } if (enableTableSearch) { tableSearchController = createTableSearchController(ureq, wControl); contentVc.put("tableSearch", tableSearchController.getInitialComponent()); contentVc.contextPut(VC_VAR_HAS_TABLE_SEARCH, Boolean.TRUE); } else { contentVc.contextPut(VC_VAR_HAS_TABLE_SEARCH, 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 */ public TableController(final TableGuiConfiguration tableConfigP, final UserRequest ureq, final WindowControl wControl, final Translator tableTrans) { super(ureq, wControl); if (tableConfigP == null){ tableConfig = new TableGuiConfiguration(); }else{ tableConfig = tableConfigP; } if (tableTrans != null) { setTranslator(Util.createPackageTranslator(TableController.class, ureq.getLocale(), tableTrans)); } table = new Table(COMPONENT_TABLE_NAME, getTranslator()); table.addListener(this); // propagate table specific configuration to table, // rest of configuration is handled by this controller table.setDisplayTableHeader(tableConfig.isDisplayTableHeader()); table.setSelectedRowUnselectable(tableConfig.isSelectedRowUnselectable()); table.setSortingEnabled(tableConfig.isSortingEnabled()); table.setPageingEnabled(tableConfig.isPageingEnabled()); table.setResultsPerPage(tableConfig.getResultsPerPage()); table.setMultiSelect(tableConfig.isMultiSelect()); table.setEnableShowAllLink(tableConfig.isShowAllLinkEnabled()); table.setDisplayTableGrid(tableConfig.isDisplayTableGrid()); table.setSuppressDirtyFormWarning(tableConfig.isSuppressDirtyFormWarning()); // table is embedded in a velocity page that renders the surrounding layout contentVc = createVelocityContainer("tablelayout"); contentVc.put(COMPONENT_TABLE_NAME, table); // fetch prefs (which were loaded at login time String preferencesKey = tableConfig.getPreferencesKey(); if (tableConfig.isPreferencesOffered() && preferencesKey != null) { prefs = (TablePrefs) ureq.getUserSession().getGuiPreferences().get(TableController.class, preferencesKey); } // empty table message String tableEmptyMessage = tableConfig.getTableEmptyMessage(); if (tableEmptyMessage == null){ tableEmptyMessage = translate("default.tableEmptyMessage"); } contentVc.contextPut("tableEmptyMessage", tableEmptyMessage); contentVc.contextPut("tableConfig", tableConfig); contentVc.contextPut(VC_VAR_HAS_TABLE_SEARCH, Boolean.FALSE); //sorters contentVc.contextPut("hasSorters", new Boolean(tableConfig.isSortingEnabled())); tableSort = new TableSort("tableSort", table); contentVc.put("tableSort", tableSort); //preference + download links preferenceLink = LinkFactory.createCustomLink("prefLink", "cmd.changecols", "", Link.BUTTON | Link.NONTRANSLATED, contentVc, this); preferenceLink.setIconLeftCSS("o_icon o_icon_customize"); preferenceLink.setTooltip(translate("command.changecols")); downloadLink = LinkFactory.createCustomLink("downloadLink", "cmd.download", "", Link.BUTTON | Link.NONTRANSLATED, contentVc, this); downloadLink.setTooltip(translate("table.export.title")); downloadLink.setIconLeftCSS("o_icon o_icon_download"); putInitialPanel(contentVc); } public TableController(final TableGuiConfiguration tableConfig, final UserRequest ureq, final WindowControl wControl, final Translator tableTrans, final boolean enableTableSearch ) { this(tableConfig, ureq, wControl, tableTrans); if (enableTableSearch) { tableSearchController = createTableSearchController(ureq, wControl); contentVc.put("tableSearch", tableSearchController.getInitialComponent()); contentVc.contextPut(VC_VAR_HAS_TABLE_SEARCH, Boolean.TRUE); } else { contentVc.contextPut(VC_VAR_HAS_TABLE_SEARCH, Boolean.FALSE); } } private AutoCompleterController createTableSearchController(final UserRequest ureq, final WindowControl wControl) { ListProvider genericProvider = new TableListProvider(table); removeAsListenerAndDispose(tableSearchController); tableSearchController = new AutoCompleterController(ureq, wControl, genericProvider, null, false, 60, 3, translate("table.filter.label")); tableSearchController.setEmptyAsReset(true); listenTo(tableSearchController); 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(final UserRequest ureq, final Component source, final Event event) { if (source == table) { String cmd = event.getCommand(); if(cmd.equalsIgnoreCase(Table.COMMAND_SORTBYCOLUMN)) { tableSort.setDirty(true); } else if (!cmd.equalsIgnoreCase(Table.COMMAND_SHOW_PAGES) && !cmd.equalsIgnoreCase(Table.COMMAND_PAGEACTION_SHOWALL)) { // forward to table controller listener fireEvent(ureq, event); } } else if (source == contentVc) { handleCommandsOfTableVcContainer(ureq, event); } else if(source == preferenceLink && tableConfig.getPreferencesKey() != null){ colsChoice = getColumnListAndTheirVisibility(); removeAsListenerAndDispose(cmc); cmc = new CloseableCalloutWindowController(ureq, getWindowControl(), colsChoice, preferenceLink , translate("title.changecols"), true, ""); listenTo(cmc); cmc.activate(); } else if(source == downloadLink && tableConfig.isDownloadOffered() ){ TableExporter tableExporter = tableConfig.getDownloadOffered(); MediaResource mr = tableExporter.export(table); ureq.getDispatchResult().setResultingMediaResource(mr); } else if (source == colsChoice) { if (event == Choice.EVNT_VALIDATION_OK) { //sideeffect on table and prefs applyAndcheckChangedColumnsChoice(ureq, colsChoice.getSelectedRows()); } else if (event == Choice.EVNT_FORM_RESETED) { //sideeffect on table and prefs List<Integer> visibleCols = table.getDefaultVisibleColumnsToResetColumnsChoice(); applyAndcheckChangedColumnsChoice(ureq, visibleCols); } else { // cancelled cmc.deactivate(); } } else if (source == resetLink) { table.setSearchString(null); modelChanged(); } } private void handleCommandsOfTableVcContainer(final UserRequest ureq, final Event event) { // links of this vc container coming in String cmd = event.getCommand(); 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(filters.get(filterPosition)); fireEvent(ureq, EVENT_FILTER_SELECTED); } } private void applyAndcheckChangedColumnsChoice(final UserRequest ureq, List<Integer> selRows) { if (selRows.size() == 0) { showError("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 { showError("error.atleastonedatacolumn"); } } } private Choice getColumnListAndTheirVisibility() { Choice choice = new Choice("colchoice", getTranslator()); choice.setModel(table.createChoiceModel()); choice.addListener(this); choice.setEscapeHtml(false); choice.setCancelKey("cancel"); choice.setResetKey("reset"); choice.setSubmitKey("save"); choice.setElementCssClass("o_table_config"); return choice; } @Override public void event(final UserRequest ureq, final Controller source, final Event event) { log.debug("dispatchEvent event=" + event + " source=" + source); if (event instanceof EntriesChosenEvent) { EntriesChosenEvent ece = (EntriesChosenEvent)event; List<String> filterList = ece.getEntries(); if (!filterList.isEmpty()) { table.setSearchString(filterList.get(0)); modelChanged(false); } else { // reset filter search filter in modelChanged modelChanged(); } } else if(event instanceof EmptyChosenEvent) { modelChanged(true); } } public int getRowCount() { return table.getRowCount(); } public int getSortedRow(int originalRow) { return table.getSortedRow(originalRow); } /** * Return the object at the visible index (sorted or not, searched or not) * @param index * @return */ public Object getSortedObjectAt(int sortedRow) { int row = table.getSortedRow(sortedRow); return getTableDataModel().getObject(row); } /** * Return the visible index of the object (sorted or not, searched or not) * @param obj * @return */ public int getIndexOfSortedObject(Object obj) { int index = -1; for(int i=getTableDataModel().getRowCount(); i-->0; ) { if(obj.equals(getTableDataModel().getObject(i))) { index = i; break; } } for(int i=0; i<getRowCount(); i++) { int currentPos = getSortedRow(i); if(currentPos == index) { return i; } } return -1; } /** * @return The currently active filter object or <code>null</code> if no * filter is applied */ public ShortName getActiveFilter() { return activeFilter; } /** * @param activeFilter The currently applied filter or <code>null</code> if * no filter is applied */ public void setActiveFilter(final ShortName activeFilter) { this.activeFilter = activeFilter; if (activeFilter == null) { contentVc.contextPut(VC_VAR_SELECTED_FILTER_VALUE, CMD_FILTER_NOFILTER); } else { contentVc.contextPut(VC_VAR_SELECTED_FILTER_VALUE, activeFilter); } } /** * Sets the list of filters and the currently active filter * * @param filters List of TableFilter * @param activeFilter active TableFilter */ public void setFilters(final List<ShortName> filters, final ShortName activeFilter) { this.filters = filters; contentVc.contextPut("hasFilters", filters == null ? Boolean.FALSE : Boolean.TRUE); contentVc.contextPut("filters", filters); contentVc.contextPut("filterTitle", filterTitle == null ? "" : filterTitle); 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(final boolean resetSearchString) { if (resetSearchString) { table.setSearchString(null); } table.modelChanged(); TableDataModel<?> tableModel = table.getTableDataModel(); if (tableModel != null) { contentVc.contextPut("tableEmpty", tableModel.getRowCount() == 0 ? Boolean.TRUE : Boolean.FALSE); contentVc.contextPut("numberOfElements", String.valueOf(table.getUnfilteredRowCount())); contentVc.contextPut("rowCounts", String.valueOf(table.getRowCount())); if (table.isTableFiltered()) { contentVc.contextPut("numberFilteredElements", String.valueOf(table.getRowCount())); contentVc.contextPut(VC_VAR_IS_FILTERED, Boolean.TRUE); contentVc.contextPut("filter", table.getSearchString()); resetLink = LinkFactory.createCustomLink(LINK_NUMBER_OF_ELEMENTS, LINK_NUMBER_OF_ELEMENTS, String.valueOf(table.getUnfilteredRowCount()), Link.NONTRANSLATED, contentVc, this); } else { contentVc.contextPut(VC_VAR_IS_FILTERED, 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(final TableDataModel tableDataModel) { table.setTableDataModel(tableDataModel); if (!tablePrefsInitialized) { // first time if (prefs != null) { try { List<Integer> 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(final boolean visible, final ColumnDescriptor cd) { table.addColumnDescriptor(cd, -1, visible); } /** * Add a visible table column descriptor * * @param cd column descriptor */ public void addColumnDescriptor(final ColumnDescriptor cd) { table.addColumnDescriptor(cd, -1, true); } /** * Get the table column descriptor. * @param row * @return ColumnDescriptor */ public ColumnDescriptor getColumnDescriptor(final 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(final BitSet objectMarkers, final 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.getComparator()); Iterator<Integer> indexesIterator = sortedIndexes.iterator(); while (indexesIterator.hasNext()) { results.add(defaultTableDataModel.getObject(indexesIterator.next())); } return results; } public List getObjects(final BitSet objectMarkers) { List results = new ArrayList(); for(int i=objectMarkers.nextSetBit(0); i >= 0; i=objectMarkers.nextSetBit(i+1)) { results.add(getTableDataModel().getObject(i)); } 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(final int selectedRowId) { table.setSelectedRowId(selectedRowId); } /** * Set the page viewed if pageing is enabled * @param pageNr */ public void setPage(Integer pageNr) { table.updatePageing(pageNr); } /** * Return the number of items per page if pageing is enable * @return */ public int getPageSize() { return table.getResultsPerPage(); } /** * 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(final int sortColumn, final boolean isSortAscending) { if ((table.getColumnCount() > sortColumn) && table.getColumnDescriptor(sortColumn).isSortingAllowed()) { table.setSortColumn(sortColumn, isSortAscending); table.resort(); tableSort.setDirty(true); } } public void setSortColumn(final ColumnDescriptor sortColumn, final boolean isSortAscending) { for(int i=table.getColumnCount(); i-->0; ) { if(sortColumn == table.getColumnDescriptor(i)) { table.setSortColumn(i, isSortAscending); table.resort(); tableSort.setDirty(true); } } } /** * Sets whether user is able to select multiple rows via checkboxes. * * @param isMultiSelect */ public void setMultiSelect(final boolean isMultiSelect) { table.setMultiSelect(isMultiSelect); } /** * Make the multi select as disabled box and remove the select all / deselect all * @param disable */ public void setMultiSelectAsDisabled(boolean disabled) { table.setMultiSelectAsDisabled(disabled); } public void setMultiSelectSelectedAt(final int row, final boolean selected) { table.setMultiSelectSelectedAt(row, selected); } public void setMultiSelectReadonlyAt(final int row, final boolean readonly) { table.setMultiSelectReadonlyAt(row, readonly); } /** * Add a multiselect action. * * @param actionKeyi18n The i18n key to translate * @param actionIdentifier */ public void addMultiSelectAction(final String actionKeyi18n, final String actionIdentifier) { table.addMultiSelectAction(null, actionKeyi18n, actionIdentifier); } /** * Add a multiselect action with an already translated label * @param label The label * @param actionIdentifier */ public void addLabeledMultiSelectAction(final String label, final String actionIdentifier) { table.addMultiSelectAction(label, null, actionIdentifier); } public int getTableSortCol() { return table.getSortColumn(); } public boolean getTableSortAsc() { return table.getSortAscending(); } /** * @see org.olat.core.gui.control.DefaultController#doDispose(boolean) */ protected void doDispose() { // } }