/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.table.support;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.AbstractTableComparatorChooser;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.swing.EventSelectionModel;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import ca.odell.glazedlists.util.concurrent.Lock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.support.PropertyComparator;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.valkyriercp.application.StatusBar;
import org.valkyriercp.application.event.LifecycleApplicationEvent;
import org.valkyriercp.command.ActionCommandExecutor;
import org.valkyriercp.command.GuardedActionCommandExecutor;
import org.valkyriercp.command.support.CommandGroup;
import org.valkyriercp.factory.AbstractControlFactory;
import org.valkyriercp.util.PopupMenuMouseListener;
import javax.annotation.PostConstruct;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Arrays;
/**
* This class provides a standard table representation for a set of objects with
* properties of the objects presented in the columns of the table. The table
* created offers the following features:
* <ol>
* <li>It uses Glazed Lists as the underlying data model and this provides for
* multi-column sorting and text filtering.</li>
* <li>It handles row selection.</>
* <li>It offers simple, delegated handling of how to handle a double-click on a
* row, by setting a command executor. See
* {@link #setDoubleClickHandler(ActionCommandExecutor)}.</li>
* <li>It supports display of a configured pop-up context menu.</li>
* <li>It can report on row counts (after filtering) and selection counts to a
* status bar</li>
* </ol>
* <p>
* Several I18N messages are needed for proper reporting to a configured status
* bar. The message keys used are:
* <p>
* <table border="1">
* <tr>
* <td><b>Message key </b></td>
* <td><b>Usage </b></td>
* </tr>
* <tr>
* <td><i>modelId</i>.objectName.singular</td>
* <td>The singular name of the objects in the table</td>
* </tr>
* <tr>
* <td><i>modelId</i>.objectName.plural</td>
* <td>The plural name of the objects in the table</td>
* </tr>
* <tr>
* <td><i>[modelId]</i>.objectTable.showingAll.message</td>
* <td>The message to show when all objects are being shown, that is no objects
* have been filtered. This is typically something like
* "Showing all nn contacts". The message takes the number of objects nd the
* object name (singular or plural) as parameters.</td>
* </tr>
* <tr>
* <td><i>[modelId]</i>.objectTable.showingN.message</td>
* <td>The message to show when some of the objects have been filtered from the
* display. This is typically something like "Showing nn contacts of nn". The
* message takes the shown count, the total count, and the object name (singular
* or plural) as parameters.</td>
* </tr>
* <tr>
* <td><i>[modelId]</i>.objectTable.selectedN.message</td>
* <td>The message to append to the filter message when the selection is not
* empty. Typically something like ", nn selected". The message takes the number
* of selected entries as a parameter.</td>
* </tr>
* </table>
* <p>
* Note that the message keys that show the model id in brackets, like this
* <i>[modelId]</i>, indicate that the model id is optional. If no message is
* found using the model id, then the key will be tried without the model id and
* the resulting string will be used. This makes it easy to construct one single
* message property that can be used on numerous tables.
* <p>
* <em>Note:</em> If you are using application events to inform UI components of
* changes to domain objects, then instances of this class have to be wired into
* the event distribution. To do this, you should construct instances (of
* concrete subclasses) in the application context. They will automatically be
* wired into the epplication event mechanism because this class implements
* {@link org.springframework.context.ApplicationListener}.
*
* @author Larry Streepy
*/
public abstract class AbstractObjectTable extends AbstractControlFactory
// implements ApplicationListener
{
private Logger logger = LoggerFactory.getLogger(getClass());
private final String modelId;
private String objectSingularName;
private String objectPluralName;
private Object[] initialData;
private String[] columnPropertyNames;
private GlazedTableModel model;
private SortedList baseList;
private EventList finalEventList;
private ActionCommandExecutor doubleClickHandler;
private CommandGroup popupCommandGroup;
private StatusBar statusBar;
private AbstractTableComparatorChooser tableSorter;
public static final String SHOWINGALL_MSG_KEY = "objectTable.showingAll.message";
public static final String SHOWINGN_MSG_KEY = "objectTable.showingN.message";
public static final String SELECTEDN_MSG_KEY = "objectTable.selectedN.message";
/**
* Constructor.
*
* @param modelId
* used for generating message keys
*/
public AbstractObjectTable(String modelId, String[] columnPropertyNames) {
this.modelId = modelId;
setColumnPropertyNames(columnPropertyNames);
init();
postConstruct();
}
protected void postConstruct() {
// this will cause a memory leak! as there is currently no support for a
// call to removeApplicationListener
((ConfigurableApplicationContext) getApplicationConfig()
.applicationContext())
.addApplicationListener(new ApplicationListener() {
@Override
public void onApplicationEvent(ApplicationEvent event) {
AbstractObjectTable.this.onApplicationEvent(event);
}
});
}
/**
* Set the initial data to display.
*
* @param initialData
* Array of objects to display
*/
public void setInitialData(Object[] initialData) {
this.initialData = initialData;
}
/**
* Get the initial data to display. If none has been set, then return the
* default initial data.
*
* @return initial data to display
* @see #getDefaultInitialData()
*/
public Object[] getInitialData() {
if (initialData == null) {
initialData = getDefaultInitialData();
}
return initialData;
}
/**
* Get the base event list for the table model. This can be used to build
* layered event models for filtering.
*
* @return base event list
*/
public EventList getBaseEventList() {
if (baseList == null) {
// Construct on demand
Object[] data = getInitialData();
if (logger.isDebugEnabled()) {
logger.debug("Table data: got " + data.length + " entries");
}
// Construct the event list of all our data and layer on the sorting
EventList rawList = GlazedLists.eventList(Arrays.asList(data));
int initialSortColumn = getInitialSortColumn();
if (initialSortColumn >= 0) {
String sortProperty = getColumnPropertyNames()[initialSortColumn];
baseList = new SortedList(rawList, new PropertyComparator(
sortProperty, false, true));
} else {
baseList = new SortedList(rawList);
}
}
return baseList;
}
/**
* Set the event list to be used for constructing the table model. The event
* list provided MUST have been constructed from the list returned by
* {@link #getBaseEventList()} or this table will not work properly.
*
* @param finalEventList
* list to use
*/
public void setFinalEventList(EventList finalEventList) {
this.finalEventList = finalEventList;
}
/**
* Get the event list to be use for constructing the table model.
*
* @return final event list
*/
public EventList getFinalEventList() {
if (finalEventList == null) {
finalEventList = getBaseEventList();
}
return finalEventList;
}
/**
* Get the data model for the table.
* <p>
* <em>Note:</em> This method returns null unless {@link #getTable()} or is
* called
*
* @return model the table model which is used for the table
*/
public GlazedTableModel getTableModel() {
return model;
}
/**
* Get the names of the properties to display in the table columns.
*
* @return array of columnproperty names
*/
public String[] getColumnPropertyNames() {
return columnPropertyNames;
}
/**
* Set the names of the properties to display in the table columns.
*
* @param columnPropertyNames
*/
public void setColumnPropertyNames(String[] columnPropertyNames) {
this.columnPropertyNames = columnPropertyNames;
}
/**
* @return the doubleClickHandler
*/
public ActionCommandExecutor getDoubleClickHandler() {
return doubleClickHandler;
}
/**
* Set the handler (action executor) that should be invoked when a row in
* the table is double-clicked.
*
* @param doubleClickHandler
* the doubleClickHandler to set
*/
public void setDoubleClickHandler(ActionCommandExecutor doubleClickHandler) {
this.doubleClickHandler = doubleClickHandler;
}
/**
* Returns the sorter which is used to sort the content of the table
*
* @return the sorter, null if {@link #getTable()} is not called before
*/
protected AbstractTableComparatorChooser getTableSorter() {
return tableSorter;
}
/**
* @return the popupCommandGroup
*/
public CommandGroup getPopupCommandGroup() {
return popupCommandGroup;
}
/**
* Set the command group that should be used to construct the popup menu
* when a user initiates the UI gesture to show the context menu. If this is
* null, then no popup menu will be shown.
*
* @param popupCommandGroup
* the popupCommandGroup to set
*/
public void setPopupCommandGroup(CommandGroup popupCommandGroup) {
this.popupCommandGroup = popupCommandGroup;
}
/**
* Set the status bar associated with this table. If non-null, then any time
* the final event list on this table changes, then the status bar will be
* updated with the current object counts.
*
* @param statusBar
* to update
*/
public void setStatusBar(StatusBar statusBar) {
this.statusBar = statusBar;
updateStatusBar();
}
/**
* @return the modelId
*/
public String getModelId() {
return modelId;
}
/**
* Initialize our internal values.
*/
protected void init() {
// Get all our messages
objectSingularName = getApplicationConfig().messageResolver().getMessage(
modelId + ".objectName.singular");
objectPluralName = getApplicationConfig().messageResolver().getMessage(
modelId + ".objectName.plural");
}
protected JComponent createControl() {
// Contstruct the table model and table to display the data
EventList finalEventList = getFinalEventList();
model = createTableModel(finalEventList);
JTable table = getApplicationConfig().componentFactory().createTable(model);
table.setSelectionModel(new EventSelectionModel(finalEventList));
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
// Install the sorter
Assert.notNull(baseList);
tableSorter = createTableSorter(table, baseList);
// Allow the derived type to configure the table
configureTable(table);
int initialSortColumn = getInitialSortColumn();
if (initialSortColumn >= 0) {
tableSorter.clearComparator();
tableSorter.appendComparator(initialSortColumn, 0, false);
}
// Add the context menu listener
table.addMouseListener(new ContextPopupMenuListener());
// Add our mouse handlers to setup our desired selection mechanics
table.addMouseListener(new DoubleClickListener());
// Keep our status line up to date with the selections and filtering
StatusBarUpdateListener statusBarUpdateListener = new StatusBarUpdateListener();
table.getSelectionModel().addListSelectionListener(
statusBarUpdateListener);
getFinalEventList().addListEventListener(statusBarUpdateListener);
return table;
}
/**
* Configure the newly created table as needed. Install any needed column
* sizes, renderers, and comparators. The default implementation does
* nothing.
*
* @param table
* The table to configure
*/
protected void configureTable(JTable table) {
}
/**
* Get the default set of objects for this table.
*
* @return Array of data for the table
*/
protected abstract Object[] getDefaultInitialData();
/**
* Returns the created JTable.
*/
protected JTable getTable() {
return (JTable) getControl();
}
protected AbstractTableComparatorChooser createTableSorter(JTable table,
SortedList sortedList) {
return new TableComparatorChooser(table, sortedList,
isMultipleColumnSort());
}
protected boolean isMultipleColumnSort() {
return true;
}
/**
* Handle a double click on a row of the table. The row will already be
* selected.
*/
protected void onDoubleClick() {
// Dispatch this to the doubleClickHandler, if any
if (doubleClickHandler != null) {
boolean okToExecute = true;
if (doubleClickHandler instanceof GuardedActionCommandExecutor) {
okToExecute = ((GuardedActionCommandExecutor) doubleClickHandler)
.isEnabled();
}
if (okToExecute) {
doubleClickHandler.execute();
}
}
}
/**
* Construct the table model for this table. The default implementation of
* this creates a GlazedTableModel using an Advanced format.
*
* @param eventList
* on which to build the model
* @return table model
*/
protected GlazedTableModel createTableModel(EventList eventList) {
return new GlazedTableModel(eventList, getColumnPropertyNames(),
modelId) {
protected TableFormat createTableFormat() {
return new DefaultAdvancedTableFormat();
}
};
}
/**
* Determine if the event should be handled on this table. If
* <code>true</code> is returned (the default), then the list holding the
* table data will be scanned for the object and updated appropriately
* depending on then event type.
*
* @param event
* to inspect
* @return boolean true if the object should be handled, false otherwise
* @see #handleDeletedObject(Object)
* @see #handleNewObject(Object)
* @see #handleUpdatedObject(Object)
*/
protected boolean shouldHandleEvent(ApplicationEvent event) {
return true;
}
/**
* Create the context popup menu, if any, for this table. The default
* operation is to create the popup from the command group if one has been
* specified. If not, then null is returned.
*
* @return popup menu to show, or null if none
*/
protected JPopupMenu createPopupContextMenu() {
return (getPopupCommandGroup() != null) ? getPopupCommandGroup()
.createPopupMenu() : null;
}
/**
* Create the context popup menu, if any, for this table. The default
* operation is to create the popup from the command group if one has been
* specified. If not, then null is returned.
*
* @param e
* the event which contains information about the current
* context.
* @return popup menu to show, or null if none
*/
protected JPopupMenu createPopupContextMenu(MouseEvent e) {
return createPopupContextMenu();
}
/**
* Get the default sort column. Defaults to 0.
*
* @return column to sort on
*/
protected int getInitialSortColumn() {
return 0;
}
/**
* Get the selection model.
*
* @return selection model
*/
public ListSelectionModel getSelectionModel() {
return getTable().getSelectionModel();
}
/**
* Executes the runnable with a write lock on the event list.
*
* @param runnable
* its run method is executed while holding a write lock for the
* event list.
*
* @see #getFinalEventList()
*/
protected void runWithWriteLock(Runnable runnable) {
runWithLock(runnable, getFinalEventList().getReadWriteLock()
.writeLock());
}
/**
* Executes the runnable with a read lock on the event list.
*
* @param runnable
* its run method is executed while holding a read lock for the
* event list.
*
* @see #getFinalEventList()
*/
protected void runWithReadLock(Runnable runnable) {
runWithLock(runnable, getFinalEventList().getReadWriteLock().readLock());
}
private void runWithLock(Runnable runnable, Lock lock) {
Assert.notNull(runnable);
Assert.notNull(lock);
lock.lock();
try {
runnable.run();
} finally {
lock.unlock();
}
}
/**
* Handle the creation of a new object.
*
* @param object
* New object to handle
*/
protected void handleNewObject(final Object object) {
runWithWriteLock(new Runnable() {
public void run() {
getFinalEventList().add(object);
}
});
}
/**
* Handle an updated object in this table. Locate the existing entry (by
* equals) and replace it in the underlying list.
*
* @param object
* Updated object to handle
*/
protected void handleUpdatedObject(final Object object) {
runWithWriteLock(new Runnable() {
public void run() {
int index = getFinalEventList().indexOf(object);
if (index >= 0) {
getFinalEventList().set(index, object);
}
}
});
}
/**
* Handle the deletion of an object in this table. Locate this entry (by
* equals) and delete it.
*
* @param object
* Updated object being deleted
*/
protected void handleDeletedObject(final Object object) {
runWithWriteLock(new Runnable() {
public void run() {
int index = getFinalEventList().indexOf(object);
if (index >= 0) {
getFinalEventList().remove(index);
}
}
});
}
/**
* Update the status bar with the current display counts.
*/
protected void updateStatusBar() {
if (statusBar != null) {
int all = getBaseEventList().size();
int showing = getFinalEventList().size();
String msg;
if (all == showing) {
String[] keys = new String[] {
modelId + "." + SHOWINGALL_MSG_KEY, SHOWINGALL_MSG_KEY };
msg = getApplicationConfig().messageResolver().getMessage(
keys,
new Object[] {
"" + all,
(all != 1) ? objectPluralName
: objectSingularName });
} else {
String[] keys = new String[] {
modelId + "." + SHOWINGN_MSG_KEY, SHOWINGN_MSG_KEY };
msg = getApplicationConfig().messageResolver().getMessage(
keys,
new Object[] {
"" + showing,
(showing != 1) ? objectPluralName
: objectSingularName, "" + all });
}
// Now add the selection info
int nselected = getTable().getSelectedRowCount();
if (nselected > 0) {
String[] keys = new String[] {
modelId + "." + SELECTEDN_MSG_KEY, SELECTEDN_MSG_KEY };
msg += getApplicationConfig().messageResolver().getMessage(keys,
new Object[] { "" + nselected });
}
statusBar.setMessage(msg.toString());
}
}
/**
* Handle an application event. This will notify us of object adds, deletes,
* and modifications. Update our table model accordingly.
*
* @param e
* event to process
*/
public void onApplicationEvent(ApplicationEvent e) {
if (e instanceof LifecycleApplicationEvent) {
LifecycleApplicationEvent le = (LifecycleApplicationEvent) e;
if (shouldHandleEvent(e)) {
if (le.getEventType() == LifecycleApplicationEvent.CREATED) {
handleNewObject(le.getObject());
} else if (le.getEventType() == LifecycleApplicationEvent.MODIFIED) {
handleUpdatedObject(le.getObject());
} else if (le.getEventType() == LifecycleApplicationEvent.DELETED) {
handleDeletedObject(le.getObject());
}
}
}
}
final class ContextPopupMenuListener extends PopupMenuMouseListener {
protected JPopupMenu getPopupMenu(MouseEvent e) {
return createPopupContextMenu(e);
}
}
final class DoubleClickListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
// If the user right clicks on a row other than the selection,
// then move the selection to the current row
if (e.getButton() == MouseEvent.BUTTON3) {
int rowUnderMouse = getTable().rowAtPoint(e.getPoint());
if (rowUnderMouse != -1
&& !getTable().isRowSelected(rowUnderMouse)) {
// Select the row under the mouse
getSelectionModel().setSelectionInterval(rowUnderMouse,
rowUnderMouse);
}
}
}
/**
* Handle double click.
*/
public void mouseClicked(MouseEvent e) {
// If the user double clicked on a row, then call onDoubleClick
if (e.getClickCount() == 2) {
onDoubleClick();
}
}
}
final class StatusBarUpdateListener implements ListSelectionListener,
ListEventListener {
public void valueChanged(ListSelectionEvent e) {
updateStatusBar();
}
public void listChanged(ListEvent listChanges) {
updateStatusBar();
}
}
}