package org.simantics.interop.update.editor; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Stack; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxCellEditor; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.Statement; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.exception.DatabaseException; import org.simantics.db.exception.DoesNotContainValueException; import org.simantics.db.exception.ManyObjectsForFunctionalRelationException; import org.simantics.db.exception.ServiceException; import org.simantics.db.layer0.util.Layer0Utils; import org.simantics.db.request.Read; import org.simantics.interop.test.GraphChanges; import org.simantics.interop.test.GraphComparator; import org.simantics.interop.update.Activator; import org.simantics.interop.update.model.UpdateNode; import org.simantics.interop.update.model.UpdateNode.Status; import org.simantics.interop.update.model.UpdateOp; import org.simantics.interop.update.model.UpdateTree; import org.simantics.interop.utils.TableUtils; import org.simantics.ui.SimanticsUI; import org.simantics.utils.datastructures.Callback; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.ui.ExceptionUtils; /** * Editor for updating models. * * @author Marko Luukkainen * */ public abstract class ModelUpdateEditor extends Composite{ private Composite errorComposite; private CheckboxTreeViewer changeBrowser; private TableViewer changeViewer; private GraphChanges changes; private UpdateTree updateTree; private Button updateAllButton; private Button updateSelectedButton; private LocalResourceManager manager = new LocalResourceManager(JFaceResources.getResources()); private Image checked; private Image unchecked; private Image warning; private Color containsColor; private Color deletedColor; private Color addedColor; private HashSet> selected = new HashSet>(); private HashSet selectedStructure = new HashSet(); private List filters = new ArrayList(); public ModelUpdateEditor(Composite parent) { super(parent,SWT.NONE); checked = manager.createImage(Activator.imageDescriptorFromPlugin("com.famfamfam.silk", "icons/tick.png")); unchecked = manager.createImage(Activator.imageDescriptorFromPlugin("com.famfamfam.silk", "icons/cross.png")); warning = manager.createImage(Activator.imageDescriptorFromPlugin("com.famfamfam.silk", "icons/error.png")); containsColor = new Color(parent.getDisplay(), new RGB(255,255,220)); deletedColor = new Color(parent.getDisplay(), new RGB(255,220,220)); addedColor = new Color(parent.getDisplay(), new RGB(220,255,220)); this.setLayout(new GridLayout(1,false)); errorComposite = new Composite(this, SWT.BORDER); GridData data = new GridData(); data.grabExcessHorizontalSpace = true; data.grabExcessVerticalSpace = false; data.horizontalAlignment = SWT.FILL; data.verticalAlignment = SWT.TOP; errorComposite.setLayoutData(data); errorComposite.setLayout(new GridLayout(2, false)); errorComposite.setVisible(false); // IEditorInput input = getEditorInput(); // if (!(input instanceof UpdateEditorInput)) { // Label label = new Label(composite, SWT.NONE); // label.setText("Unknown input."); // return; // } Composite fillComposite = new Composite(this, SWT.NONE); data = new GridData(); data.grabExcessHorizontalSpace = true; data.grabExcessVerticalSpace = true; data.horizontalAlignment = SWT.FILL; data.verticalAlignment = SWT.FILL; fillComposite.setLayoutData(data); fillComposite.setLayout(new FillLayout(SWT.VERTICAL)); { changeBrowser = new CheckboxTreeViewer(fillComposite,SWT.MULTI|SWT.V_SCROLL|SWT.BORDER|SWT.FULL_SELECTION ); changeBrowser.setContentProvider(new UpdateTreeContentProvider()); changeBrowser.getTree().setHeaderVisible(true); ColumnViewerToolTipSupport.enableFor(changeBrowser); TreeViewerColumn dataColumn = TableUtils.addColumn(changeBrowser, "Data", true, 600); dataColumn.setLabelProvider(new UpdateNodeLabelProvider()); changeBrowser.addCheckStateListener(new ICheckStateListener() { @Override public void checkStateChanged(CheckStateChangedEvent event) { UpdateNode node = (UpdateNode) event.getElement(); if (node.getOp() != null) { node.getOp().select(Boolean.TRUE.equals(event.getChecked())); } refreshChecked(); } }); changeBrowser.addTreeListener(new ITreeViewerListener() { @Override public void treeExpanded(TreeExpansionEvent event) { event.getTreeViewer().getControl().getDisplay().asyncExec(new Runnable() { @Override public void run() { // TreeViewer uses lazy load, checked states must be updated when the tree is expanded. refreshChecked(); } }); } @Override public void treeCollapsed(TreeExpansionEvent event) { } }); changeBrowser.setUseHashlookup(true); } { changeViewer = new TableViewer(fillComposite,SWT.MULTI|SWT.V_SCROLL|SWT.BORDER|SWT.FULL_SELECTION); changeViewer.getTable().setHeaderVisible(true); changeViewer.getTable().setLinesVisible(true); changeViewer.setContentProvider(new ModificationListContentProvider()); changeViewer.setUseHashlookup(true); TableViewerColumn selection = TableUtils.addColumn(changeViewer, getColumntTitle(0), false, false, 20); TableViewerColumn diagram = TableUtils.addColumn(changeViewer, getColumntTitle(1), true, true, 100); TableViewerColumn symbol = TableUtils.addColumn(changeViewer, getColumntTitle(2), true, true, 100); TableViewerColumn property = TableUtils.addColumn(changeViewer, getColumntTitle(3), true, true, 100); TableViewerColumn oldValue = TableUtils.addColumn(changeViewer, getColumntTitle(4), true, true, 100); TableViewerColumn newValue = TableUtils.addColumn(changeViewer, getColumntTitle(5), true, true, 100); diagram.setLabelProvider(getLabelProvider(1)); symbol.setLabelProvider(getLabelProvider(2)); property.setLabelProvider(getLabelProvider(3)); oldValue.setLabelProvider(getLabelProvider(4)); newValue.setLabelProvider(getLabelProvider(5)); selection.setLabelProvider(new SelectionLabelProvider(selected)); selection.getColumn().addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (changes.getModifications().size() > 0) { if (selected.contains(changes.getModifications().get(0))) { for (Pair nr : changes.getModifications()) selected.remove(nr); } else { for (Pair nr : changes.getModifications()) selected.add(nr); } changeViewer.refresh(); } } }); selection.setEditingSupport(new SelectionEditingSupport(changeViewer, selected)); } Composite buttonComposite = new Composite(this, SWT.NONE); data = new GridData(); data.grabExcessHorizontalSpace = true; data.grabExcessVerticalSpace = false; data.horizontalAlignment = SWT.FILL; data.verticalAlignment = SWT.BOTTOM; buttonComposite.setLayoutData(data); buttonComposite.setLayout(new GridLayout(3, false)); Label label = new Label(buttonComposite, SWT.NONE); data = new GridData(); data.grabExcessHorizontalSpace = true; data.grabExcessVerticalSpace = false; data.horizontalAlignment = SWT.FILL; data.verticalAlignment = SWT.CENTER; label.setLayoutData(data); updateAllButton = new Button(buttonComposite, SWT.PUSH); updateAllButton.setText("Update All"); updateAllButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { applyAll(); } }); updateSelectedButton = new Button(buttonComposite, SWT.PUSH); updateSelectedButton.setText("Update Selected"); updateSelectedButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { applySelected(); } }); } protected Session getSession() { return SimanticsUI.getSession(); } protected String getColumntTitle(int i) { switch (i) { case 0: return "!"; case 1: return "Diagram"; case 2: return "Symbol"; case 3: return "Property"; case 4: return "Old Value"; case 5: return "New Value"; default: throw new RuntimeException("Unknown column index" + i); } } protected abstract ColumnLabelProvider getLabelProvider(int i); protected abstract Pair getChanges(Resource r1, Resource r2) throws DatabaseException; protected abstract UpdateTree getUpdateTree(GraphChanges changes) throws DatabaseException; protected void addFilters(List filters) { } public GraphChanges getChanges() { return changes; } public UpdateTree getUpdateTree() { return updateTree; } public CheckboxTreeViewer getChangeBrowser() { return changeBrowser; } public TableViewer getChangeViewer() { return changeViewer; } private void showWarning(String text) { errorComposite.setVisible(true); Label label = new Label(errorComposite, SWT.NONE); label.setImage(warning); label = new Label(errorComposite, SWT.NONE); label.setText(text); //this.setStatusMessage("Update contains structural changes (new or deleted symbols), please create a new model."); this.layout(true); } private List checkStateListeners = new ArrayList<>(); public void addCheckStateListener(ICheckStateListener listener) { checkStateListeners.add(listener); } public void removeCheckStateListener(ICheckStateListener listener) { checkStateListeners.remove(listener); } public void refreshChecked() { Stack nodeStack = new Stack(); nodeStack.push((UpdateNode)updateTree.getRootNode()); while (!nodeStack.isEmpty()) { UpdateNode n = nodeStack.pop(); if (n.getOp() != null) { UpdateOp op = n.getOp(); if (!op.isAdd() && !op.isDelete()) { changeBrowser.setGrayed(n, true); changeBrowser.setChecked(n, true); } else { boolean applied = op.applied(); if (applied) { changeBrowser.setChecked(n, true); changeBrowser.setGrayed(n, true); selectedStructure.remove(n); } else { boolean sel = op.selected(); if (sel) { selectedStructure.add(n); } else { selectedStructure.remove(n); } changeBrowser.setChecked(n, sel); changeBrowser.setGrayed(n, false); } } } else { changeBrowser.setGrayed(n, true); changeBrowser.setChecked(n, true); } for (UpdateNode c : n.getChildren()) { nodeStack.add((UpdateNode)c); } } changeBrowser.refresh(); for (ICheckStateListener l : checkStateListeners) { l.checkStateChanged(new CheckStateChangedEvent(changeBrowser, null, false)); } } public void load(UpdateEditorInput uei) { addFilters(filters); Resource r1 = uei.getR1(); Resource r2 = uei.getR2(); try { Pair result = getChanges(r1,r2); GraphComparator comparator = result.first; if (result.second) showWarning("Structural symbols have been changed. Model update is not able to update these, please create a new model."); comparator.test(getSession()); changes = comparator.getChanges(); changes = getSession().syncRequest(new FilterChangesRead(changes)); updateTree = getUpdateTree(changes); } catch (DatabaseException e) { Text text = new Text(this, SWT.MULTI); text.setText(e.getMessage()); e.printStackTrace(); return; } changeViewer.setInput(changes.getModifications()); changeBrowser.setInput(updateTree); refreshChecked(); } private void applyAll() { updateAllButton.setEnabled(false); updateSelectedButton.setEnabled(false); getSession().asyncRequest(new WriteRequest(){ @Override public void perform(WriteGraph graph) throws DatabaseException { Layer0Utils.addCommentMetadata(graph, "Apply all model updates"); graph.markUndoPoint(); for (Pair mod : changes.getModifications()) { applyLiteralChange(graph, mod); } selected.clear(); changes.getModifications().clear(); updateTree.getUpdateOps().applyAll(graph); Display.getDefault().asyncExec(new Runnable() { @Override public void run() { updateAllButton.setEnabled(true); updateSelectedButton.setEnabled(true); refreshChecked(); changeViewer.refresh(); } }); } }, new Callback() { @Override public void run(DatabaseException parameter) { if (parameter != null) ExceptionUtils.logAndShowError("Cannot update model", parameter); } }); } private void applyLiteralChange(WriteGraph graph, Pair mod) throws DoesNotContainValueException, ServiceException, ManyObjectsForFunctionalRelationException { Resource s = changes.getComparable().getLeft(mod.second.getSubject()); Resource pred = mod.first.getPredicate(); if (graph.hasValue(mod.second.getObject())) { Object value = graph.getValue(mod.second.getObject()); graph.claimLiteral(s, pred, value); } else { graph.deny(s,pred); } } private void applySelected() { updateAllButton.setEnabled(false); updateSelectedButton.setEnabled(false); getSession().asyncRequest(new WriteRequest(){ @Override public void perform(WriteGraph graph) throws DatabaseException { Layer0Utils.addCommentMetadata(graph, "Apply selected model updates"); graph.markUndoPoint(); for (Pair mod : selected) { changes.getModifications().remove(mod); applyLiteralChange(graph, mod); } selected.clear(); updateTree.getUpdateOps().applySelected(graph); Display.getDefault().asyncExec(new Runnable() { @Override public void run() { changeViewer.refresh(); updateAllButton.setEnabled(true); updateSelectedButton.setEnabled(true); refreshChecked(); } }); } }); } /** * Filters changes: * 1. Changes that are not essential for model update (changes that can be found when the models are axcatly the same) * 2. Runs custom filters for value changes. * * @param g * @param changes * @return * @throws DatabaseException */ protected GraphChanges filterChanges(ReadGraph g, GraphChanges changes) throws DatabaseException { List> modifications = new ArrayList>(); for (Pair mod : changes.getModifications()) { boolean accept = true; for (ChangeFilter filter : filters) { if (!filter.accept(g, mod)) { accept = false; break; } } if (accept) modifications.add(mod); } GraphChanges newChanges = new GraphChanges(changes.getResource1(),changes.getResource2(),changes.getDeletions(), changes.getAdditions(), modifications, changes.getComparable()); return newChanges; } public interface ChangeFilter { public boolean accept(ReadGraph g, Pair change) throws DatabaseException; } /** * * Filters floating point value changes (default filter is set filter when the change is less than 1%) * */ protected class FPValueFilter implements ChangeFilter { private double percentage = 0.01; public FPValueFilter() { } public FPValueFilter(double percentage) { if (percentage < 0.0 || percentage > 1.0) throw new IllegalArgumentException("Percentage must be between 0.0 and 1.0."); this.percentage = percentage; } @Override public boolean accept(ReadGraph g, Pair change) throws DatabaseException { //filter floating point values that have less than 1% difference. if (!g.hasValue(change.first.getObject()) || !g.hasValue(change.second.getObject())) return true; Object v1 = g.getValue(change.first.getObject()); Object v2 = g.getValue(change.second.getObject()); if (v1 instanceof Double && v2 instanceof Double) { double d1 = (Double)v1; double d2 = (Double)v2; if (Math.abs(d1-d2) / Math.max(Math.abs(d1), Math.abs(d2)) < percentage) return false; } else if (v1 instanceof Float && v2 instanceof Float) { float d1 = (Float)v1; float d2 = (Float)v2; if (Math.abs(d1-d2) / Math.max(Math.abs(d1), Math.abs(d2)) < percentage) return false; } return true; } } private class FilterChangesRead implements Read { private GraphChanges changes; public FilterChangesRead(GraphChanges changes) { this.changes = changes; } @Override public GraphChanges perform(ReadGraph graph) throws DatabaseException { return filterChanges(graph, changes); } } private class ModificationListContentProvider implements IStructuredContentProvider { @SuppressWarnings("unchecked") @Override public Object[] getElements(Object inputElement) { if (inputElement == null) return null; Collection> coll = (Collection>)inputElement; return coll.toArray(); } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public void dispose() { } } private class UpdateTreeContentProvider implements ITreeContentProvider { @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof UpdateTree) return new Object[]{((UpdateTree)inputElement).getRootNode()}; if (inputElement instanceof UpdateNode) { UpdateNode node = (UpdateNode)inputElement; return node.getChildren().toArray(); } return new Object[0]; } @Override public Object getParent(Object element) { return null; } @Override public Object[] getChildren(Object parentElement) { UpdateNode node = (UpdateNode)parentElement; return node.getChildren().toArray(); } @Override public boolean hasChildren(Object element) { UpdateNode node = (UpdateNode)element; return node.getChildren().size() > 0; } @Override public void dispose() { } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } } private class SelectionLabelProvider extends ColumnLabelProvider { Collection selected; public SelectionLabelProvider(Collection selected) { this.selected = selected; } @Override public String getText(Object element) { return ""; } @Override public Image getImage(Object element) { if (selected.contains(element)) return checked; else return unchecked; } } private class UpdateNodeLabelProvider extends ColumnLabelProvider { @Override public String getText(Object element) { final UpdateNode node = (UpdateNode)element; return node.getLabel(); } @Override public Image getImage(Object element) { final UpdateNode node = (UpdateNode)element; try { ImageDescriptor id = getSession().syncRequest(new Read() { @Override public ImageDescriptor perform(ReadGraph graph) throws DatabaseException { return node.getImage(graph); } }); return manager.createImage(id); } catch (Exception e) { return null; } } @Override public String getToolTipText(Object element) { final UpdateNode node = (UpdateNode)element; if (node.getOp() != null) { return node.getOp().toString(); } else { return null; } } @Override public int getToolTipDisplayDelayTime(Object object) { return 1000; } @Override public int getToolTipTimeDisplayed(Object object) { return 10000; } @Override public Color getBackground(Object element) { final UpdateNode node = (UpdateNode)element; Status status = node.getStatus(); if (status == Status.CONTAINS) return containsColor; if (status == Status.DELETED) return deletedColor; if (status == Status.NEW) return addedColor; return null; } } private class SelectionEditingSupport extends EditingSupport { @SuppressWarnings("rawtypes") Collection selected; @SuppressWarnings("rawtypes") public SelectionEditingSupport(ColumnViewer viewer, Collection selected) { super(viewer); this.selected = selected; } @Override protected boolean canEdit(Object element) { return true; } @Override protected CellEditor getCellEditor(Object element) { return new CheckboxCellEditor(null, SWT.CHECK); } @Override protected Object getValue(Object element) { return selected.contains(element); } @SuppressWarnings("unchecked") @Override protected void setValue(Object element, Object value) { if (Boolean.TRUE.equals(value)) selected.add(element); else selected.remove(element); getViewer().refresh(element); } } }