Skip to content

Commit

Permalink
Fix delete by using the model server
Browse files Browse the repository at this point in the history
- Use ModelServer to delegate GLSP delete
- Sort commands by descending index before sending to model server
- Add support to tree for compoundcommands
- Create correct compoundcommand on tree

Fixes eclipse-emfcloud#180
  • Loading branch information
eneufeld committed Feb 27, 2020
1 parent a226476 commit 5825726
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,26 @@
******************************************************************************/
package com.eclipsesource.workflow.glsp.server.handler.operation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.command.CommandParameter;
import org.eclipse.emf.edit.command.RemoveCommand;
import org.eclipse.emfcloud.modelserver.coffee.model.coffee.Flow;
import org.eclipse.emfcloud.modelserver.coffee.model.coffee.Node;
import org.eclipse.glsp.api.action.kind.AbstractOperationAction;
import org.eclipse.glsp.api.action.kind.DeleteOperationAction;
import org.eclipse.glsp.api.handler.OperationHandler;
import org.eclipse.glsp.api.model.GraphicalModelState;
import org.eclipse.glsp.graph.GEdge;
import org.eclipse.glsp.graph.GModelElement;
Expand All @@ -36,9 +45,11 @@
import com.eclipsesource.workflow.glsp.server.wfnotation.DiagramElement;
import com.eclipsesource.workflow.glsp.server.wfnotation.Shape;

public class DeleteOperationHandler implements OperationHandler {
public class DeleteOperationHandler implements ModelStateAwareOperationHandler {

private Set<EObject> toDelete;
private Set<EObject> toDeleteNodes;
private Set<EObject> toDeleteEdges;
private Set<DiagramElement> toDeleteLocal;

@Override
public boolean handles(AbstractOperationAction action) {
Expand All @@ -51,11 +62,61 @@ public String getLabel(AbstractOperationAction action) {
}

@Override
public void execute(AbstractOperationAction action, GraphicalModelState modelState) {
public void doExecute(AbstractOperationAction action, GraphicalModelState modelState,
final WorkflowModelServerAccess modelAccess) throws Exception {
DeleteOperationAction deleteAction = (DeleteOperationAction) action;
toDelete = new HashSet<>();
toDeleteNodes = new HashSet<>();
toDeleteEdges = new HashSet<>();
toDeleteLocal = new HashSet<>();
deleteAction.getElementIds().forEach(id -> collectElementsToDelete(id, modelState));
toDelete.forEach(e -> EcoreUtil.delete(e, true));
toDeleteLocal.forEach(e -> EcoreUtil.delete(e, true));

List<Command> deleteEdges = delete(toDeleteEdges, modelAccess);
List<Command> deleteNodes = delete(toDeleteNodes, modelAccess);
List<Command> unifiedToDelete = new ArrayList<>();
Comparator<Command> sortByIndex = new Comparator<Command>() {

@Override
public int compare(Command o1, Command o2) {
if (!(o1 instanceof RemoveCommand))
return 1;
if (!(o2 instanceof RemoveCommand))
return -1;
RemoveCommand rc1 = (RemoveCommand) o1;
RemoveCommand rc2 = (RemoveCommand) o2;
CommandParameter.Indices cp2 = (CommandParameter.Indices) rc2.getCollection().iterator().next();
CommandParameter.Indices cp1 = (CommandParameter.Indices) rc1.getCollection().iterator().next();
return cp2.getIndices()[0] - cp1.getIndices()[0];

}
};
// need to sort as otherwise the index is not correct when commands are applied.
Collections.sort(deleteEdges, sortByIndex);
Collections.sort(deleteNodes, sortByIndex);
unifiedToDelete.addAll(deleteEdges);
unifiedToDelete.addAll(deleteNodes);

CompoundCommand cc = new CompoundCommand(unifiedToDelete);
if (!modelAccess.edit(cc).thenApply(res -> res.body()).get()) {
throw new IllegalAccessError("Could not execute command: " + cc);
}
}

@SuppressWarnings("unchecked")
private List<Command> delete(Set<EObject> eObjects, final WorkflowModelServerAccess modelAccess) {
List<Command> result = new ArrayList<>();
for (EObject e : eObjects) {
EObject container = e.eContainer();
EStructuralFeature containingFeature = e.eContainingFeature();
// use index as object id cannot be used due to glsp -> modelserver -> glsp
// communication
int index = ((EList<EObject>) container.eGet(containingFeature)).indexOf(e);
Command removeEdgesCommand = RemoveCommand.create(modelAccess.getEditingDomain(), container,
containingFeature, index);
result.add(removeEdgesCommand);

}
return result;
}

protected void collectElementsToDelete(String id, GraphicalModelState modelState) {
Expand All @@ -72,11 +133,11 @@ protected void collectElementsToDelete(String id, GraphicalModelState modelState
WorkflowModelServerAccess modelAccess = WorkflowModelState.getModelAccess(modelState);
if (element instanceof GNode) {
Node node = modelAccess.getNodeById(element.getId());
toDelete.add(node);
toDeleteNodes.add(node);

Optional<DiagramElement> diagramElement = modelAccess.getWorkflowFacade().findDiagramElement(node);
if (!diagramElement.isEmpty() && diagramElement.get() instanceof Shape) {
toDelete.add(diagramElement.get());
toDeleteLocal.add(diagramElement.get());
}

modelState.getIndex().getIncomingEdges(element)
Expand All @@ -91,14 +152,14 @@ protected void collectElementsToDelete(String id, GraphicalModelState modelState
if (maybeFlow.isEmpty()) {
return;
}
toDelete.add(maybeFlow.get());
toDeleteEdges.add(maybeFlow.get());

Optional<DiagramElement> edge = maybeFlow
.flatMap(flow -> modelAccess.getWorkflowFacade().findDiagramElement(flow));
if (edge.isEmpty()) {
return;
}
toDelete.add(edge.get());
toDeleteLocal.add(edge.get());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import {

import { CoffeeModel } from './coffee-model';

const sortByIndex = (a: ModelServerCommand, b: ModelServerCommand) =>
b.indices[0] - a.indices[0];

@injectable()
export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget {
constructor(
Expand Down Expand Up @@ -75,76 +78,10 @@ export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget {
});
this.subscriptionService.onIncrementalUpdateListener(incrementalUpdate => {
const command = incrementalUpdate as ModelServerCommand;
// the #/ marks the beginning of the actual path, but we also want the first slash removed so +3
const ownerPropIndexPath = command.owner.$ref
.substring(command.owner.$ref.indexOf('#/') + 3)
.split('/')
.filter(v => v.length !== 0)
.map(path => {
const indexSplitPos = path.indexOf('.');
// each property starts with an @ so we ignore it
return {
property: path.substring(1, indexSplitPos),
index: path.substring(indexSplitPos + 1)
};
});
let ownerNode;
if (ownerPropIndexPath.length !== 0) {
ownerNode = this.treeWidget.findNode(ownerPropIndexPath);
if (command.commands !== undefined) {
command.commands.forEach(c => this.handleCommand(c));
} else {
// TODO should be done in findNode
ownerNode = (this.treeWidget.model.root as TreeEditor.RootNode)
.children[0];
}
const objectToModify =
ownerPropIndexPath.length === 0
? this.instanceData
: ownerPropIndexPath.reduce(
(data, path) =>
path.index === undefined
? data[path.property]
: data[path.property][path.index],
this.instanceData
);
switch (command.type) {
case 'add': {
if (!objectToModify[command.feature]) {
objectToModify[command.feature] = [];
}
objectToModify[command.feature].push(...command.objectsToAdd);
this.treeWidget.addChildren(
ownerNode,
command.objectsToAdd,
command.feature
);
break;
}
case 'remove': {
command.indices.forEach(i =>
objectToModify[command.feature].splice(i, 1)
);
this.treeWidget.removeChildren(
ownerNode,
command.indices,
command.feature
);
break;
}
case 'set': {
// maybe we can directly manipulate the data?
const data = clone(ownerNode.jsonforms.data);
// FIXME handle array changes
if (command.dataValues) {
data[command.feature] = command.dataValues[0];
objectToModify[command.feature] = command.dataValues[0];
} else {
data[command.feature] = command.objectsToAdd[0];
objectToModify[command.feature] = command.objectsToAdd[0];
}
this.treeWidget.updateDataForNode(ownerNode, data);
}
default: {
}
this.handleCommand(command);
}
});
this.modelServerApi.get(this.getModelIDToRequest()).then(response => {
Expand Down Expand Up @@ -174,6 +111,79 @@ export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget {
// see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
window.onbeforeunload = () => this.dispose();
}
private handleCommand(command: ModelServerCommand) {
// the #/ marks the beginning of the actual path, but we also want the first slash removed so +3
const ownerPropIndexPath = command.owner.$ref
.substring(command.owner.$ref.indexOf('#/') + 3)
.split('/')
.filter(v => v.length !== 0)
.map(path => {
const indexSplitPos = path.indexOf('.');
// each property starts with an @ so we ignore it
return {
property: path.substring(1, indexSplitPos),
index: path.substring(indexSplitPos + 1)
};
});
let ownerNode;
if (ownerPropIndexPath.length !== 0) {
ownerNode = this.treeWidget.findNode(ownerPropIndexPath);
} else {
// TODO should be done in findNode
ownerNode = (this.treeWidget.model.root as TreeEditor.RootNode)
.children[0];
}
const objectToModify =
ownerPropIndexPath.length === 0
? this.instanceData
: ownerPropIndexPath.reduce(
(data, path) =>
path.index === undefined
? data[path.property]
: data[path.property][path.index],
this.instanceData
);
switch (command.type) {
case 'add': {
if (!objectToModify[command.feature]) {
objectToModify[command.feature] = [];
}
objectToModify[command.feature].push(...command.objectsToAdd);
this.treeWidget.addChildren(
ownerNode,
command.objectsToAdd,
command.feature
);
break;
}
case 'remove': {
command.indices.forEach(i =>
objectToModify[command.feature].splice(i, 1)
);
this.treeWidget.removeChildren(
ownerNode,
command.indices,
command.feature
);
break;
}
case 'set': {
// maybe we can directly manipulate the data?
const data = clone(ownerNode.jsonforms.data);
// FIXME handle array changes
if (command.dataValues) {
data[command.feature] = command.dataValues[0];
objectToModify[command.feature] = command.dataValues[0];
} else {
data[command.feature] = command.objectsToAdd[0];
objectToModify[command.feature] = command.objectsToAdd[0];
}
this.treeWidget.updateDataForNode(ownerNode, data);
}
default: {
}
}
}
private getOldSelectedPath(): string[] {
const paths: string[] = [];
if (!this.selectedNode) {
Expand All @@ -199,12 +209,68 @@ export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget {
}

protected deleteNode(node: Readonly<TreeEditor.Node>): void {
const removeCommand = ModelServerCommandUtil.createRemoveCommand(
this.getNodeDescription(node.parent as TreeEditor.Node),
node.jsonforms.property,
node.jsonforms.index ? [Number(node.jsonforms.index)] : []
const elements = this.collectElementsToDelete(node);
const compoundCommand = {
eClass:
'http://www.eclipsesource.com/schema/2019/modelserver/command#//CompoundCommand',
type: 'compound',
commands: []
};
let edges = [];
let nodes = [];
elements.edges.forEach(e => {
const removeCommand = ModelServerCommandUtil.createRemoveCommand(
this.getNodeDescription(e.parent as TreeEditor.Node),
e.jsonforms.property,
e.jsonforms.index ? [Number(e.jsonforms.index)] : []
);
edges.push(removeCommand);
});
elements.nodes.forEach(e => {
const removeCommand = ModelServerCommandUtil.createRemoveCommand(
this.getNodeDescription(e.parent as TreeEditor.Node),
e.jsonforms.property,
e.jsonforms.index ? [Number(e.jsonforms.index)] : []
);
nodes.push(removeCommand);
});
edges = edges.sort(sortByIndex);
nodes = nodes.sort(sortByIndex);
compoundCommand.commands.push(...edges);
compoundCommand.commands.push(...nodes);
this.modelServerApi.edit(
this.getModelIDToRequest(),
compoundCommand as ModelServerCommand
);
}
private collectElementsToDelete(
node: Readonly<TreeEditor.Node>
): { nodes: TreeEditor.Node[]; edges: TreeEditor.Node[] } {
const result = { nodes: [], edges: [] };
switch (node.jsonforms.type) {
case CoffeeModel.Type.AutomaticTask:
case CoffeeModel.Type.ManualTask:
result.nodes.push(node);
result.edges.push(...this.findEdges(node));
break;
case CoffeeModel.Type.WeightedFlow:
case CoffeeModel.Type.Flow:
result.edges.push(node);
break;
}
return result;
}
private findEdges(node: Readonly<TreeEditor.Node>): TreeEditor.Node[] {
const parent = node.parent as TreeEditor.Node;
const flows: any[] = parent.children.filter(
c => (c as TreeEditor.Node).jsonforms.property === 'flows'
);
const ref = `//@workflows.0/@nodes.${node.jsonforms.index}`;
return flows.filter(
f =>
f.jsonforms.data.source.$ref === ref ||
f.jsonforms.data.target.$ref === ref
);
this.modelServerApi.edit(this.getModelIDToRequest(), removeCommand);
}
protected addNode({ node, type, property }: AddCommandProperty): void {
const addCommand = ModelServerCommandUtil.createAddCommand(
Expand Down

0 comments on commit 5825726

Please sign in to comment.