package com.cedarsoft.spring.rcp.table;
import org.springframework.richclient.factory.AbstractControlFactory;
import org.springframework.richclient.table.support.GlazedTableModel;
import org.springframework.richclient.table.TableUtils;
import org.springframework.richclient.application.event.LifecycleApplicationEvent;
import org.springframework.richclient.application.statusbar.StatusBar;
import org.springframework.richclient.command.ActionCommandExecutor;
import org.springframework.richclient.command.CommandGroup;
import org.springframework.richclient.core.Guarded;
import org.springframework.richclient.util.PopupMenuMouseListener;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ApplicationEvent;
import org.springframework.beans.support.PropertyComparator;
import org.springframework.util.Assert;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NonNls;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.Period;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.JPopupMenu;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.table.TableColumnModel;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.util.concurrent.Lock;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.swing.EventSelectionModel;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import java.util.List;
import java.util.Collections;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Arrays;
import java.math.BigDecimal;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import com.cedarsoft.spring.rcp.PropertyPath;
import com.cedarsoft.spring.rcp.table.renderer.EnumTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.PercentageTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.LocalDateTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.LocalTimeTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.DateTimeTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.IntervalTableCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.PeriodCellRenderer;
import com.cedarsoft.spring.rcp.table.renderer.TaggedCellRenderer;
import com.cedarsoft.spring.rcp.events.SelectionEvent;
import com.cedarsoft.spring.rcp.selection.NoElementSelectedException;
import com.cedarsoft.utils.tags.Tagged;
import com.cedarsoft.utils.tags.TagSet;
import com.cedarsoft.ObjectAccess;
/**
* Improved Object table.
*
* @param <T> the type
*/
public abstract class ObjectTable<T> extends AbstractControlFactory implements ApplicationListener {
@NotNull
private final StatusBarSupport statusBarSupport;
@NotNull
@NonNls
private final String modelId;
/**
* Whether this object table should listen to events from spring
*/
protected final boolean handleApplicationEvents;
@NotNull
protected final Class<? super T> type;
@NotNull
private final List<ColumnDefinition> columnDefinitions = new ArrayList<ColumnDefinition>();
/**
* Creates a new object table
*
* @param modelId the model id
* @param type the type
* @param handleApplicationEvents whether this table should handle application events
* @param columnDefinitions the column definitions
*/
protected ObjectTable( @NotNull @NonNls String modelId, @NotNull Class<? super T> type, boolean handleApplicationEvents, @NotNull ColumnDefinition... columnDefinitions ) {
this.modelId = modelId;
this.type = type;
this.statusBarSupport = new StatusBarSupport( modelId );
this.handleApplicationEvents = handleApplicationEvents;
this.columnDefinitions.addAll( Arrays.asList( columnDefinitions ) );
}
@Override
protected JComponent createControl() {
// Contstruct the table model and table to display the data
EventList<T> finalList = getFinalEventList();
model = createTableModel( finalList );
JTable table = getComponentFactory().createTable( model );
table.setSelectionModel( new EventSelectionModel<T>( finalList ) );
table.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );
// Install the sorter
SortedList<T> baseList = getBaseEventList();
tableSorter = installTableSorter( 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;
}
/**
* Returns the created JTable.
*
* @return the table
*/
@NotNull
protected JTable getTable() {
return ( JTable ) getControl();
}
/**
* Returns the column definitions
*
* @return the column definitions
*/
@NotNull
public List<? extends ColumnDefinition> getColumnDefinitions() {
return Collections.unmodifiableList( columnDefinitions );
}
private TableComparatorChooser<T> tableSorter;
/**
* Returns the sorter which is used to sort the content of the table
*
* @return the sorter, null if {@link #getTable()} or {@link #createControl()} is not called before
*/
@Nullable
protected TableComparatorChooser<T> getTableSorter() {
return tableSorter;
}
/**
* Creates the table sorter
*
* @param table the table
* @param sortedList the sorted list
* @return the table sorter
*/
@NotNull
protected TableComparatorChooser<T> installTableSorter( @NotNull JTable table, @NotNull SortedList<T> sortedList ) {
return TableComparatorChooser.install( table, sortedList, isMultipleColumnSort() ? TableComparatorChooser.MULTIPLE_COLUMN_KEYBOARD : TableComparatorChooser.SINGLE_COLUMN );
}
protected boolean isMultipleColumnSort() {
return true;
}
private EventList<T> finalEventList;
/**
* Get the event list to be use for constructing the table model.
*
* @return final event list
*/
@NotNull
public EventList<T> getFinalEventList() {
if ( finalEventList == null ) {
finalEventList = createFinalEventList();
}
//noinspection ReturnOfCollectionOrArrayField
return finalEventList;
}
@NotNull
protected EventList<T> createFinalEventList() {
FinalEventListFactory<T> factory = finalEventListFactory;
if ( factory == null ) {
return getBaseEventList();
}
return factory.create( getBaseEventList() );
}
@Nullable
private FinalEventListFactory<T> finalEventListFactory;
@Nullable
public FinalEventListFactory<T> getFinalEventListFactory() {
return finalEventListFactory;
}
public void setFinalEventListFactory( @Nullable FinalEventListFactory<T> finalEventListFactory ) {
this.finalEventListFactory = finalEventListFactory;
}
private SortedList<T> baseEventList;
/**
* Get the base event list for the table model. This can be used to build layered event models for filtering.
*
* @return base event list
*/
@NotNull
public SortedList<T> getBaseEventList() {
if ( baseEventList == null ) {
// Construct on demand
List<? extends T> data = getInitialData();
if ( logger.isDebugEnabled() ) {
logger.debug( "Table data: got " + data.size() + " entries" );
}
// Construct the event list of all our data and layer on the sorting
EventList<T> rawList = GlazedLists.eventList( data );
int initialSortColumn = getInitialSortColumn();
if ( initialSortColumn >= 0 ) {
PropertyPath sortProperty = getColumnDefinition( initialSortColumn ).getPropertyPath();
//noinspection unchecked
baseEventList = new SortedList<T>( rawList, new PropertyComparator( sortProperty.getProperty(), false, true ) );
} else {
baseEventList = new SortedList<T>( rawList );
}
}
//noinspection ReturnOfCollectionOrArrayField
return baseEventList;
}
/**
* Returns the column definition for the given column
*
* @param column the column
* @return the column definition
*/
@NotNull
public ColumnDefinition getColumnDefinition( int column ) {
return columnDefinitions.get( column );
}
/**
* Get the default sort column. Defaults to 0.
*
* @return column to sort on
*/
protected int getInitialSortColumn() {
return 0;
}
protected void updateStatusBar() {
int all = getBaseEventList().size();
int showing = getFinalEventList().size();
int selectedRow = getTable().getSelectedRowCount();
statusBarSupport.updateStatusBar( all, showing, selectedRow );
}
/**
* Returns the initial data
*
* @return the initial data
*/
@NotNull
protected abstract List<? extends T> getInitialData();
/**
* Selets the given object
*
* @param object the object that is selected
*/
public void selectObject( @NotNull T object ) {
try {
getSelectionModel().setValueIsAdjusting( true );
GlazedTableModel tableModel = getTableModel();
int rowCount = tableModel.getRowCount();
for ( int i = 0; i < rowCount; i++ ) {
Object rowObject = tableModel.getElementAt( i );
//noinspection ObjectEquality
if ( rowObject == object ) {
getSelectionModel().setSelectionInterval( i, i );
TableUtils.scrollToRow( getTable(), i );
return;
}
}
} finally {
getSelectionModel().setValueIsAdjusting( false );
}
}
@NotNull
protected GlazedTableModel createTableModel( @NotNull EventList<T> eventList ) {
return new ExtendedTableModel( eventList );
}
/**
* @param event the event
* @return wether the application events should be handled
*/
protected boolean shouldHandleEvent( @NotNull LifecycleApplicationEvent event ) {
return handleApplicationEvents;
}
/**
* Handle the addition of new object.
*
* @param objects the new objects to handle
*/
protected void handleNewObjects( final Collection<? extends T> objects ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
getFinalEventList().addAll( objects );
}
} );
getControl().repaint();
}
protected void handleUpdatedObjects( final Collection<? extends T> objects ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
for ( T object : objects ) {
int index = getFinalEventList().indexOf( object );
if ( index >= 0 ) {
getFinalEventList().set( index, object );
}
}
}
} );
getControl().repaint();
}
protected void handleDeletedObjects( final Collection<? extends T> objects ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
getFinalEventList().removeAll( objects );
}
} );
getControl().repaint();
}
/**
* Returns whether this object is selected
*
* @param object the object
* @return true if this object is selected, false otherwise
*/
public boolean isSelected( @NotNull T object ) {
return getSelectedObjects().contains( object );
}
/**
* Returns the (first) selected object.
*
* @return the selected object.
*
* @throws IllegalStateException if no element is selected
* @throws NoElementSelectedException
*/
@NotNull
public T getSelectedObject() throws NoElementSelectedException {
List<? extends T> selectedElements = getSelectedObjects();
if ( selectedElements.isEmpty() ) {
throw new NoElementSelectedException();
}
return selectedElements.get( 0 );
}
/**
* Returns the selected objects
*
* @return the selected objects
*/
@NotNull
public List<? extends T> getSelectedObjects() {
List<T> selected = new ArrayList<T>();
for ( int selectedIndicy : getTable().getSelectedRows() ) {
//noinspection unchecked
selected.add( ( T ) getTableModel().getElementAt( selectedIndicy ) );
}
return selected;
}
protected void configureTable( @NotNull final JTable table ) {
TableColumnModel columnModel = table.getColumnModel();
//Register the renderers
registerDefaultRenderers( table );
//Apply the column definitions
for ( int i = 0; i < columnDefinitions.size(); i++ ) {
ColumnDefinition columnDefinition = columnDefinitions.get( i );
columnDefinition.apply( columnModel.getColumn( i ) );
}
//call the configurers
for ( TableConfigurer tableConfigurer : tableConfigurers ) {
tableConfigurer.configure( table );
}
//Publish changes to the selection model
final EventSelectionModel<T> selectionModel = ( EventSelectionModel<T> ) table.getSelectionModel();
selectionModel.getSelected().addListEventListener( new ListEventListener<T>() {
@Override
public void listChanged( @NotNull ListEvent<T> listChanges ) {
//Iterate two times - first unselect, then select
//First unselect
while ( listChanges.next() ) {
if ( listChanges.getType() == ListEvent.DELETE ) {
getApplicationContext().publishEvent( new SelectionEvent( SelectionEvent.Type.UNSELECT, listChanges.getOldValue() ) );
}
}
listChanges.reset();
//Now select
while ( listChanges.next() ) {
if ( listChanges.getType() == ListEvent.INSERT ) {
getApplicationContext().publishEvent( new SelectionEvent( SelectionEvent.Type.SELECT, selectionModel.getSelected().get( listChanges.getIndex() ) ) );
}
}
}
} );
}
@NotNull
private final List<TableConfigurer> tableConfigurers = new ArrayList<TableConfigurer>();
@NotNull
public List<? extends TableConfigurer> getTableConfigurers() {
return Collections.unmodifiableList( tableConfigurers );
}
public void addTableConfigurer( @NotNull TableConfigurer tableConfigurer ) {
this.tableConfigurers.add( tableConfigurer );
}
/**
* Get the selection model.
*
* @return selection model
*/
public EventSelectionModel<T> getSelectionModel() {
//noinspection unchecked
return ( EventSelectionModel<T> ) getTable().getSelectionModel();
}
/**
* Registers the renderes
*
* @param table the table the renderers should be registered at
*/
protected void registerDefaultRenderers( @NotNull JTable table ) {
table.setDefaultRenderer( Enum.class, new EnumTableCellRenderer() );
table.setDefaultRenderer( BigDecimal.class, new PercentageTableCellRenderer() );
table.setDefaultRenderer( LocalDate.class, new LocalDateTableCellRenderer() );
table.setDefaultRenderer( LocalTime.class, new LocalTimeTableCellRenderer() );
table.setDefaultRenderer( DateTime.class, new DateTimeTableCellRenderer( true ) );
table.setDefaultRenderer( Interval.class, new IntervalTableCellRenderer() );
table.setDefaultRenderer( Period.class, new PeriodCellRenderer() );
table.setDefaultRenderer( Tagged.class, new TaggedCellRenderer() );
table.setDefaultRenderer( TagSet.class, new TaggedCellRenderer() );
}
/**
* Extended table model
*/
private class ExtendedTableModel extends GlazedTableModel {
private ExtendedTableModel( @NotNull EventList<T> eventList ) {
super( eventList, ColumnDefinition.extractColumnProperties( getColumnDefinitions() ), ObjectTable.this.getModelId() );
}
/**
* @noinspection RefusedBequest
*/
@Override
protected TableFormat<?> createTableFormat() {
DefaultAdvancedTableFormat format = new DefaultAdvancedTableFormat();
applyComparators( format );
return format;
}
private void applyComparators( @NotNull DefaultAdvancedTableFormat format ) {
//Set the comparators
for ( int i = 0; i < columnDefinitions.size(); i++ ) {
ColumnDefinition definition = columnDefinitions.get( i );
Comparator<?> comparator = definition.getComparator();
if ( comparator != null ) {
format.setComparator( i, comparator );
}
}
}
@Override
protected String[] createColumnNames( @NotNull String[] propertyColumnNames ) {
String[] copy = propertyColumnNames.clone();
for ( int i = 0; i < columnDefinitions.size(); i++ ) {
ColumnDefinition definition = columnDefinitions.get( i );
String override = definition.getColumnNameOverride();
if ( override != null ) {
copy[i] = override;
}
}
return super.createColumnNames( copy );
}
}
/**
* @return the modelId
*/
@NotNull
@NonNls
public String getModelId() {
return modelId;
}
@Nullable
private GlazedTableModel model;
/**
* Get the data model for the table.
* <p/>
* <em>Note:</em> This method returns null unless {@link #getTable()} or {@link #createTable()} is called
*
* @return model the table model which is used for the table
*/
@NotNull
public GlazedTableModel getTableModel() {
if ( model == null ) {
throw new IllegalArgumentException( "No model available yet" );
}
return model;
}
/**
* 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.statusBarSupport.setCurrentStatusBar( statusBar );
updateStatusBar();
}
@Nullable
private ActionCommandExecutor doubleClickHandler;
/**
* @return the doubleClickHandler
*/
@Nullable
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( @Nullable ActionCommandExecutor doubleClickHandler ) {
this.doubleClickHandler = doubleClickHandler;
}
/**
* 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
ActionCommandExecutor handler = doubleClickHandler;
if ( handler == null ) {
return;
}
//May execute an guarded command executor
if ( handler instanceof Guarded ) {
if ( !( ( Guarded ) handler ).isEnabled() ) {
return;
}
}
handler.execute();
}
@Nullable
private CommandGroup popupCommandGroup;
public void setPopupCommandGroup( @Nullable CommandGroup popupCommandGroup ) {
this.popupCommandGroup = popupCommandGroup;
}
/**
* @return the popupCommandGroup
*/
@Nullable
public CommandGroup getPopupCommandGroup() {
return popupCommandGroup;
}
/**
* 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
*/
@Nullable
protected JPopupMenu createPopupContextMenu() {
CommandGroup commandGroup = getPopupCommandGroup();
if ( commandGroup == null ) {
return null;
}
return commandGroup.createPopupMenu();
}
/**
* 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
*/
@Nullable
protected JPopupMenu createPopupContextMenu( MouseEvent e ) {
return createPopupContextMenu();
}
/**
* Handle the creation of a new object.
*
* @param object New object to handle
*/
protected void handleNewObject( @NotNull final T object ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
getFinalEventList().add( object );
}
} );
getControl().repaint();
}
/**
* 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( @NotNull final T object ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
int index = getFinalEventList().indexOf( object );
if ( index >= 0 ) {
getFinalEventList().set( index, object );
}
}
} );
getControl().repaint();
}
/**
* 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( @NotNull final T object ) {
runWithWriteLock( new Runnable() {
@Override
public void run() {
int index = getFinalEventList().indexOf( object );
if ( index >= 0 ) {
getFinalEventList().remove( index );
}
}
} );
getControl().repaint();
}
/**
* Handle an application event. This will notify us of object adds, deletes, and modifications. Update our table
* model accordingly.
*
* @param event event to process
*/
@Override
public void onApplicationEvent( ApplicationEvent event ) {
if ( !( event instanceof LifecycleApplicationEvent ) ) {
return;
}
LifecycleApplicationEvent lifecycleApplicationEvent = ( LifecycleApplicationEvent ) event;
if ( !shouldHandleEvent( lifecycleApplicationEvent ) ) {
return;
}
if ( !type.isAssignableFrom( lifecycleApplicationEvent.getObject().getClass() ) ) {
return;
}
T eventObject = ( T ) lifecycleApplicationEvent.getObject();
if ( LifecycleApplicationEvent.CREATED.equals( lifecycleApplicationEvent.getEventType() ) ) {
handleNewObject( eventObject );
} else if ( LifecycleApplicationEvent.MODIFIED.equals( lifecycleApplicationEvent.getEventType() ) ) {
handleUpdatedObject( eventObject );
} else if ( LifecycleApplicationEvent.DELETED.equals( lifecycleApplicationEvent.getEventType() ) ) {
handleDeletedObject( eventObject );
}
}
/**
* 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( @NotNull 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( @NotNull Runnable runnable ) {
runWithLock( runnable, getFinalEventList().getReadWriteLock().readLock() );
}
final class StatusBarUpdateListener implements ListSelectionListener, ListEventListener<T> {
@Override
public void valueChanged( ListSelectionEvent e ) {
updateStatusBar();
}
@Override
public void listChanged( ListEvent<T> listChanges ) {
updateStatusBar();
}
}
final class ContextPopupMenuListener extends PopupMenuMouseListener {
/**
* @noinspection RefusedBequest
*/
@Nullable
@Override
protected JPopupMenu getPopupMenu( MouseEvent e ) {
return createPopupContextMenu( e );
}
}
final class DoubleClickListener extends MouseAdapter {
@Override
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.
*/
@Override
public void mouseClicked( MouseEvent e ) {
// If the user double clicked on a row, then call onDoubleClick
if ( e.getClickCount() == 2 ) {
onDoubleClick();
}
}
}
protected static void runWithLock( @NotNull Runnable runnable, @NotNull Lock lock ) {
Assert.notNull( runnable );
Assert.notNull( lock );
lock.lock();
try {
runnable.run();
}
finally {
lock.unlock();
}
}
}