/*******************************************************************************
* This program is 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:
* Tomas Milata - initial API and implementation
******************************************************************************/
package org.jboss.tools.batch.ui.editor.internal.services.diagram.connection;
import static org.jboss.tools.batch.ui.editor.internal.services.diagram.connection.BachtConnectionIdConst.NEXT_ATTRIBUTE_CONNECTION_ID;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.sapphire.Element;
import org.eclipse.sapphire.ElementList;
import org.eclipse.sapphire.FilteredListener;
import org.eclipse.sapphire.PropertyContentEvent;
import org.eclipse.sapphire.ReferenceValue;
import org.eclipse.sapphire.ui.diagram.ConnectionAddEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionDeleteEvent;
import org.eclipse.sapphire.ui.diagram.ConnectionEndpointsEvent;
import org.eclipse.sapphire.ui.diagram.DiagramConnectionPart;
import org.eclipse.sapphire.ui.diagram.StandardConnectionService;
import org.eclipse.sapphire.ui.diagram.editor.DiagramNodePart;
import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart;
import org.jboss.tools.batch.ui.editor.internal.model.FlowElement;
import org.jboss.tools.batch.ui.editor.internal.model.FlowElementsContainer;
import org.jboss.tools.batch.ui.editor.internal.model.NextAttributeElement;
/**
* A custom implementation of Sapphire connection service. It provides adapted
* methods for the connection using {@code next} attribute, i.e. @
* {@link BachtConnectionIdConst#NEXT_ATTRIBUTE_CONNECTION_ID} and delegates
* call to the standard service for other connection types.
*
* @author Tomas Milata
*/
public class BatchDiagramConnectionService extends StandardConnectionService {
private List<DiagramConnectionPart> connections;
private Set<NextAttributeElement> nextAttributeElements = new HashSet<>();
private Map<NextAttributeElement, FlowElement> nodesConnectionsMap = new HashMap<>();
private SapphireDiagramEditorPagePart diagramPart;
private EventHandler eventHandler = new EventHandler();
@Override
protected void init() {
super.init();
diagramPart = context(SapphireDiagramEditorPagePart.class);
}
/**
* Exposes the custom implementation when connectionType is
* {@link BachtConnectionIdConst#NEXT_ATTRIBUTE_CONNECTION_ID}, uses
* standard implementation otherwise.
*
* @param connectionType
* id as specified in the .sdef file
*/
@Override
public boolean valid(DiagramNodePart node1, DiagramNodePart node2, String connectionType) {
if (NEXT_ATTRIBUTE_CONNECTION_ID.equals(connectionType)) {
return valid(node1, node2);
} else {
Element target = node2.getLocalModelElement();
if(target instanceof FlowElement) {
FlowElement f = (FlowElement)target;
if(f.getId() == null || f.getId().content() == null) {
return false;
}
}
return super.valid(node1, node2, connectionType);
}
}
/**
* Exposes the custom implementation when connectionType is
* {@link BachtConnectionIdConst#NEXT_ATTRIBUTE_CONNECTION_ID}, uses
* standard implementation otherwise.
*
* @param connectionType
* id as specified in the .sdef file
*/
@Override
public DiagramConnectionPart connect(DiagramNodePart node1, DiagramNodePart node2, String connectionType) {
if (NEXT_ATTRIBUTE_CONNECTION_ID.equals(connectionType)) {
return connect(node1, node2);
} else {
return super.connect(node1, node2, connectionType);
}
}
/**
* @return list of all connections of all types
*/
@Override
public List<DiagramConnectionPart> list() {
List<DiagramConnectionPart> allConnections = new ArrayList<>();
if (connections == null) {
initConnections();
}
allConnections.addAll(connections);
allConnections.addAll(super.list());
return allConnections;
}
/**
* A connection from a {@code <step>}, {@code <split>} or {@code <flow>} to
* a {@code <step>}, {@code <split>}, {@code <flow>} or {@code <decision>}
* can be created iff target has an id, source is different than target and
* same connection does not exist yet.
*/
private boolean valid(DiagramNodePart node1, DiagramNodePart node2) {
Element src = node1.getLocalModelElement();
if (!(src instanceof NextAttributeElement)) {
return false;
}
FlowElement target = (FlowElement) node2.getLocalModelElement();
if (target.getId().empty()) {
// target must have id, otherwise there is nothing to write to
// xml
return false;
}
if (src.equals(target)) {
return false; // no self-loop
}
FlowElement existingConnectionTarget = nodesConnectionsMap.get(src);
// true if connection does not exist yet
return existingConnectionTarget == null || !existingConnectionTarget.equals(target);
}
/**
* Connects two nodes in the model and initializes the Sapphire part.
*/
private DiagramConnectionPart connect(DiagramNodePart node1, DiagramNodePart node2) {
// connect the reference in the model
FlowElement target = (FlowElement) node2.getLocalModelElement();
String nextId = target.getId().content();
NextAttributeElement src = (NextAttributeElement) node1.getLocalModelElement();
src.setNext(nextId);
FlowElement existingEndpoint = nodesConnectionsMap.get(node1.getLocalModelElement());
if (existingEndpoint == null) {
return addConnectionPart(src, target);
} else {
return null;
}
}
/**
* Initializes diagram connections according to the model.
*/
private void initConnections() {
connections = new ArrayList<>();
if(diagramPart == null) return;
FlowElementsContainer currentModelRoot = (FlowElementsContainer) diagramPart.getLocalModelElement();
attachListenerForNewNodes(currentModelRoot.getFlowElements());
for (FlowElement src : currentModelRoot.getFlowElements()) {
if (src instanceof NextAttributeElement) {
initializeNextAttributeElement((NextAttributeElement) src);
}
initializeTargetElement(src);
}
}
/**
* Attaches listener that watches if Id of element has changed. (The element
* may become a new target of another element then).
*/
private void initializeTargetElement(final FlowElement target) {
target.getId().attach(new FilteredListener<PropertyContentEvent>() {
@Override
protected void handleTypedEvent(PropertyContentEvent event) {
connectIfIsTarget(target);
}
});
}
/**
* If there exists a source for the target, connection is created.
*/
private void connectIfIsTarget(FlowElement target) {
for (NextAttributeElement src : nextAttributeElements) {
String next = src.getNext().content();
if (next != null && next.equals(target.getId().content()) && !nodesConnectionsMap.containsKey(src)) {
addConnectionPart(src, target);
}
}
}
/**
* Creates a connection if src already has target, stores the node among
* current nodes and attaches listener for target changes.
*
* @param src
* the source node
*/
private void initializeNextAttributeElement(NextAttributeElement src) {
ReferenceValue<String, FlowElement> next = src.getNext();
if (next.target() != null) {
addConnectionPart(src, next.target());
}
nextAttributeElements.add(src);
attachListenerForNewConnection(src);
}
/**
* Watches the list of flow elements and when a new element is added, a
* listener for changes is added to it.
*
* @param flowElements
* the list of elements to watch
*/
private void attachListenerForNewNodes(final ElementList<FlowElement> flowElements) {
flowElements.attach(new FilteredListener<PropertyContentEvent>() {
@Override
protected void handleTypedEvent(PropertyContentEvent event) {
Iterator<NextAttributeElement> nextIt = nextAttributeElements.iterator();
while (nextIt.hasNext()) {
NextAttributeElement next = nextIt.next();
if (!flowElements.contains(next)) {
nextIt.remove();
Iterator<DiagramConnectionPart> connIt = connections.iterator();
while (connIt.hasNext()) {
DiagramConnectionPart c = connIt.next();
if (c.getEndpoint1().equals(next)) {
connIt.remove();
nodesConnectionsMap.remove(c.getEndpoint1());
broadcast(new ConnectionDeleteEvent(c));
}
}
}
}
for (FlowElement element : flowElements) {
if (element instanceof NextAttributeElement && !nextAttributeElements.contains(element)) {
initializeNextAttributeElement((NextAttributeElement) element);
}
initializeTargetElement(element);
// The new node might have been added with a target already
connectIfIsTarget(element);
}
}
});
}
/**
* Attaches a listener for changes in the value of the {@code next}
* attribute.
*/
private void attachListenerForNewConnection(final NextAttributeElement element) {
element.attach(new FilteredListener<PropertyContentEvent>() {
@Override
protected void handleTypedEvent(final PropertyContentEvent event) {
if (!nodesConnectionsMap.containsKey(element)) {
addConnectionPart(element, element.getNext().target());
}
}
}, NextAttributeElement.PROP_NEXT.name());
}
/**
* Creates, initializes and returns a Sapphire connection part and adds it
* to the existing connections.
*
* @param src
* @param target
* @return the new connection part
*/
private DiagramConnectionPart addConnectionPart(NextAttributeElement src, FlowElement target) {
nodesConnectionsMap.put(src, target);
NextAttributeConnectionPart connectionPart = new NextAttributeConnectionPart(src, target, this, eventHandler);
connectionPart.init(diagramPart, src, diagramPart.getDiagramConnectionDef(NEXT_ATTRIBUTE_CONNECTION_ID),
Collections.<String, String> emptyMap());
connectionPart.initialize();
connections.add(connectionPart);
return connectionPart;
}
/**
* Manages the state of existing connections and notifies Sapphire framework
* about changes via broadcast events.
*/
private final class EventHandler implements BatchDiagramConnectionEventHandler {
@Override
public void onConnectionEndpointsEvent(ConnectionEndpointsEvent event) {
nodesConnectionsMap.put((NextAttributeElement) event.part().getEndpoint1(),
(FlowElement) event.part().getEndpoint2());
BatchDiagramConnectionService.this.broadcast(event);
}
@Override
public void onConnectionAddEvent(ConnectionAddEvent event) {
BatchDiagramConnectionService.this.broadcast(event);
}
@Override
public void onConnectionDeleteEvent(ConnectionDeleteEvent event) {
connections.remove(event.part());
nodesConnectionsMap.remove(event.part().getEndpoint1());
BatchDiagramConnectionService.this.broadcast(event);
}
}
}