// $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.searchresults; import java.io.Serializable; import java.util.Iterator; import java.util.List; import java.util.Observable; import java.util.Observer; import javax.faces.component.UIData; import javax.faces.event.ActionEvent; import javax.faces.model.DataModelListener; import com.google.common.collect.Lists; import org.apache.log4j.Logger; import org.apache.myfaces.custom.datascroller.HtmlDataScroller; import org.apache.myfaces.custom.datascroller.ScrollerActionEvent; import edu.harvard.med.screensaver.db.SortDirection; import edu.harvard.med.screensaver.model.Entity; import edu.harvard.med.screensaver.model.meta.PropertyPath; import edu.harvard.med.screensaver.ui.arch.datatable.DataTableModelType; import edu.harvard.med.screensaver.ui.arch.datatable.column.TableColumn; import edu.harvard.med.screensaver.ui.arch.datatable.model.DataTableModel; import edu.harvard.med.screensaver.ui.arch.util.UISelectOneBean; import edu.harvard.med.screensaver.ui.arch.view.EntityViewer; import edu.harvard.med.screensaver.ui.arch.view.EntityViewerBackingBean; import edu.harvard.med.screensaver.ui.arch.view.aspects.UICommand; import edu.harvard.med.screensaver.util.NullSafeUtils; /** * SearchResults where each row represents an {@link Entity}. Provides "Summary" and "Entity" viewing modes, where * Summary mode is the normal table-based view of entities, and Entity mode show a detailed, full-page view of a * single-entity. * * @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a> * @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a> */ public abstract class EntitySearchResults<E extends Entity<K>,R,K extends Serializable> extends SearchResults<R,K,PropertyPath<E>> { private static Logger log = Logger.getLogger(EntitySearchResults.class); private static final String[] CAPABILITIES = { "viewEntity", "filter" }; protected Observer _rowsPerPageSelectorObserver; private EntityViewer<E> _entityViewer; private boolean _isNested; public void initialize(DataTableModel<R> dataTableModel) { super.initialize(new EntitySearchResultsDataModel(dataTableModel)); // reset to default rows-per-page, if in "entity view" mode if (isEntityView()) { getRowsPerPageSelector().setSelection(getRowsPerPageSelector().getDefaultSelection()); } } public EntityViewer getEntityViewer() { return _entityViewer; } /** * Returns true if this search result is nested within another viewer, which causes all the special behavior of this * class to be disabled * * @motivation Java does not support mix-ins, so need a way to mix-out the behavior of this superclass */ public boolean isNested() { return _isNested; } public void setNested(boolean isNested) { _isNested = isNested; } private EntityViewerBackingBean _nestedIn; /** * @return the parent viewer in which this search result is nested */ public EntityViewerBackingBean getNestedIn() { return _nestedIn; } public void setNestedIn(EntityViewerBackingBean nestedIn) { setNested(nestedIn != null); _nestedIn = nestedIn; } /** * View the entity currently selected in the DataTableModel in entity view * mode. * * @motivation To be called by a TableColumn.cellAction() method to view the * current row's entity in the "entity view" mode, or by any other * code that wants to switch to entity view mode. * @motivation To be called by a DataTableModel listener in response to * rowSelected() events. */ @UICommand final protected String viewSelectedEntity() { if (getDataTableModel().getRowCount() > 0 && getDataTableModel().isRowAvailable()) { log.debug("viewSelectedEntity(): row " + getDataTableModel().getRowIndex()); viewEntityAtRow(getDataTableModel().getRowIndex()); } return REDISPLAY_PAGE_ACTION_RESULT; } protected UISelectOneBean<Integer> buildRowsPerPageSelector() { UISelectOneBean<Integer> rowsPerPageSelector; if (getEntityViewer() == null) { rowsPerPageSelector = super.buildRowsPerPageSelector(); _rowsPerPageSelectorObserver = new Observer() { @Override public void update(Observable o, Object arg) { } }; } else { // note: we need a special "single" (1) selection item, for viewing the // entity in its full viewer page if (getRowsPerPageSelections().get(0) != 1) { List<Integer> rowsPerPageSelections = Lists.newArrayList(getRowsPerPageSelections()); rowsPerPageSelections.add(0, 1); setRowsPerPageSelections(rowsPerPageSelections); } rowsPerPageSelector = new UISelectOneBean<Integer>(getRowsPerPageSelections(), getDefaultRowsPerPage()) { @Override public String makeLabel(Integer value) { if (value.equals(1)) { return "Single"; } else { return super.makeLabel(value); } } }; _rowsPerPageSelectorObserver = new Observer() { public void update(Observable obs, Object o) { if (((Integer) o) == 1) { int firstRow = 0; if (getDataTableUIComponent() != null) { firstRow = getDataTableUIComponent().getFirst(); } log.debug("entering 'entity view' mode; setting data table row to first row on page:" + firstRow); getDataTableModel().setRowIndex(firstRow); viewSelectedEntity(); } } }; } rowsPerPageSelector.addObserver(_rowsPerPageSelectorObserver); return rowsPerPageSelector; } // public constructors and methods /** * @motivation for CGLIB2 */ public EntitySearchResults() { this(null); } public EntitySearchResults(EntityViewer<E> entityViewer) { super(CAPABILITIES); _entityViewer = entityViewer; if (_entityViewer == null) { getCapabilities().remove("viewEntity"); } } public boolean isRowRestricted() { return rowToEntity(getRowData()).isRestricted(); } public boolean isSummaryView() { return !isEntityView(); } public boolean isEntityView() { if (getEntityViewer() == null) { return false; } return getRowsPerPage() == 1 && getRowCount() > 0; } @UICommand public String returnToSummaryList() { getRowsPerPageSelector().setSelection(getRowsPerPageSelector().getDefaultSelection()); scrollToPageContainingRow(getDataTableUIComponent().getFirst()); return REDISPLAY_PAGE_ACTION_RESULT; } abstract protected E rowToEntity(R row); abstract public void searchAll(); /** * Switch to entity view mode and show the specified entity, automatically * scrolling the data table to the row containing the entity. * * @param entity * @return true if the entity exists in the search result, otherwise false */ public boolean findEntity(E entity) { int rowIndex; // first test whether the current row is already the one with the requested entity if (getDataTableModel().isRowAvailable() && getRowData().equals(entity)) { rowIndex = getDataTableModel().getRowIndex(); log.debug("entity " + entity + " found at current row in entity search results"); } else { // do linear search to find the entity (but only works for InMemoryDataModel) rowIndex = findRowOfEntity(entity); if (rowIndex < 0) { log.debug("entity " + entity + " not found in current entity search results"); searchAll(); rowIndex = findRowOfEntity(entity); if (rowIndex < 0) { log.debug("entity " + entity + " not found in full entity search results"); return false; } } log.debug("entity " + entity + " found in entity search results at row " + rowIndex); } viewEntityAtRow(rowIndex); return true; } private void viewEntityAtRow(int rowIndex) { if (rowIndex >= 0 && rowIndex < getRowCount()) { getDataTableModel().setRowIndex(rowIndex); R row = (R) getRowData(); E entity = rowToEntity(row); log.debug("viewEntityAtRow(): setting entity to view: " + entity + " at row " + rowIndex); scrollToRow(rowIndex); _entityViewer.setEntity(entity); switchToEntityViewMode(); } } /** * Switch to entity view mode, but without notifying observer. * * @motivation for switching into entity view mode programatically, as opposed * to in response to a user event */ private void switchToEntityViewMode() { getRowsPerPageSelector().deleteObserver(_rowsPerPageSelectorObserver); getRowsPerPageSelector().setSelection(1); getRowsPerPageSelector().addObserver(_rowsPerPageSelectorObserver); } /** * Override to ensure that "table filter mode" is always disabled when in * "entity view" mode, since search fields would entirely hidden from user in * this mode. */ @Override public boolean isTableFilterMode() { return super.isTableFilterMode() && isSummaryView(); } /** * Override to ensure that "table filter mode" is always disabled when in * "entity view" mode, since search fields would entirely hidden from user in * this mode. */ @Override public void setTableFilterMode(boolean isTableFilterMode) { if (isEntityView()) { return; } else { super.setTableFilterMode(isTableFilterMode); } } // private methods public void dataScrollerListener(ActionEvent event) { if (isEntityView()) { if (getDataTableUIComponent() != null) { UIData uiData = getDataTableUIComponent(); HtmlDataScroller scroller = (HtmlDataScroller) event.getSource(); ScrollerActionEvent scrollerEvent = (ScrollerActionEvent) event; String facet = scrollerEvent.getScrollerfacet(); int nextRowIndex = -1; int originalRowIndex = uiData.getFirst(); // the following code was copied from HtmlDataScroller.broadcast(), // since we must calculate the next row to be scrolled to in exactly the // same way, since the new row is not yet calculated for us at the time // this listener is called. if (HtmlDataScroller.FACET_FIRST.equals(facet)) { nextRowIndex = 0; } else if (HtmlDataScroller.FACET_PREVIOUS.equals(facet)) { int previous = uiData.getFirst() - uiData.getRows(); if (previous >= 0) { nextRowIndex = previous; } } else if (HtmlDataScroller.FACET_NEXT.equals(facet)) { int next = uiData.getFirst() + uiData.getRows(); if (next < uiData.getRowCount()) { nextRowIndex = next; } } else if (HtmlDataScroller.FACET_FAST_FORWARD.equals(facet)) { int fastStep = Math.max(1, scroller.getFastStep()); nextRowIndex = uiData.getFirst() + uiData.getRows() * fastStep; int rowcount = uiData.getRowCount(); if (nextRowIndex > rowcount) { nextRowIndex = (rowcount - 1) - ((rowcount - 1) % uiData.getRows()); } } else if (HtmlDataScroller.FACET_FAST_REWIND.equals(facet)) { int fastStep = Math.max(1, scroller.getFastStep()); nextRowIndex = uiData.getFirst() - uiData.getRows() * fastStep; nextRowIndex = Math.max(0, nextRowIndex); } else if (HtmlDataScroller.FACET_LAST.equals(facet)) { int rowcount = uiData.getRowCount(); int rows = uiData.getRows(); int delta = rowcount % rows; nextRowIndex = delta > 0 && delta < rows ? rowcount - delta : rowcount - rows; nextRowIndex = Math.max(0, nextRowIndex); } if (nextRowIndex >= 0) { getDataTableModel().setRowIndex(nextRowIndex); viewSelectedEntity(); // revert scrolling performed by viewSelectedEntity(), since // HtmlDataScroller handler will expect this to be unchanged uiData.setFirst(originalRowIndex); } } } } protected void updateEntityView() { if (isEntityView()) { if (getDataTableUIComponent() != null) { getDataTableModel().setRowIndex(getDataTableUIComponent().getFirst()); } viewSelectedEntity(); } } private int findRowOfEntity(E entity) { DataTableModel model = (DataTableModel) getDataTableModel(); if (model.getModelType() == DataTableModelType.IN_MEMORY) { List<R> data = (List<R>) model.getWrappedData(); for (int i = 0; i < data.size(); i++) { R row = data.get(i); E entityRow = rowToEntity(row); if (NullSafeUtils.nullSafeEquals(entityRow, entity)) { return i; } } } return -1; } private class EntitySearchResultsDataModel extends DataTableModel<R> { private DataTableModel<R> _baseDataModel; protected EntitySearchResultsDataModel(DataTableModel<R> baseDataModel) { super(); _baseDataModel = baseDataModel; } @Override public void addDataModelListener(DataModelListener listener) { _baseDataModel.addDataModelListener(listener); } @Override public DataModelListener[] getDataModelListeners() { return _baseDataModel.getDataModelListeners(); } @Override public void removeDataModelListener(DataModelListener listener) { _baseDataModel.removeDataModelListener(listener); } @Override public void fetch(List<? extends TableColumn<R,?>> columns) { _baseDataModel.fetch(columns); } @Override public void filter(List<? extends TableColumn<R,?>> filterColumns) { _baseDataModel.filter(filterColumns); updateEntityView(); } @Override public DataTableModelType getModelType() { return _baseDataModel.getModelType(); } @Override public void sort(List<? extends TableColumn<R,?>> sortColumns, SortDirection sortDirection) { _baseDataModel.sort(sortColumns, sortDirection); updateEntityView(); } @Override public int getRowCount() { return _baseDataModel.getRowCount(); } @Override public Object getRowData() { return _baseDataModel.getRowData(); } @Override public int getRowIndex() { return _baseDataModel.getRowIndex(); } @Override public Object getWrappedData() { return _baseDataModel.getWrappedData(); } @Override public boolean isRowAvailable() { return _baseDataModel.isRowAvailable(); } @Override public void setRowIndex(int rowIndex) { _baseDataModel.setRowIndex(rowIndex); } @Override public void setWrappedData(Object data) { _baseDataModel.setWrappedData(data); } @Override public Iterator<R> iterator() { return _baseDataModel.iterator(); } } }