package name.abuchen.portfolio.ui.util.viewers;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerColumn;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.Widget;
import name.abuchen.portfolio.model.Adaptor;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.ui.PortfolioPlugin;
public final class ColumnViewerSorter
{
/**
* The SortingContext provides comparators access to the original sort
* direction and the currently selected column option. The sort direction is
* typically used to keep an element at a stable position regardless of the
* sort direction (for example a summary line shows up always at the end).
*/
public static final class SortingContext
{
private static final String OPTION = "option"; //$NON-NLS-1$
private static final String DIRECTION = "direction"; //$NON-NLS-1$
private static final ThreadLocal<Map<String, Object>> MAP = new ThreadLocal<Map<String, Object>>()
{
@Override
protected Map<String, Object> initialValue()
{
return new HashMap<>();
}
};
private SortingContext()
{}
/* protected */ static void setSortDirection(int direction)
{
MAP.get().put(DIRECTION, direction);
}
/**
* Returns the original sort direction.
*/
public static int getSortDirection()
{
Object direction = MAP.get().get(DIRECTION);
return direction == null ? SWT.DOWN : (int) direction;
}
/* protected */ static void setOption(Object option)
{
MAP.get().put(OPTION, option);
}
/**
* Returns the currently selected column option.
*/
public static Object getColumnOption()
{
return MAP.get().get(OPTION);
}
/* protected */ static void clear()
{
MAP.get().clear();
}
}
private static final class ChainedComparator implements Comparator<Object>
{
private final List<Comparator<Object>> comparators;
private ChainedComparator(List<Comparator<Object>> comparators)
{
this.comparators = comparators;
}
@Override
public int compare(Object o1, Object o2)
{
for (Comparator<Object> c : comparators)
{
int result = c.compare(o1, o2);
if (result != 0)
return result;
}
return 0;
}
}
private static final class BeanComparator implements Comparator<Object>
{
private final Class<?> clazz;
private final Method method;
private final int type;
private BeanComparator(Class<?> clazz, String attribute)
{
this.clazz = clazz;
this.method = determineReadMethod(clazz, attribute);
Class<?> returnType = method.getReturnType();
if (returnType.equals(Object.class))
type = 0;
else if (returnType.isAssignableFrom(String.class))
type = 1;
else if (returnType.isAssignableFrom(Enum.class))
type = 2;
else if (returnType.isAssignableFrom(Integer.class) || returnType.isAssignableFrom(int.class))
type = 3;
else if (returnType.isAssignableFrom(Double.class) || returnType.isAssignableFrom(double.class))
type = 4;
else if (returnType.isAssignableFrom(Date.class))
type = 5;
else if (returnType.isAssignableFrom(Long.class) || returnType.isAssignableFrom(long.class))
type = 6;
else if (returnType.isAssignableFrom(Boolean.class) || returnType.isAssignableFrom(boolean.class))
type = 7;
else if (returnType.isAssignableFrom(Money.class))
type = 8;
else
type = 0;
}
private Method determineReadMethod(Class<?> clazz, String attribute)
{
String camelCaseAttribute = Character.toUpperCase(attribute.charAt(0)) + attribute.substring(1);
try
{
return clazz.getMethod("get" + camelCaseAttribute); //$NON-NLS-1$
}
catch (NoSuchMethodException e)
{
try
{
return clazz.getMethod("is" + camelCaseAttribute); //$NON-NLS-1$
}
catch (NoSuchMethodException e1)
{
PortfolioPlugin.log(Arrays.asList(e, e1));
throw new IllegalArgumentException();
}
}
}
@Override
public int compare(Object o1, Object o2)
{
Object object1 = Adaptor.adapt(clazz, o1);
Object object2 = Adaptor.adapt(clazz, o2);
if (object1 == null && object2 == null)
return 0;
else if (object1 == null)
return -1;
else if (object2 == null)
return 1;
try
{
Object attribute1 = method.invoke(object1);
Object attribute2 = method.invoke(object2);
if (attribute1 == null && attribute2 == null)
return 0;
else if (attribute1 == null)
return -1;
else if (attribute2 == null)
return 1;
switch (type)
{
case 1:
return ((String) attribute1).compareToIgnoreCase((String) attribute2);
case 2:
return ((Enum<?>) attribute2).name().compareTo(((Enum<?>) attribute2).name());
case 3:
return ((Integer) attribute1).compareTo((Integer) attribute2);
case 4:
return ((Double) attribute1).compareTo((Double) attribute2);
case 5:
return ((Date) attribute1).compareTo((Date) attribute2);
case 6:
return ((Long) attribute1).compareTo((Long) attribute2);
case 7:
return ((Boolean) attribute1).compareTo((Boolean) attribute2);
case 8:
return ((Money) attribute1).compareTo((Money) attribute2);
default:
return String.valueOf(attribute1).compareToIgnoreCase(String.valueOf(attribute2));
}
}
catch (IllegalAccessException | InvocationTargetException e)
{
throw new UnsupportedOperationException(e);
}
}
}
private static final class ValueProviderComparator implements Comparator<Object>
{
private final Function<Object, Comparable<?>> valueProvider;
public ValueProviderComparator(Function<Object, Comparable<?>> valueProvider)
{
this.valueProvider = valueProvider;
}
@Override
public int compare(Object o1, Object o2)
{
if (o1 == null && o2 == null)
return 0;
else if (o1 == null)
return -1;
else if (o2 == null)
return 1;
@SuppressWarnings("unchecked")
Comparable<Object> v1 = (Comparable<Object>) valueProvider.apply(o1);
@SuppressWarnings("unchecked")
Comparable<Object> v2 = (Comparable<Object>) valueProvider.apply(o2);
if (v1 == null && v2 == null)
return 0;
else if (v1 == null)
return -1;
else if (v2 == null)
return 1;
return v1.compareTo(v2);
}
}
private static final class ViewerSorter extends ViewerComparator
{
private ColumnViewer columnViewer;
private ViewerColumn viewerColumn;
private Comparator<Object> comparator;
private int direction = SWT.DOWN;
public ViewerSorter(ColumnViewer columnViewer, ViewerColumn viewerColumn, Comparator<Object> comparator)
{
this.columnViewer = columnViewer;
this.viewerColumn = viewerColumn;
this.comparator = comparator;
Widget widget;
if (viewerColumn instanceof TableViewerColumn)
widget = ((TableViewerColumn) viewerColumn).getColumn();
else if (viewerColumn instanceof TreeViewerColumn)
widget = ((TreeViewerColumn) viewerColumn).getColumn();
else
throw new UnsupportedOperationException();
widget.addListener(SWT.Selection, event -> handleSelectionEvent());
}
private void handleSelectionEvent()
{
// check if current column is already sorted -> switch direction
boolean columnIsCurrentlySorted;
if (viewerColumn instanceof TableViewerColumn)
{
columnIsCurrentlySorted = ((TableViewer) columnViewer).getTable()
.getSortColumn() == ((TableViewerColumn) viewerColumn).getColumn();
}
else if (viewerColumn instanceof TreeViewerColumn)
{
columnIsCurrentlySorted = ((TreeViewer) columnViewer).getTree()
.getSortColumn() == ((TreeViewerColumn) viewerColumn).getColumn();
}
else
{
throw new IllegalArgumentException();
}
if (columnIsCurrentlySorted)
setSorter(direction == SWT.DOWN ? SWT.UP : SWT.DOWN);
else
setSorter(SWT.DOWN);
}
private void setSorter(int direction)
{
this.direction = direction;
if (viewerColumn instanceof TableViewerColumn)
{
TableColumn c = ((TableViewerColumn) viewerColumn).getColumn();
c.getParent().setSortColumn(c);
c.getParent().setSortDirection(direction);
}
else if (viewerColumn instanceof TreeViewerColumn)
{
TreeColumn c = ((TreeViewerColumn) viewerColumn).getColumn();
c.getParent().setSortColumn(c);
c.getParent().setSortDirection(direction);
}
if (columnViewer.getComparator() == this)
columnViewer.refresh();
else
columnViewer.setComparator(this);
}
@Override
public int compare(Viewer viewer, Object element1, Object element2)
{
try
{
setupSortingContext();
int dir = direction == SWT.DOWN ? 1 : -1;
if (element1 == null && element2 == null)
return 0;
else if (element1 == null)
return dir;
else if (element2 == null)
return -dir;
return dir * comparator.compare(element1, element2);
}
finally
{
SortingContext.clear();
}
}
private void setupSortingContext()
{
SortingContext.setSortDirection(direction);
Object option = null;
if (viewerColumn instanceof TableViewerColumn)
option = ((TableViewerColumn) viewerColumn).getColumn().getData(ShowHideColumnHelper.OPTIONS_KEY);
else if (viewerColumn instanceof TreeViewerColumn)
option = ((TreeViewerColumn) viewerColumn).getColumn().getData(ShowHideColumnHelper.OPTIONS_KEY);
SortingContext.setOption(option);
}
}
private Comparator<Object> comparator;
private ColumnViewerSorter(Comparator<Object> comparator)
{
this.comparator = comparator;
}
public static ColumnViewerSorter create(Class<?> clazz, String... attributes)
{
List<Comparator<Object>> comparators = new ArrayList<>();
for (String attribute : attributes)
comparators.add(new BeanComparator(clazz, attribute));
return new ColumnViewerSorter(
comparators.size() == 1 ? comparators.get(0) : new ChainedComparator(comparators));
}
public static ColumnViewerSorter create(Function<Object, Comparable<?>> valueProvider)
{
return create(new ValueProviderComparator(valueProvider));
}
@SuppressWarnings("unchecked")
public static ColumnViewerSorter create(Comparator<? extends Object> comparator)
{
return new ColumnViewerSorter((Comparator<Object>) comparator);
}
public ColumnViewerSorter wrap(Function<Comparator<Object>, Comparator<Object>> provider)
{
this.comparator = provider.apply(this.comparator);
return this;
}
public void attachTo(Column column)
{
column.setSorter(this);
}
public void attachTo(Column column, int direction)
{
column.setSorter(this, direction);
}
public void attachTo(ColumnViewer viewer, ViewerColumn column)
{
attachTo(viewer, column, SWT.NONE);
}
public void attachTo(ColumnViewer viewer, ViewerColumn column, boolean makeDefault)
{
attachTo(viewer, column, makeDefault ? SWT.DOWN : SWT.NONE);
}
public void attachTo(ColumnViewer viewer, ViewerColumn column, int direction)
{
ViewerSorter x = new ViewerSorter(viewer, column, comparator);
if (direction != SWT.NONE)
x.setSorter(direction);
}
}