/******************************************************************************* * Copyright (c) 2007, 2010 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.spreadsheet.graph; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.e4.ui.di.UISynchronize; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.ui.PlatformUI; import org.simantics.Simantics; import org.simantics.databoard.Bindings; import org.simantics.databoard.binding.Binding; import org.simantics.databoard.binding.mutable.MutableVariant; import org.simantics.databoard.binding.mutable.Variant; import org.simantics.db.AsyncReadGraph; import org.simantics.db.ReadGraph; import org.simantics.db.RequestProcessor; import org.simantics.db.Resource; import org.simantics.db.WriteGraph; import org.simantics.db.common.procedure.adapter.AsyncListenerSupport; import org.simantics.db.common.procedure.adapter.ListenerSupport; import org.simantics.db.common.procedure.adapter.SyncListenerSupport; import org.simantics.db.common.procedure.adapter.TransientCacheAsyncListener; import org.simantics.db.common.procedure.single.SingleSetSyncListenerDelegate; import org.simantics.db.common.request.ReadRequest; import org.simantics.db.common.request.ResourceRead; import org.simantics.db.common.request.UnaryRead; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.request.WriteResultRequest; import org.simantics.db.common.session.SessionEventListenerAdapter; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.StandardRealm; import org.simantics.db.layer0.request.PossibleURIVariable; import org.simantics.db.layer0.request.VariableName; import org.simantics.db.layer0.request.VariableRead; import org.simantics.db.layer0.variable.ProxyVariables; import org.simantics.db.layer0.variable.Variable; import org.simantics.db.layer0.variable.Variables; import org.simantics.db.procedure.SyncListener; import org.simantics.db.request.Write; import org.simantics.db.service.SessionEventSupport; import org.simantics.layer0.Layer0; import org.simantics.spreadsheet.Adaptable; import org.simantics.spreadsheet.CellEditor; import org.simantics.spreadsheet.ClientModel; import org.simantics.spreadsheet.ClientModel.OperationMode; import org.simantics.spreadsheet.SheetCommands; import org.simantics.spreadsheet.event.model.RemoveCellHandler; import org.simantics.spreadsheet.resource.SpreadsheetResource; import org.simantics.ui.selection.WorkbenchSelectionUtils; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.strings.AlphanumComparator; import org.simantics.utils.threads.logger.ITask; import org.simantics.utils.threads.logger.ThreadLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TObjectIntHashMap; class FilteredVariableProperties extends UnaryRead>> { final static String CLASSIFICATION = SpreadsheetResource.URIs.Attribute; public FilteredVariableProperties(Variable variable) { super(variable); } @Override public Collection> perform(ReadGraph graph) throws DatabaseException { ArrayList> result = new ArrayList>(); for(Variable var : parameter.getProperties(graph, CLASSIFICATION)) { String name = var.getName(graph); String uri = var.getURI(graph); result.add(Pair.make(name, var)); Variable expression = var.getPossibleProperty(graph, "expression"); if(expression != null) result.add(Pair.make(name + "#expression", expression)); Variable editable = var.getPossibleProperty(graph, "editable"); if(editable != null) result.add(Pair.make(name + "#editable", editable)); } return result; } } public class GraphUI implements Adaptable, ListenerSupport, AsyncListenerSupport, SyncListenerSupport { private static final Logger LOGGER = LoggerFactory.getLogger(GraphUI.class); final public static boolean DEBUG = false; final private RequestProcessor processor; private CellEditor cellEditor; private Variable run; private ClientModel client; private Map listenerCache = new THashMap<>(); public GraphUI(RequestProcessor processor) { this.processor = processor; } public void addCell(ReadGraph graph, Pair child, final ClientModel client) throws DatabaseException { if(DEBUG) System.out.println("GraphUI adds cell " + child.second.getURI(graph)); final String childName = child.second.getName(graph); Boolean immutable = child.second.getPossiblePropertyValue(graph, "immutable", Bindings.BOOLEAN); if(immutable != null && immutable) { Collection properties = child.second.getProperties(graph, FilteredVariableProperties.CLASSIFICATION); addProperties(graph, properties, client, childName); } else { PropertyListener listener = listenerCache.get(child.first); if (listener == null) { listener = propertyListener(client, childName); listenerCache.put(child.first, listener); } graph.asyncRequest(new FilteredVariableProperties(child.second), listener); } } public void removeCell(ReadGraph graph, Pair child, final ClientModel client) throws DatabaseException { if(DEBUG) System.out.println("GraphUI removed cell " + child.first); client.clear(child.first); PropertyListener listener = listenerCache.remove(child.first); if (listener != null) listener.dispose(); } public void loadCells(ReadGraph graph, Variable container, boolean immutable, final ClientModel client) throws DatabaseException { if(DEBUG) System.out.println("GraphUI loads cells from " + container.getURI(graph)); if(immutable) { for(Pair cell : graph.syncRequest(new Cells(container), TransientCacheAsyncListener.>>instance())) { addCell(graph, cell, client); } } else { graph.syncRequest(new Cells(container), new SingleSetSyncListenerDelegate>(GraphUI.this) { @Override public void add(ReadGraph graph, final Pair child) throws DatabaseException { addCell(graph, child, client); } @Override public void remove(ReadGraph graph, final Pair child) throws DatabaseException { removeCell(graph, child, client); } }); } } private SessionEventListenerAdapter listener; private String currentSource; private boolean disposed; public Resource load(final Variable variable, final ClientModel client) throws DatabaseException { // for (PropertyListener listener : listenerCache.values()) // listener.dispose(); // // listenerCache.clear(); assert(variable != null); this.run = variable; this.client = client; SessionEventSupport support = processor.getService(SessionEventSupport.class); for (PropertyListener listener : listenerCache.values()) { listener.dispose(); } listenerCache.clear(); if(listener != null) support.removeListener(listener); listener = new SessionEventListenerAdapter() { @Override public void writeTransactionFinished() { client.flush(); } }; support.addListener(listener); this.cellEditor = processor.sync(new VariableRead>(variable) { @Override public CellEditor perform(ReadGraph graph) throws DatabaseException { SpreadsheetResource SHEET = SpreadsheetResource.getInstance(graph); return variable.getPropertyValue(graph, SHEET.cellEditor); } }); final ITask task = ThreadLogger.getInstance().begin("GraphUI.init"); client.clearAll(); Map sources = processor.syncRequest(new Sources(variable)); List sheetList = processor.syncRequest(new Sheets(variable)); String currentSheet = processor.syncRequest(new VariableName(variable)); Map stateList = processor.syncRequest(new SpreadsheetStates(variable)); if(currentSource == null) currentSource = "Sheet"; ArrayList sourceList = new ArrayList(sources.keySet()); Collections.sort(sourceList, AlphanumComparator.CASE_INSENSITIVE_COMPARATOR); if(!sourceList.contains(currentSource)) sourceList.add(currentSource); client.setProperty(ClientModel.SOURCES, ClientModel.SOURCES_AVAILABLE, sourceList.toArray(new String[sourceList.size()])); client.setProperty(ClientModel.SOURCES, ClientModel.SOURCES_CURRENT, currentSource); client.setProperty(ClientModel.SHEETS, ClientModel.SHEETS_AVAILABLE, sheetList.toArray(new String[sheetList.size()])); client.setProperty(ClientModel.SHEETS, ClientModel.SHEETS_CURRENT, currentSheet); client.setProperty(ClientModel.STATES, ClientModel.STATES_AVAILABLE, stateList.keySet().toArray(new String[stateList.size()])); client.setProperty(ClientModel.CONTEXT, ClientModel.CONTEXT_CURRENT, variable); client.setProperty(ClientModel.MODE, ClientModel.MODE_CURRENT, OperationMode.OPERATION); String currentState = processor.syncRequest(new UniqueRead() { @Override public String perform(ReadGraph graph) throws DatabaseException { Resource book = variable.getParent(graph).getRepresents(graph); Resource ic = graph.getPossibleObject(book, SpreadsheetResource.getInstance(graph).Book_HasDefaultInitialCondition); if (ic == null) return ""; return graph.getRelatedValue2(ic, Layer0.getInstance(graph).HasName, Bindings.STRING); } }); client.setProperty(ClientModel.STATES, ClientModel.STATES_CURRENT, currentState); processor.syncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { loadCells(graph, variable, false, client); graph.syncRequest(new Ranges(variable), new SingleSetSyncListenerDelegate(GraphUI.this) { @Override public void add(ReadGraph graph, final Variable range) throws DatabaseException { if(DEBUG) System.out.println("GraphUI adds range " + range.getURI(graph)); Boolean immutable = range.getPossiblePropertyValue(graph, "immutable", Bindings.BOOLEAN); loadCells(graph, range, immutable != null && immutable, client); } @Override public void remove(ReadGraph graph, final Variable range) throws DatabaseException { } }); graph.syncRequest(new SheetLines(variable), new SingleSetSyncListenerDelegate(GraphUI.this) { @Override public void add(ReadGraph graph, final Variable range) throws DatabaseException { if(DEBUG) System.out.println("GraphUI adds line " + range.getURI(graph)); Boolean immutable = range.getPossiblePropertyValue(graph, "immutable", Bindings.BOOLEAN); loadCells(graph, range, immutable != null && immutable, client); } @Override public void remove(ReadGraph graph, final Variable range) throws DatabaseException { } }); } // @Override // public void remove(ReadGraph graph, Variable child) throws DatabaseException { // // String location = locations.get(cellResource); // assert(location != null); // // client.setProperty(location, "Label", null); // client.setProperty(location, "Expression", null); // // } }); task.finish(); client.flush(); return null; } private static class PropertyListener extends SingleSetSyncListenerDelegate> { private static final Logger LOGGER = LoggerFactory.getLogger(PropertyListener.class); private ClientModel client; private String childName; private boolean listenerDisposed; public PropertyListener(AsyncListenerSupport support, ClientModel client, String childName) { super(support); this.client = client; this.childName = childName; } @Override public void add(ReadGraph graph, final Pair property) throws DatabaseException { if(DEBUG) System.out.println("GraphUI adds property " + property.second.getURI(graph)); graph.asyncRequest(new CellValue(property.second), new SyncListener() { @Override public void execute(ReadGraph graph, final Object value) throws DatabaseException { String propertyName = property.first; if(DEBUG) System.out.println("GraphUI detected content change(1) at " + childName + " - " + propertyName + " -> " + value); client.setProperty(childName, propertyName, value); } @Override public void exception(ReadGraph graph, Throwable throwable) throws DatabaseException { LOGGER.error("PropertyListener.exception", throwable); String propertyName = property.first; if("content".equals(propertyName)) { if(throwable == null) throwable = new Exception(); String message = throwable.getMessage(); if(message == null) message = throwable.toString(); client.setProperty(childName, propertyName, Variant.ofInstance(message)); } else { client.setProperty(childName, propertyName, null); } } @Override public boolean isDisposed() { return listenerDisposed; } }); } public void dispose() { listenerDisposed = true; } @Override public String toString() { return super.toString() + ":" + childName; } } private PropertyListener propertyListener(final ClientModel client, final String childName) { return new PropertyListener(this, client, childName); } private void addProperties(ReadGraph graph, final Collection properties, final ClientModel client, final String childName) throws DatabaseException { for(Variable property : properties) { if(DEBUG) System.out.println("GraphUI adds immutable property " + property.getURI(graph)); final String propertyName = property.getName(graph); Object value = property.getValue(graph); if(DEBUG) System.out.println("GraphUI detected change at " + childName + " - " + propertyName + " -> " + value); client.setProperty(childName, propertyName, value); String expression = property.getPossiblePropertyValue(graph, "expression", Bindings.STRING); if(expression != null) { if(DEBUG) System.out.println("GraphUI detected change at " + childName + " - " + (propertyName + "#expression") + " -> " + value); client.setProperty(childName, propertyName + "#expression", expression); } Boolean editable = property.getPossiblePropertyValue(graph, "editable", Bindings.STRING); if(editable != null) { if(DEBUG) System.out.println("GraphUI detected change at " + childName + " - " + (propertyName + "#editable") + " -> " + value); client.setProperty(childName, propertyName + "#editable", editable); } } } @SuppressWarnings("unchecked") @Override public T getAdapter(Class clazz) { if(Variable.class == clazz) { return (T)run; } else if(RemoveCellHandler.class == clazz) { return (T) new RemoveCellHandler() { @Override public void handle(final String location) { processor.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { Variable cellVariable = run.getPossibleChild(graph, location); if(cellVariable != null) { final Resource config = cellVariable.getPossiblePropertyValue(graph, "Represents"); if(config != null) { graph.asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { Layer0 l0 = Layer0.getInstance(graph); // SpreadsheetResource sr = SpreadsheetResource.getInstance(graph); graph.deny(config, l0.PartOf); // graph.deny(config, sr.RowOf); // graph.deny(config, sr.ColumnOf); } }); } } } }); } }; } else if(CellEditor.class == clazz) { return (T)new CellEditor() { @Override public void edit(Transaction transaction, String location, String property, E value, Binding binding, Consumer callback) { if (ClientModel.ITERATION_ENABLED.equals(location)) { Simantics.getSession().asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { getBook(graph).setIterationEnabled((boolean)value); } }); return; } if (ClientModel.MODE.equals(location)) { if (ClientModel.MODE_CURRENT.equals(property)) { client.setProperty(location, property, value); client.flush(); return; } } if (ClientModel.CONTEXT.equals(location)) { if(ClientModel.CONTEXT_CURRENT.equals(property)) { if(value instanceof String) { try { Variable newContext = processor.syncRequest(new UnaryRead((String)value) { @Override public Variable perform(ReadGraph graph) throws DatabaseException { String sheetName = run.getName(graph); Variable book = Variables.getContext(graph, run); Resource bookResource = book.getRepresents(graph); Variable input = Variables.getVariable(graph, parameter); Variable proxy = ProxyVariables.makeProxyVariable(graph, Variables.getVariable(graph, bookResource), input); return proxy.getChild(graph, sheetName); // return variable.getParent(graph).getChild(graph, parameter); } }); load(newContext, client); return; } catch (DatabaseException e) { LOGGER.error("edit failed for model key '" + ClientModel.CONTEXT_CURRENT + "'", e); } } } } if(ClientModel.SHEETS.equals(location)) { if(ClientModel.SHEETS_CURRENT.equals(property)) { if(value instanceof String) { try { Variable newInput = processor.syncRequest(new UnaryRead((String)value) { @Override public Variable perform(ReadGraph graph) throws DatabaseException { return run.getParent(graph).getChild(graph, parameter); } }); load(newInput, client); return; } catch (DatabaseException e) { LOGGER.error("edit failed for model key '" + ClientModel.SHEETS_CURRENT + "'", e); } } } } if(ClientModel.STATES.equals(location)) { if(ClientModel.STATES_CURRENT.equals(property)) { if(value instanceof String) { final String parameter = (String) value; try { String uri = processor.syncRequest(new WriteResultRequest() { @Override public String perform(WriteGraph graph) throws DatabaseException { Map states = graph.syncRequest(new SpreadsheetStates(run)); Resource state = null; for (Map.Entry entry : states.entrySet()) { if (entry.getKey().equals(parameter)) { state = entry.getValue(); break; } } if (state != null) { Variable context = Variables.getContext(graph, run); Resource bookResource = context.getRepresents(graph); SpreadsheetGraphUtils.setDefaultInitialConditionForBook(graph, bookResource, state); String contextURI = context.getURI(graph); String sessionName = context.getParent(graph).getURI(graph); SpreadsheetSessionManager.getInstance().removeRealm(sessionName); SpreadsheetSessionManager.getInstance().getOrCreateRealm(graph, sessionName); } return run.getURI(graph); } }); Variable newInput = processor.syncRequest(new PossibleURIVariable(uri)); load(newInput, client); // fullSynchronize(); return; } catch (DatabaseException e) { LOGGER.error("edit failed for model key '" + ClientModel.STATES_CURRENT + "'", e); } } } } if(ClientModel.SOURCES.equals(location)) { if(ClientModel.SOURCES_CURRENT.equals(property)) { try { Resource res = WorkbenchSelectionUtils.getPossibleResource(value); if(res != null) { Variable newInput = processor.syncRequest(new ResourceRead(res) { @Override public Variable perform(ReadGraph graph) throws DatabaseException { Variable base = ProxyVariables.proxyVariableBase(graph, run); Variable in = Variables.getVariable(graph, resource); currentSource = in.getURI(graph); return ProxyVariables.makeProxyVariable(graph, base, in); } }); load(newInput, client); return; } else if(value instanceof String) { Variable newInput = processor.syncRequest(new UnaryRead((String)value) { @Override public Variable perform(ReadGraph graph) throws DatabaseException { Variable base = ProxyVariables.proxyVariableBase(graph, run); Map sources = graph.syncRequest(new Sources(base)); Variable found = sources.get(parameter); if(found == null) return null; currentSource = parameter; return ProxyVariables.makeProxyVariable(graph, base, found); } }); load(newInput, client); return; } } catch (DatabaseException e) { LOGGER.error("edit failed for model key '" + ClientModel.SOURCES_CURRENT + "'", e); } } return; } boolean needsCommit = false; if (transaction == null) { OperationMode mode = client.getPropertyAt(ClientModel.MODE, ClientModel.MODE_CURRENT); transaction = startTransaction(mode); // if (mode.equals(OperationMode.OPERATION)) transaction.setContext(run); needsCommit = true; } final Transaction finalTransaction = transaction; cellEditor.edit(transaction, location, property, value, binding, new Consumer() { @Override public void accept(Object param) { if (finalTransaction.needSynchronization() != null) synchronize(finalTransaction.needSynchronization()); } }); if (needsCommit) transaction.commit(); } @Override public void edit(Transaction transaction, String location, Variant variant, Consumer callback) { boolean needsCommit = false; if (transaction == null) { OperationMode mode = client.getPropertyAt(ClientModel.MODE, ClientModel.MODE_CURRENT); transaction = startTransaction(mode); // if (mode.equals(OperationMode.OPERATION)) transaction.setContext(run); needsCommit = true; } final Transaction finalTransaction = transaction; cellEditor.edit(transaction, location, variant, new Consumer() { @Override public void accept(Object param) { if (finalTransaction.needSynchronization() != null) synchronize(finalTransaction.needSynchronization()); } }); if (needsCommit) transaction.commit(); } @Override public void copy(final Transaction transaction, String location, MutableVariant variant, Consumer callback) { cellEditor.edit(transaction, location, variant, new Consumer() { @Override public void accept(Object param) { if (transaction.needSynchronization() != null) synchronize(transaction.needSynchronization()); } }); } @Override public Transaction startTransaction(OperationMode mode) { return cellEditor.startTransaction(mode); } }; } else if (SheetCommands.class == clazz ) { return (T) new SheetCommands() { @Override public void saveState() { Simantics.getSession().asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { IEclipseContext context = PlatformUI.getWorkbench().getService(IEclipseContext.class); Resource uiContextResource = run.getRepresents(graph); Resource bookResource = Variables.getContext(graph, run).getRepresents(graph); Layer0 L0 = Layer0.getInstance(graph); String uiContextName = graph.getRelatedValue2(uiContextResource, L0.HasName, Bindings.STRING); String bookName = graph.getRelatedValue2(bookResource, L0.HasName, Bindings.STRING); UISynchronize synchronizer = context.get(UISynchronize.class); synchronizer.asyncExec(() -> { Pair[] pairs = new Pair[] {Pair.make(uiContextName, uiContextResource), Pair.make(bookName, bookResource) }; SaveSpreadsheetStateDialog dialog = new SaveSpreadsheetStateDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor().getSite(), "Save Spreadsheet state", pairs); if (dialog.open() == Dialog.OK) { Object[] result = dialog.getSelection(); if (result != null) { Pair p = (Pair) result[0]; Simantics.getSession().asyncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { Variable parent = run.getParent(graph); Variable base = ProxyVariables.proxyVariableBase(graph, parent); SpreadsheetGraphUtils.saveInitialCondition(graph, parent, p.first, p.second); } }); } } else { return; } }); } }); } }; } return null; } @Override public void exception(Throwable t) { t.printStackTrace(); } @Override public boolean isDisposed() { return disposed; } @Override public void exception(AsyncReadGraph graph, Throwable t) { LOGGER.error("Failed to read properties.", t); } @Override public void exception(ReadGraph graph, Throwable t) { LOGGER.error("Failed to read properties.", t); } public void dispose() { for (PropertyListener listener : listenerCache.values()) listener.dispose(); listenerCache.clear(); SessionEventSupport support = processor.getService(SessionEventSupport.class); support.removeListener(listener); disposed = true; } private void synchronize(List list) { Simantics.getSession().asyncRequest(new FullSynchronizeBook(run, list)); } public static class FullSynchronizeBook extends ReadRequest { private final Variable run; private final List location; public FullSynchronizeBook(Variable run, List cellLocation) { this.run = run; this.location = cellLocation; } @Override public void run(ReadGraph graph) throws DatabaseException { String uri = run.getURI(graph); String parentUri = run.getParent(graph).getURI(graph); System.err.println("Full sync for book " + parentUri); Resource sheetResource = run.getRepresents(graph); Variable sheetVariable = Variables.getVariable(graph, sheetResource); TObjectIntHashMap changes = null; if (location != null) { changes = new TObjectIntHashMap<>(location.size()); for (Object loc : location) { Variable var = (Variable) loc; changes.put(var, 1); }; } SpreadsheetGraphUtils.partialSynchronization(graph, run.getParent(graph), changes); } } private SpreadsheetBook getBook(ReadGraph graph) throws DatabaseException { String sessionName = run.getParent(graph).getParent(graph).getURI(graph); StandardRealm realm = SpreadsheetSessionManager.getInstance().getOrCreateRealm(graph, sessionName); SpreadsheetBook book = realm.getEngine(); return book; } }