/******************************************************************************* * Copyright (c) 2007, 2011 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.event.view; import gnu.trove.map.hash.THashMap; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.ui.IMemento; import org.eclipse.ui.IWorkbenchSite; import org.simantics.Simantics; import org.simantics.browsing.ui.BuiltinKeys; import org.simantics.browsing.ui.Column; import org.simantics.browsing.ui.Column.Align; import org.simantics.browsing.ui.GraphExplorer; import org.simantics.browsing.ui.NodeContext; import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey; import org.simantics.browsing.ui.NodeQueryManager; import org.simantics.browsing.ui.PrimitiveQueryUpdater; import org.simantics.browsing.ui.common.EvaluatorData; import org.simantics.browsing.ui.common.EvaluatorData.Evaluator; import org.simantics.browsing.ui.common.comparators.ImmutableLexicalComparable; import org.simantics.browsing.ui.common.processors.AbstractPrimitiveQueryProcessor; import org.simantics.browsing.ui.common.processors.ProcessorLifecycle; import org.simantics.browsing.ui.content.ComparableContext; import org.simantics.browsing.ui.content.ComparableContextFactory; import org.simantics.browsing.ui.content.Labeler; import org.simantics.browsing.ui.swt.widgets.GraphExplorerComposite; import org.simantics.browsing.ui.swt.widgets.impl.WidgetSupport; import org.simantics.databoard.Bindings; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.common.procedure.adapter.DisposableListener; import org.simantics.db.common.request.ObjectsWithType; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.common.uri.UnescapedChildMapOfResource; import org.simantics.db.common.utils.Logger; import org.simantics.db.exception.BindingException; import org.simantics.db.exception.DatabaseException; import org.simantics.db.exception.DoesNotContainValueException; import org.simantics.db.exception.NoSingleResultException; import org.simantics.db.exception.ServiceException; import org.simantics.db.management.ISessionContext; import org.simantics.db.service.QueryControl; import org.simantics.event.ontology.EventResource; import org.simantics.event.util.EventUtils; import org.simantics.operation.Layer0X; import org.simantics.simulation.ontology.SimulationResource; import org.simantics.ui.workbench.IPropertyPage; import org.simantics.utils.datastructures.ArrayMap; import org.simantics.utils.strings.AlphanumComparator; import org.simantics.utils.ui.workbench.StringMemento; import org.simantics.views.swt.SimanticsView; /** * @author Tuukka Lehtonen */ public class EventView extends SimanticsView { private static final String EVENTS_TABLE_MEMENTO_ID = "events"; private static final String BROWSE_CONTEXT = "http://www.simantics.org/Event-1.2/View/EventBrowseContext"; private static final String CONTEXT_MENU_ID = "org.simantics.event.view.popup"; private GraphExplorerComposite eventExplorer; private TableComparatorFactory comparator; @Override protected Set getBrowseContexts() { return Collections.singleton(BROWSE_CONTEXT); } @Override protected String getContextMenuId() { return CONTEXT_MENU_ID; } @Override protected void createControls(Composite body, IWorkbenchSite site, ISessionContext context, WidgetSupport support) { Column[] COLUMNS = new Column[] { //new Column(ColumnKeys.SINGLE, "S", Align.LEFT, 40, "S", false), new Column(Constants.COLUMN_EVENT_INDEX, "#", Align.LEFT, 70, "Event Index", false), new Column(Constants.COLUMN_TIMESTAMP, "Time Stamp", Align.LEFT, 100, "Time Stamp", false), new Column(Constants.COLUMN_MILESTONE, "M", Align.LEFT, 24, "Is Milestone?", false), new Column(Constants.COLUMN_EVENT_TYPE, "Type", Align.LEFT, 110, "Event Type", false), new Column(Constants.COLUMN_RETURNED, "R", Align.LEFT, 24, "Has Event Returned?", false), new Column(Constants.COLUMN_TAG_NAME, "Tag", Align.LEFT, 120, "Tag Name", true, 1), new Column(Constants.COLUMN_MESSAGE, "Message", Align.LEFT, 100, "Message", true, 2), new Column(Constants.COLUMN_RETURN_TIME, "Return Time", Align.LEFT, 100, "Return Event Time Stamp", false), }; final Text headerText = new Text(body, SWT.NONE); final DisposableListener headerTextListener = new DisposableListener() { @Override public void execute(final String result) { Display d = headerText.getDisplay(); if(d.isDisposed()) return; d.asyncExec(new Runnable() { @Override public void run() { if(headerText.isDisposed()) return; headerText.setText(result); headerText.getParent().layout(); } }); } @Override public void exception(Throwable t) { Logger.defaultLogError(t); } }; headerText.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { headerTextListener.dispose(); } }); headerText.setText(""); headerText.setBackground(body.getBackground()); GridDataFactory.fillDefaults().align(SWT.CENTER, SWT.CENTER).grab(true, false).span(2, 1).applyTo(headerText); Simantics.getSession().asyncRequest(new UniqueRead() { public String getChildrenImpl(ReadGraph graph, Resource project, List eventLogs) throws DatabaseException { int discardedEvents = 0; for(Resource log : eventLogs) { Map slices = graph.syncRequest(new UnescapedChildMapOfResource(log)); if(slices.isEmpty()) continue; ArrayList keys = new ArrayList(slices.keySet()); Collections.sort(keys, AlphanumComparator.COMPARATOR); Integer base = Integer.parseInt(keys.get(0)); discardedEvents += EventUtils.SLICE_SIZE * base; } if(discardedEvents > 0) return discardedEvents + " events in chronological order have been automatically discarded from event log"; else return ""; } @Override public String perform(ReadGraph graph) throws DatabaseException { List eventLogs = EventView.getEventLogs(graph); QueryControl qc = graph.getService(QueryControl.class); return getChildrenImpl(qc.getIndependentGraph(graph), Simantics.getProjectResource(), eventLogs); } }, headerTextListener); eventExplorer = new GraphExplorerComposite( ArrayMap.keys("displaySelectors", "displayFilter", "maxChildren").values(false, false, 5000), site, body, support, SWT.FULL_SELECTION | SWT.MULTI) { @Override protected void initializeExplorerWithEvaluator(GraphExplorer explorer, ISessionContext context, EvaluatorData data) { Evaluator eval = data.newEvaluator(); IMemento m = memento; if (comparator != null) { m = new StringMemento(); comparator.saveState(EVENTS_TABLE_MEMENTO_ID, m); } comparator = new TableComparatorFactory(explorer); if (m != null) comparator.restoreState(EVENTS_TABLE_MEMENTO_ID, m); eval.addComparator(comparator, 2.0); data.addEvaluator(Object.class, eval); } }; eventExplorer.setBrowseContexts(getBrowseContexts()); eventExplorer.setContextMenuId(getContextMenuId()); eventExplorer.setColumns(COLUMNS); eventExplorer.finish(); GridDataFactory.fillDefaults().grab(true, true).span(2, 1).applyTo(eventExplorer); attachHeaderListeners(eventExplorer); } public static List getEventLogs(ReadGraph graph) throws NoSingleResultException, DoesNotContainValueException, BindingException, ServiceException, DatabaseException { Layer0X L0X = Layer0X.getInstance(graph); SimulationResource SIMU = SimulationResource.getInstance(graph); EventResource EVENT = EventResource.getInstance(graph); Resource project = Simantics.getProjectResource(); List eventLogs = new ArrayList(); for (Resource activeModel : graph.syncRequest(new ObjectsWithType(project, L0X.Activates, SIMU.Model))) { for (Resource eventLog : graph.syncRequest(new ObjectsWithType(activeModel, EVENT.HasEventLog, EVENT.EventLog))) { if (!graph.hasStatement(eventLog, EVENT.Hidden)) { eventLogs.add(eventLog); // graph.getRelatedValue(eventLog, EVENT.HasModificationCounter, Bindings.INTEGER); } } } return eventLogs; } @Override public void saveState(IMemento memento) { super.saveState(memento); if (comparator != null) comparator.saveState(EVENTS_TABLE_MEMENTO_ID, memento); } private void attachHeaderListeners(GraphExplorerComposite explorer) { final Tree tree = explorer.getExplorerControl(); String sortColumn = null; if (comparator != null) { sortColumn = comparator.getColumn(); } for (final TreeColumn column : tree.getColumns()) { column.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { Column c = (Column) column.getData(); String key = transformColumn( c.getKey() ); comparator.setColumn(key); comparator.toggleAscending(key); comparator.refresh(); // Visualizes the selected sorting in the Tree column header tree.setSortColumn(column); tree.setSortDirection(comparator.isAscending(key) ? SWT.DOWN : SWT.UP); } }); Column c = (Column) column.getData(); String key = transformColumn( c.getKey() ); if (key.equals(sortColumn)) { tree.setSortColumn(column); tree.setSortDirection(comparator.isAscending(key) ? SWT.DOWN : SWT.UP); } } } private Class getColumnType(String key) { if (Constants.COLUMN_EVENT_INDEX.equals(key)) return Long.class; if (Constants.COLUMN_TIMESTAMP_NUMERIC.equals(key) || Constants.COLUMN_RETURN_TIME_NUMERIC.equals(key)) return Double.class; return String.class; } private String transformColumn(String key) { // HACK: for proper numeric timestamp sorting since // formatted time values don't sort all that well. if (Constants.COLUMN_TIMESTAMP.equals(key)) key = Constants.COLUMN_TIMESTAMP_NUMERIC; if (Constants.COLUMN_RETURN_TIME.equals(key)) key = Constants.COLUMN_RETURN_TIME_NUMERIC; return key; } @Override public void setFocus() { eventExplorer.setFocus(); } @Override protected IPropertyPage getPropertyPage() { return null; } public class TableComparatorFactory implements ComparableContextFactory { private static final String SORTING = "sorting"; private final static String SORT_COL = "sortCol"; private final static String COLS = "cols"; private final static String COL_ASCEND = "ascend"; UpdateTrigger updateTrigger = new UpdateTrigger(); /** Current sort column */ private String column = null; /** Ascending state of each column */ private Map ascendMap = new HashMap(); public TableComparatorFactory(GraphExplorer ge) { ge.setPrimitiveProcessor(updateTrigger); } public void saveState(String id, IMemento memento) { if (id == null) throw new NullPointerException("null id"); IMemento m = null; for (IMemento child : memento.getChildren(SORTING)) { if (id.equals(child.getID())) m = child; } if (m == null) m = memento.createChild(SORTING, id); if (getColumn() != null) m.putString(SORT_COL, getColumn()); columns: for (String columnKey : ascendMap.keySet()) { for (IMemento col : m.getChildren(COLS)) { if (columnKey.equals(col.getID())) { col.putBoolean(COL_ASCEND, isAscending(columnKey)); continue columns; } } IMemento col = m.createChild(COLS, columnKey); col.putBoolean(COL_ASCEND, isAscending(columnKey)); } } public void restoreState(String id, IMemento memento) { if (id == null) throw new NullPointerException("null id"); if (!hasState(id, memento)) return; for (IMemento m : memento.getChildren(SORTING)) { if (!id.equals(m.getID())) continue; for (IMemento column : m.getChildren(COLS)) { setAscending( column.getID(), column.getBoolean(COL_ASCEND) ); } setColumn( m.getString(SORT_COL) ); break; } } public boolean hasState(String id, IMemento memento) { for (IMemento m : memento.getChildren(SORTING)) if (id.equals(m.getID())) return true; return false; } public void setAscending(String column, Boolean ascending) { if (ascending == null) ascendMap.remove(column); else ascendMap.put(column, ascending); } public boolean isAscending(String column) { return !Boolean.FALSE.equals(ascendMap.get(column)); } public void setColumn(String column) { this.column = column; } public void toggleAscending(String column) { Boolean ascending = ascendMap.get(column); if (ascending == null) { ascendMap.put(column, Boolean.TRUE); } else { ascendMap.put(column, !ascending); } } public void refresh() { updateTrigger.refresh(); } public String getColumn() { return column; } @Override public ComparableContext[] create(NodeQueryManager manager, NodeContext parent, NodeContext[] children) { // To make this query refreshable. manager.query(parent, UpdateTrigger.KEY); // Don't sort if no sort column is defined if (column == null) return null; final boolean _ascending = isAscending(column); final Class _clazz = getColumnType(column); ComparableContext[] result = new ComparableContext[children.length]; for (int i = 0; i < children.length; i++) { NodeContext child = children[i]; Labeler labeler = manager.query(child, BuiltinKeys.SELECTED_LABELER); int category = (labeler != null) ? labeler.getCategory() : 0; String label = (labeler != null && column != null) ? labeler.getLabels().get(column) : ""; if (label == null) label = ""; result[i] = new ImmutableLexicalComparable(category, label, child) { final boolean ascending = _ascending; final Class clazz = _clazz; @SuppressWarnings("unchecked") @Override public int compareTo(ComparableContext arg0) { ImmutableLexicalComparable other = (ImmutableLexicalComparable) arg0; int catDelta = getCategory() - other.getCategory(); if (!ascending) catDelta = -catDelta; if (catDelta != 0) return catDelta; String label1 = getLabel(); String label2 = other.getLabel(); @SuppressWarnings("rawtypes") Comparator comparator = AlphanumComparator.CASE_INSENSITIVE_COMPARATOR; if (clazz == Double.class) { comparator = DoubleStringComparator.INSTANCE; } else if (clazz == Long.class) { comparator = LongStringComparator.INSTANCE; } return ascending ? comparator.compare(label1, label2) : comparator.compare(label2, label1); } }; } return result; } @Override public String toString() { return "Event Table Sort"; } } private static class LongStringComparator implements Comparator { private static final LongStringComparator INSTANCE = new LongStringComparator(); @Override public int compare(String s1, String s2) { long n1 = parseLong(s1); long n2 = parseLong(s2); return compare(n1, n2); } private long parseLong(String s) { try { return Long.parseLong(s); } catch (NumberFormatException e1) { return Long.MIN_VALUE; } } public static int compare(long x, long y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } } private static class DoubleStringComparator implements Comparator { private static final DoubleStringComparator INSTANCE = new DoubleStringComparator(); @Override public int compare(String s1, String s2) { double n1 = parseDouble(s1); double n2 = parseDouble(s2); return Double.compare(n1, n2); } private double parseDouble(String s) { try { return Double.parseDouble(s); } catch (NumberFormatException e2) { return Double.NaN; } } } public static class UpdateTrigger extends AbstractPrimitiveQueryProcessor implements ProcessorLifecycle { private Double random = Math.random(); private Map contexts = new THashMap(); public static final PrimitiveQueryKey KEY = new PrimitiveQueryKey() { @Override public String toString() { return "TRIGGER"; } }; @Override public Object getIdentifier() { return KEY; } public void refresh() { random = Math.random(); for (Map.Entry entry : contexts.entrySet()) { entry.getValue().scheduleReplace(entry.getKey(), KEY, random); } } @Override public Double query(PrimitiveQueryUpdater updater, NodeContext context, PrimitiveQueryKey key) { this.contexts.put(context, updater); return random; } @Override public String toString() { return "UpdateTrigger"; } @Override public void attached(GraphExplorer explorer) { } @Override public void detached(GraphExplorer explorer) { clear(); } @Override public void clear() { contexts.clear(); } } }