// $HeadURL$ // $Id$ // // Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College. // // Screensaver is an open-source project developed by the ICCB-L and NSRB labs // at Harvard Medical School. This software is distributed under the terms of // the GNU General Public License. package edu.harvard.med.screensaver.ui.arch.datatable.column; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import javax.faces.model.ListDataModel; import javax.faces.model.SelectItem; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; import org.apache.myfaces.custom.tree2.TreeModel; import org.apache.myfaces.custom.tree2.TreeModelBase; import org.apache.myfaces.custom.tree2.TreeNode; import org.apache.myfaces.custom.tree2.TreeNodeBase; import edu.harvard.med.screensaver.ScreensaverConstants; import edu.harvard.med.screensaver.db.Criterion; import edu.harvard.med.screensaver.db.SortDirection; import edu.harvard.med.screensaver.model.users.ScreensaverUserRole; import edu.harvard.med.screensaver.policy.CurrentScreensaverUser; import edu.harvard.med.screensaver.ui.arch.datatable.ColumnVisibilityChangedEvent; import edu.harvard.med.screensaver.ui.arch.datatable.SortChangedEvent; import edu.harvard.med.screensaver.ui.arch.datatable.SortDirectionSelector; import edu.harvard.med.screensaver.ui.arch.util.UISelectOneBean; import edu.harvard.med.screensaver.ui.arch.view.aspects.UICommand; /** * Notifies observers when set of available columns are changed, either from * setColumns() being called, or a column's setVisible() being called. * * @author drew */ public class TableColumnManager<R> extends Observable implements Observer { public static final String GROUP_NODE_DELIMITER = "::"; // static members private static Logger log = Logger.getLogger(TableColumnManager.class); // instance data private CurrentScreensaverUser _currentScreensaverUser; private List<TableColumn<R,?>> _columns = new ArrayList<TableColumn<R,?>>(); private List<TableColumn<R,?>> _visibleColumns = new ArrayList<TableColumn<R,?>>(); private List<TableColumn<R,?>> _sortableSearchableColumns = new ArrayList<TableColumn<R,?>>(); private ListDataModel _columnModel; // contains visible columns only private TreeModel _columnsSelectionTree; private UISelectOneBean<SortDirection> _sortDirectionSelector; private UISelectOneBean<TableColumn<R,?>> _sortColumnSelector; private Map<TableColumn<R,?>,List<TableColumn<R,?>>> _compoundSortColumnsMap = new HashMap<TableColumn<R,?>,List<TableColumn<R,?>>>(); private Map<String,TableColumn<R,?>> _name2Column = new HashMap<String,TableColumn<R,?>>(); // For ReorderListWidget: List and arrays for the pick list private List<SelectItem> allItemsLeft; private List<SelectItem> allItemsRight; private List<SelectItem> defaultItemsRight; // the default columns displayed at the start private List<SelectItem> defaultItemsLeft; // the default columns not displayed at the start private String[] selectedItemsLeft = {}; private String[] selectedItemsRight = {}; private boolean _useReorderListWidget = false; // ReorderListWidget: End /** * @param columns * @param currentScreensaverUser * @param useReorderListWidget if true use the dual list based column selector with the ability to re-orde * (do NOT use the tree based column selector) */ public TableColumnManager(List<? extends TableColumn<R,?>> columns, CurrentScreensaverUser currentScreensaverUser, boolean useReorderListWidget) { // For ReorderListWidget: Initialize the lists _useReorderListWidget = useReorderListWidget; if(isUseReorderListWidget()) { allItemsLeft = new ArrayList<SelectItem>(); allItemsRight = new ArrayList<SelectItem>(); defaultItemsRight = new ArrayList<SelectItem>(); defaultItemsLeft = new ArrayList<SelectItem>(); for (int i=0; i<columns.size(); i++) { if (columns.get(i).isVisible()) { allItemsRight.add(new SelectItem(columns.get(i).getName(), columns.get(i).getName())); defaultItemsRight.add(new SelectItem(columns.get(i).getName(), columns.get(i).getName())); } else { allItemsLeft.add(new SelectItem(columns.get(i).getName(), columns.get(i).getName())); defaultItemsLeft.add(new SelectItem(columns.get(i).getName(), columns.get(i).getName())); } } } // For ReorderListWidget: End _currentScreensaverUser = currentScreensaverUser; setColumns(columns); } /** * Get the current sort column. * * @motivation allow sort column to be set from a drop-down list UI component * (in addition to clicking on table column headers) * @return the current sort column */ public TableColumn<R,?> getSortColumn() { return getSortColumnSelector().getSelection(); } public List<TableColumn<R,?>> getSortColumns() { if (getSortColumnSelector().getSelection() == null) { return Collections.emptyList(); } List<TableColumn<R,?>> sortColumns = _compoundSortColumnsMap.get(getSortColumn()); if (sortColumns == null) { sortColumns = new ArrayList<TableColumn<R,?>>(); sortColumns.add(getSortColumnSelector().getSelection()); } return sortColumns; } public void addCompoundSortColumns(List<TableColumn<R,?>> compoundSortColumns) { _compoundSortColumnsMap.put(compoundSortColumns.get(0), compoundSortColumns); } public void addCompoundSortColumns(TableColumn<R,?>... compoundSortColumns) { addCompoundSortColumns(Arrays.asList(compoundSortColumns)); } public void addAllCompoundSorts(List<List<TableColumn<R,?>>> allCompoundSorts) { for (List<TableColumn<R,?>> compoundSort : allCompoundSorts) { addCompoundSortColumns(compoundSort); } } @SuppressWarnings("unchecked") public TreeModel getColumnsTreeModel() { if (_columnsSelectionTree == null) { TreeNodeBase root = new TreeNodeBase("root", "Columns", false); Map<String,TreeNode> groups = new HashMap<String,TreeNode>(); for (TableColumn<R,?> column : _columns) { if (!isColumnRestricted(column)) { TreeNode groupNode = getOrCreateGroupNode(root, groups, column.getGroup()); groupNode.getChildren().add(new SelectableColumnTreeNode<R>(column)); } } _columnsSelectionTree = new TreeModelBase(root); } return _columnsSelectionTree; } public int getSortColumnIndex() { return getSortColumnSelector().getSelectionIndex(); } /** * Get the index of the column currently being rendered by JSF. */ public int getCurrentColumnIndex() { return getVisibleColumnModel().getRowIndex(); } /** * Get the column currently being rendered by JSF. */ @SuppressWarnings("unchecked") public TableColumn<R,?> getCurrentColumn() { return (TableColumn<R,?>) getVisibleColumnModel().getRowData(); } public TableColumn<R,?> getColumn(String columnName) { return _name2Column.get(columnName); } /** * @return the n'th visible column (zero-based) */ public TableColumn<R,?> getColumn(int i) { return getVisibleColumns().get(i); } /** * Set the current sort column. * * @motivation allow sort column to be set from a drop-down list UI component * (in addition to clicking on table column headers) * @param newSortColumn the new current sort column */ public void setSortColumn(TableColumn<R,?> newSortColumn) { if (newSortColumn != null) { if (!newSortColumn.equals(getSortColumns().get(0))) { getSortColumnSelector().setSelection(newSortColumn); } } } /** * @motivation for use by dataTable JSF component. * @param sortColumnName the name of the new sort column */ public void setSortColumnName(String sortColumnName) { if (sortColumnName != null) { setSortColumn(getColumn(sortColumnName)); } else { setSortColumn(null); } } /** * @motivation for use by dataTable JSF component. * @return true the name of the current sort column */ public String getSortColumnName() { if (getSortColumn() == null) { return null; } return getSortColumn().getName(); } /** * @motivation for use by dataTable JSF component. * @param sortAscending true if new sort direction is ascending; false if * descending */ public void setSortAscending(boolean sortAscending) { if (sortAscending) { setSortDirection(SortDirection.ASCENDING); } else { setSortDirection(SortDirection.DESCENDING); } } /** * @motivation for use by dataTable JSF component. * @return true if current sort direction is ascending; false if descending */ public boolean isSortAscending() { return getSortDirection().equals(SortDirection.ASCENDING); } /** * Get the current sort direction. * * @motivation allow sort direction to be set from a drop-down list UI * component (in addition to clicking on table column headers) * @return the current sort column name */ public SortDirection getSortDirection() { return getSortDirectionSelector().getSelection(); } /** * Set the current sort direction. * * @motivation allow sort direction to be set from a drop-down list UI * component (in addition to clicking on table column headers) * @param currentSortDirection the new current sort direction */ public void setSortDirection(SortDirection currentSortDirection) { if (!getSortDirection().equals(currentSortDirection)) { getSortDirectionSelector().setSelection(currentSortDirection); } } /** * Get the JSF column model, containing visible columns. * * @return the data columns column model */ public ListDataModel getVisibleColumnModel() { return _columnModel; } public void setColumns(List<? extends TableColumn<R,?>> columns) { Set<TableColumn<R,?>> oldColumns = new HashSet<TableColumn<R,?>>(_columns); _columns.clear(); _columns.addAll(columns); _columnsSelectionTree = null; // force re-create _name2Column.clear(); for (TableColumn<R,?> column : columns) { column.addObserver(this); if (_name2Column.containsKey(column.getName())) { throw new IllegalArgumentException("column " + column + " has non-unique name"); } _name2Column.put(column.getName(), column); } updateVisibleColumns(new ColumnVisibilityChangedEvent(CollectionUtils.subtract(columns, oldColumns), CollectionUtils.subtract(oldColumns, columns))); } @SuppressWarnings("unchecked") public List<TableColumn<R,?>> getAllColumns() { return _columns; } public List<TableColumn<R,?>> getVisibleColumns() { return _visibleColumns; } public List<TableColumn<R,?>> getSortableSearchableColumns() { return _sortableSearchableColumns; } public void setVisibilityOfColumnsInGroup(String columnGroupName, boolean isVisible) { for (TableColumn<R,?> column : getAllColumns()) { if (column.getGroup().equals(columnGroupName)) { column.setVisible(isVisible); } } } public UISelectOneBean<TableColumn<R,?>> getSortColumnSelector() { if (_sortColumnSelector == null) { _sortColumnSelector = new UISelectOneBean<TableColumn<R,?>>(getSortableSearchableColumns()) { @Override protected String makeLabel(TableColumn<R,?> t) { return t.getName(); } }; _sortColumnSelector.addObserver(this); } return _sortColumnSelector; } public UISelectOneBean<SortDirection> getSortDirectionSelector() { if (_sortDirectionSelector == null) { _sortDirectionSelector = new SortDirectionSelector(); _sortDirectionSelector.addObserver(this); } return _sortDirectionSelector; } // JSF application methods @UICommand public String updateColumnSelections() { if(isUseReorderListWidget()) { // For ReorderListWidget: To arrange the columns according to the order specified by user List<TableColumn<R,?>> columns = getAllColumns(); List<TableColumn<R,?>> tempColumns = new ArrayList<TableColumn<R,?>>(); // Reset all columns to hidden for (int j=0; j<columns.size(); j++) { columns.get(j).setVisible(false); } for (int i = 0; i < allItemsRight.size(); i++) { for (int j=0; j<columns.size(); j++) { // Add visible columns to the front of list. Will be used in updateVisibleColumns // to obtain all visible columns if (allItemsRight.get(i).getValue().equals(columns.get(j).getName())) { columns.get(j).setVisible(true); tempColumns.add(columns.get(j)); break; } } } // add remaining (hidden) columns to the back for (int j=0; j<columns.size(); j++) { if (!columns.get(j).isVisible()) { tempColumns.add(columns.get(j)); } } _columns = tempColumns; updateVisibleColumns(new ColumnVisibilityChangedEvent()); } return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } @UICommand public String selectAllColumns() { // TODO return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } @UICommand public String unselectAllColumns() { // TODO return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } // Observer methods public void update(Observable o, Object arg) { if (o == _sortColumnSelector) { setChanged(); notifyObservers(new SortChangedEvent<R>(getSortColumn())); } else if (o == _sortDirectionSelector) { setChanged(); notifyObservers(new SortChangedEvent<R>(getSortDirection())); } else if (o instanceof TableColumn) { if (arg instanceof ColumnVisibilityChangedEvent) { log.debug("TableColumnManager notified of column visibility change: " + o); updateVisibleColumns((ColumnVisibilityChangedEvent) arg); } else if (arg instanceof Criterion) { // column's filtering criteria changed setChanged(); notifyObservers(arg); } } } // private methods private void updateVisibleColumns(ColumnVisibilityChangedEvent event) { // for reorder mode, update should be done even if columnsAdded // and columnsRemoved are unchanged if (event.getColumnsAdded().size() > 0 || event.getColumnsRemoved().size() > 0 || isUseReorderListWidget() ) { if (log.isDebugEnabled()) { log.debug("column selections changed: " + event); } // rebuild _visibleColumns, maintaining the fixed order of the columns // we ignore the event's take on added & removed columns, since we can determine this reliably by inspecting each column _visibleColumns.clear(); _sortableSearchableColumns.clear(); for (TableColumn<R,?> column : getAllColumns()) { if (column.isVisible() && !isColumnRestricted(column)) { _visibleColumns.add(column); if (column.isSortableSearchable()) { _sortableSearchableColumns.add(column); } } } getSortColumnSelector().setDomain(_sortableSearchableColumns); _columnModel = new ListDataModel(_visibleColumns); setChanged(); notifyObservers(event); } } private boolean isColumnRestricted(TableColumn<?,?> column) { if (!column.isAdministrative()) { return false; } if (_currentScreensaverUser != null && _currentScreensaverUser.getScreensaverUser().isUserInRole(ScreensaverUserRole.READ_EVERYTHING_ADMIN)) { return false; } return true; } private TreeNode getOrCreateGroupNode(TreeNodeBase root, Map<String,TreeNode> groups, String groupPath) { TreeNode groupNode = groups.get(groupPath); if (groupNode == null) { if (groupPath.equals(TableColumn.UNGROUPED)) { return root; } TreeNode parent; String groupName; int lastPathDelimPos = groupPath.lastIndexOf(GROUP_NODE_DELIMITER); if (lastPathDelimPos < 0) { // leaf group node parent = root; groupName = groupPath; } else { // internal group node parent = getOrCreateGroupNode(root, groups, groupPath.substring(0, lastPathDelimPos)); groupName = groupPath.substring(lastPathDelimPos + GROUP_NODE_DELIMITER.length()); } // TODO: remove special cases here by adding appropriate flag param if (parent.getDescription().contains("Annotations") || parent.getDescription().contains("Data Columns")) { groupNode = new SelectableColumnGroupTreeNode(groupName); } else { groupNode = new TreeNodeBase("group", groupName, false); } parent.getChildren().add(groupNode); groups.put(groupPath, groupNode); } return groupNode; } // For ReorderListWidget: Getters and setters public boolean isUseReorderListWidget() { return _useReorderListWidget; } public boolean isUseTreeWidget() { return !_useReorderListWidget; } public List<SelectItem> getAllItemsLeft() { return allItemsLeft; } public void setAllItemsLeft(List<SelectItem> allItemsLeft) { this.allItemsLeft = allItemsLeft; } public List<SelectItem> getAllItemsRight() { return allItemsRight; } public void setAllItemsRight(List<SelectItem> allItemsRight) { this.allItemsRight = allItemsRight; } public String[] getSelectedItemsLeft() { return selectedItemsLeft; } public void setSelectedItemsLeft(String[] selectedItemsLeft) { this.selectedItemsLeft = selectedItemsLeft; } public String[] getSelectedItemsRight() { return selectedItemsRight; } public void setSelectedItemsRight(String[] selectedItemsRight) { this.selectedItemsRight = selectedItemsRight; } public String leftToRight() { for (int i = 0; i < selectedItemsLeft.length; i++) { for (int j=0; j<allItemsLeft.size(); j++) { if (selectedItemsLeft[i].equals(allItemsLeft.get(j).getValue())) { allItemsRight.add(allItemsLeft.get(j)); allItemsLeft.remove(j); } } } return null; } public String allLeftToRight() { allItemsRight.addAll(allItemsLeft); allItemsLeft.clear(); return null; } public String rightToLeft() { for (int i = 0; i < selectedItemsRight.length; i++) { for (int j=0; j<allItemsRight.size(); j++) { if (selectedItemsRight[i].equals(allItemsRight.get(j).getValue())) { allItemsLeft.add(allItemsRight.get(j)); allItemsRight.remove(j); } } } return null; } public String allRightToLeft() { allItemsLeft.addAll(allItemsRight); allItemsRight.clear(); return null; } public String moveUp() { int j = 0; for (int i=0; i<allItemsRight.size(); i++) { if (selectedItemsRight.length <= j) break; if (selectedItemsRight[j].equals(allItemsRight.get(i).getValue())) { j++; if (i != 0) allItemsRight.add(i-1, allItemsRight.remove(i)); } } return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } public String moveDown() { int j = selectedItemsRight.length-1; for (int i=allItemsRight.size()-1; i>=0; i--) { if (j < 0) break; if (selectedItemsRight[j].equals(allItemsRight.get(i).getValue())) { j--; if (i != allItemsRight.size()-1) allItemsRight.add(i, allItemsRight.remove(i+1)); } } return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } // JSF UI method to restore columns to the default settings public String updateDefaultColumns() { if (log.isDebugEnabled()) { log.debug("BII: column selections changed back to default"); } // clear the current selection lists allItemsRight.clear(); allItemsLeft.clear(); // Set both selection list to default settings for (int i = 0; i < defaultItemsRight.size(); i++) { allItemsRight.add(new SelectItem(defaultItemsRight.get(i).getValue(),defaultItemsRight.get(i).getLabel())); } for (int i = 0; i < defaultItemsLeft.size(); i++) { allItemsLeft.add(new SelectItem(defaultItemsLeft.get(i).getValue(),defaultItemsLeft.get(i).getLabel())); } updateColumnSelections(); return ScreensaverConstants.REDISPLAY_PAGE_ACTION_RESULT; } // For ReorderListWidget: End }