/****************************************************************************** * Copyright (c) 2016 Oracle * 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: * Shenxue Zhou - initial implementation and ongoing maintenance * Konstantin Komissarchik - [342897] Integrate with properties view * Konstantin Komissarchik - [342775] Support EL in MasterDetailsTreeNodeDef.ImagePath * Konstantin Komissarchik - [378756] Convert ModelElementListener and ModelPropertyListener to common listener infrastructure * Ling Hao - [383924] Flexible diagram node shapes ******************************************************************************/ package org.eclipse.sapphire.ui.diagram.internal; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.eclipse.sapphire.Element; import org.eclipse.sapphire.ElementList; import org.eclipse.sapphire.ElementType; import org.eclipse.sapphire.FilteredListener; import org.eclipse.sapphire.ListProperty; import org.eclipse.sapphire.Listener; import org.eclipse.sapphire.PropertyContentEvent; import org.eclipse.sapphire.PropertyDef; import org.eclipse.sapphire.PropertyEvent; import org.eclipse.sapphire.ReferenceValue; import org.eclipse.sapphire.Value; import org.eclipse.sapphire.ValueProperty; import org.eclipse.sapphire.modeling.ModelPath; import org.eclipse.sapphire.modeling.annotations.Reference; import org.eclipse.sapphire.modeling.el.FailSafeFunction; import org.eclipse.sapphire.modeling.el.Function; import org.eclipse.sapphire.modeling.el.FunctionResult; import org.eclipse.sapphire.modeling.el.Literal; import org.eclipse.sapphire.modeling.el.ModelElementFunctionContext; import org.eclipse.sapphire.modeling.localization.LocalizationService; import org.eclipse.sapphire.ui.SapphirePart; 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.def.IDiagramConnectionDef; import org.eclipse.sapphire.ui.diagram.def.IDiagramConnectionEndpointBindingDef; import org.eclipse.sapphire.ui.diagram.def.IDiagramExplicitConnectionBindingDef; import org.eclipse.sapphire.ui.diagram.editor.DiagramNodePart; import org.eclipse.sapphire.ui.diagram.editor.DiagramNodeTemplate; import org.eclipse.sapphire.ui.diagram.editor.SapphireDiagramEditorPagePart; import org.eclipse.sapphire.util.CollectionsUtil; /** * @author <a href="mailto:shenxue.zhou@oracle.com">Shenxue Zhou</a> * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a> * @author <a href="mailto:ling.hao@oracle.com">Ling Hao</a> */ public class DiagramConnectionTemplate extends SapphirePart { public static abstract class DiagramConnectionTemplateListener { public void handleConnectionEndpointUpdate(final ConnectionEndpointsEvent event) { } public void handleConnectionAddEvent(final ConnectionAddEvent event) { } public void handleConnectionDeleteEvent(final ConnectionDeleteEvent event) { } } protected SapphireDiagramEditorPagePart diagramEditor; protected IDiagramConnectionDef connectionDef; protected IDiagramExplicitConnectionBindingDef bindingDef; protected String propertyName; private ListProperty modelProperty; protected ListProperty connListProperty; protected Listener modelPropertyListener; protected Listener connPartListener; protected Set<DiagramConnectionTemplateListener> templateListeners; // The model path specified in the sdef file private ModelPath originalEndpoint2Path; // The converted model path for the endpoints. The path is based on the connection element private ModelPath endpoint1Path; private ModelPath endpoint2Path; private ValueProperty endpoint1Property; private ValueProperty endpoint2Property; private List<StandardDiagramConnectionPart> diagramConnections = new ArrayList<StandardDiagramConnectionPart>(); public DiagramConnectionTemplate() {} public DiagramConnectionTemplate(IDiagramExplicitConnectionBindingDef bindingDef) { this.bindingDef = bindingDef; } @Override public void init() { final Element element = getModelElement(); this.diagramEditor = (SapphireDiagramEditorPagePart)parent(); this.connectionDef = (IDiagramConnectionDef)super.definition(); this.propertyName = this.bindingDef.getProperty().content(); this.modelProperty = (ListProperty) element.property(this.propertyName).definition(); initConnPartListener(); this.templateListeners = new CopyOnWriteArraySet<DiagramConnectionTemplateListener>(); String endpt1PropStr = this.bindingDef.getEndpoint1().content().getProperty().content(); String endpt2PropStr = this.bindingDef.getEndpoint2().content().getProperty().content(); this.originalEndpoint2Path = new ModelPath(endpt2PropStr); ElementType type = this.modelProperty.getType(); this.endpoint1Property = type.property(endpt1PropStr); this.endpoint2Property = type.property(this.originalEndpoint2Path); if (getConnectionType() == ConnectionType.OneToOne) { this.endpoint1Path = new ModelPath(endpt1PropStr); this.endpoint2Path = new ModelPath(endpt2PropStr); this.connListProperty = this.modelProperty; } else { ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); PropertyDef prop = type.property(head.getPropertyName()); if (prop instanceof ListProperty) { this.endpoint1Path = new ModelPath("../" + endpt1PropStr); this.endpoint2Path = this.originalEndpoint2Path.tail(); this.connListProperty = (ListProperty)prop; } else { throw new RuntimeException("Invaid Model Path:" + this.originalEndpoint2Path); } } // initialize the connection parts ElementList<?> list = element.property(this.modelProperty); for( Element listEntryModelElement : list ) { // check the type of connection: 1x1 connection versus 1xn connection if (getConnectionType() == ConnectionType.OneToOne) { // The connection model element specifies a 1x1 connection createNewConnectionPart(listEntryModelElement, null); } else { ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); PropertyDef connProp = listEntryModelElement.property(head.getPropertyName()).definition(); if (!(connProp instanceof ListProperty)) { throw new RuntimeException("Expecting " + connProp.name() + " to be a list property"); } // the connection is of type 1xn ElementList<?> connList = listEntryModelElement.property((ListProperty)connProp); for (Element connElement : connList) { createNewConnectionPart(connElement, null); } } } // Add model property listener this.modelPropertyListener = new FilteredListener<PropertyContentEvent>() { @Override protected void handleTypedEvent( final PropertyContentEvent event ) { handleModelPropertyChange( event ); } }; addModelListener(); } protected void initConnPartListener() { this.connPartListener = new FilteredListener<ConnectionEndpointsEvent>() { @Override public void handleTypedEvent(ConnectionEndpointsEvent e) { notifyConnectionEndpointUpdate(e); } }; } public IDiagramConnectionDef getConnectionDef() { return this.connectionDef; } public String getConnectionTypeId() { return this.connectionDef.getId().content(); } public List<StandardDiagramConnectionPart> getDiagramConnections(Element connListParent) { List<StandardDiagramConnectionPart> connList = new ArrayList<StandardDiagramConnectionPart>(); if (connListParent == null || getConnectionType() == ConnectionType.OneToOne) { connList.addAll(this.diagramConnections); } else { for (StandardDiagramConnectionPart connPart : this.diagramConnections) { Element connModel = connPart.getLocalModelElement(); if (connModel.parent().element() == connListParent) { connList.add(connPart); } } } return connList; } public Element getConnectionParentElement(Element srcNodeModel) { if (getConnectionType() == ConnectionType.OneToMany) { ElementList<?> list = getModelElement().property(this.modelProperty); for( Element listEntryModelElement : list ) { Object valObj = listEntryModelElement.property(this.endpoint1Property); if (valObj instanceof ReferenceValue) { ReferenceValue<?,?> reference = (ReferenceValue<?,?>)valObj; Element model = (Element)reference.target(); if (model == null) { if (reference.text() != null) { SapphireDiagramEditorPagePart diagramEditorPart = getDiagramEditor(); DiagramNodePart targetNode = diagramEditorPart.getNode(reference.text()); if (targetNode != null) { model = targetNode.getLocalModelElement(); } } } if (srcNodeModel == model) { return listEntryModelElement; } } } } return null; } public boolean canStartNewConnection(DiagramNodePart srcNode) { boolean canStart = false; ElementType srcType = srcNode.getModelElement().type(); if (this.endpoint1Property.getType() == null && this.endpoint1Property.hasAnnotation(Reference.class)) { canStart = this.endpoint1Property.getAnnotation(Reference.class).target().isAssignableFrom(srcType.getModelElementClass()); } return canStart; } public boolean canCreateNewConnection(DiagramNodePart srcNode, DiagramNodePart targetNode) { boolean canCreate = false; canCreate = canStartNewConnection(srcNode); if (!canCreate) return false; ElementType targetType = targetNode.getModelElement().type(); if (this.endpoint2Property.getType() == null && this.endpoint2Property.hasAnnotation(Reference.class)) { canCreate = this.endpoint2Property.getAnnotation(Reference.class).target().isAssignableFrom(targetType.getModelElementClass()); } return canCreate; } public void addModelListener() { getModelElement().attach(this.modelPropertyListener, this.propertyName); if (getConnectionType() == ConnectionType.OneToMany) { // it's a 1xn connection type; need to listen to each connection list property ElementList<?> list = getModelElement().property(this.modelProperty); ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); for( Element listEntryModelElement : list ) { listEntryModelElement.attach(this.modelPropertyListener, head.getPropertyName()); } } } public void removeModelListener() { getModelElement().detach(this.modelPropertyListener, this.propertyName); if (getConnectionType() == ConnectionType.OneToMany) { // it's a 1xn connection type; need to listen to each connection list property ElementList<?> list = getModelElement().property(this.modelProperty); for( Element listEntryModelElement : list ) { ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); listEntryModelElement.detach(this.modelPropertyListener, head.getPropertyName()); } } } public void addTemplateListener( final DiagramConnectionTemplateListener listener ) { this.templateListeners.add( listener ); } public void removeTemplateListener( final DiagramConnectionTemplateListener listener ) { this.templateListeners.remove( listener ); } public SapphireDiagramEditorPagePart getDiagramEditor() { return this.diagramEditor; } public ConnectionType getConnectionType() { if (this.originalEndpoint2Path.length() > 1) { return ConnectionType.OneToMany; } else { return ConnectionType.OneToOne; } } public String getSerializedEndpoint1(DiagramNodePart srcNode) { String endpoint1Value = null; IDiagramConnectionEndpointBindingDef srcAnchorDef = this.bindingDef.getEndpoint1().content(); Value<Function> srcFunc = srcAnchorDef.getValue(); try( FunctionResult srcFuncResult = getNodeReferenceFunction(srcNode, srcFunc, this.bindingDef.adapt( LocalizationService.class )) ) { if (srcFuncResult != null) { endpoint1Value = (String)srcFuncResult.value(); } } if (endpoint1Value == null || endpoint1Value.length() == 0) { endpoint1Value = srcNode.getId(); } return endpoint1Value; } public String getSerializedEndpoint2(DiagramNodePart targetNode) { String endpoint2Value = null; IDiagramConnectionEndpointBindingDef targetAnchorDef = this.bindingDef.getEndpoint2().content(); Value<Function> targetFunc = targetAnchorDef.getValue();; try( FunctionResult targetFuncResult = getNodeReferenceFunction(targetNode, targetFunc, this.bindingDef.adapt( LocalizationService.class )) ) { if (targetFuncResult != null) { endpoint2Value = (String)targetFuncResult.value(); } } if (endpoint2Value == null || endpoint2Value.length() == 0) { endpoint2Value = targetNode.getId(); } return endpoint2Value; } private Element getOneToManyConnectionSrcElement(String endpoint1Value) { ElementList<?> list = getModelElement().property(this.modelProperty); Element srcElement = null; for (Element element : list) { final String val = element.property(this.endpoint1Property).text(); if (val != null && val.equals(endpoint1Value)) { srcElement = element; break; } } return srcElement; } public void setSerializedEndpoint1(Element connModelElement, String endpoint1Value) { IDiagramConnectionEndpointBindingDef srcAnchorDef = this.bindingDef.getEndpoint1().content(); if (getConnectionType() == ConnectionType.OneToOne) { setModelProperty(connModelElement, ((ModelPath.PropertySegment)this.endpoint1Path.head()).getPropertyName(), endpoint1Value); } else { Element srcElement = getOneToManyConnectionSrcElement(endpoint1Value); if (srcElement != null) { String srcProperty = srcAnchorDef.getProperty().content(); setModelProperty(srcElement, srcProperty, endpoint1Value); } } } public void setSerializedEndpoint2(Element connModelElement, String endpoint2Value) { setModelProperty(connModelElement, ((ModelPath.PropertySegment)this.endpoint2Path.head()).getPropertyName(), endpoint2Value); } public StandardDiagramConnectionPart createNewDiagramConnection(DiagramNodePart srcNode, DiagramNodePart targetNode) { // Get the serialized value of endpoint1 String endpoint1Value = getSerializedEndpoint1(srcNode); // get the serialized value of endpoint2 String endpoint2Value = getSerializedEndpoint2(targetNode); if (endpoint1Value != null && endpoint2Value != null) { if (getConnectionType() == ConnectionType.OneToOne) { ElementList<?> list = getModelElement().property(this.modelProperty); Element newElement = list.insert(); setModelProperty(newElement, ((ModelPath.PropertySegment)this.endpoint1Path.head()).getPropertyName(), endpoint1Value); setModelProperty(newElement, ((ModelPath.PropertySegment)this.endpoint2Path.head()).getPropertyName(), endpoint2Value); StandardDiagramConnectionPart newConn = getConnectionPart(srcNode.getModelElement(), newElement); if (newConn == null) { newConn = createNewConnectionPart(newElement, null); } return newConn; } else { Element srcElement = getOneToManyConnectionSrcElement(endpoint1Value); IDiagramConnectionEndpointBindingDef srcAnchorDef = this.bindingDef.getEndpoint1().content(); String srcProperty = srcAnchorDef.getProperty().content(); if (srcElement == null) { ElementList<?> list = getModelElement().property(this.modelProperty); srcElement = list.insert(); setModelProperty(srcElement, srcProperty, endpoint1Value); } ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); PropertyDef connProp = srcElement.property(head.getPropertyName()).definition(); if (!(connProp instanceof ListProperty)) { throw new RuntimeException("Expecting " + connProp.name() + " to be a list property"); } // the connection is of type 1xn ElementList<?> connList = srcElement.property((ListProperty)connProp); Element newElement = connList.insert(); setModelProperty(newElement, ((ModelPath.PropertySegment)this.endpoint2Path.head()).getPropertyName(), endpoint2Value); StandardDiagramConnectionPart newConn = getConnectionPart(srcElement, newElement); if (newConn == null) { newConn = createNewConnectionPart(newElement, null); } return newConn; } } return null; } protected StandardDiagramConnectionPart createNewConnectionPart(Element connElement, Element srcNodeElement) { StandardDiagramConnectionPart connPart = new StandardDiagramConnectionPart(this.bindingDef, this.endpoint1Path, this.endpoint2Path); addConnectionPart(srcNodeElement, connPart); connPart.init(this, connElement, this.connectionDef, Collections.<String,String>emptyMap()); connPart.initialize(); connPart.attach(this.connPartListener); notifyConnectionAddEvent(new ConnectionAddEvent(connPart)); return connPart; } public void showAllConnectionParts(DiagramNodeTemplate nodeTemplate) { List<StandardDiagramConnectionPart> connParts = getDiagramConnections(null); for (StandardDiagramConnectionPart connPart : connParts) { Element endpt1 = connPart.getEndpoint1(); Element endpt2 = connPart.getEndpoint2(); DiagramNodePart nodePart1 = this.diagramEditor.getDiagramNodePart(endpt1); if (nodePart1 != null && nodePart1.getDiagramNodeTemplate() == nodeTemplate) { notifyConnectionAddEvent(new ConnectionAddEvent(connPart)); } else { DiagramNodePart nodePart2 = this.diagramEditor.getDiagramNodePart(endpt2); if (nodePart2 != null && nodePart2.getDiagramNodeTemplate() == nodeTemplate) { notifyConnectionAddEvent(new ConnectionAddEvent(connPart)); } } } } public void hideAllConnectionParts(DiagramNodeTemplate nodeTemplate) { List<StandardDiagramConnectionPart> connParts = getDiagramConnections(null); for (StandardDiagramConnectionPart connPart : connParts) { Element endpt1 = connPart.getEndpoint1(); Element endpt2 = connPart.getEndpoint2(); DiagramNodePart nodePart1 = this.diagramEditor.getDiagramNodePart(endpt1); if (nodePart1 != null && nodePart1.getDiagramNodeTemplate() == nodeTemplate) { notifyConnectionDeleteEvent(new ConnectionDeleteEvent(connPart)); } else { DiagramNodePart nodePart2 = this.diagramEditor.getDiagramNodePart(endpt2); if (nodePart2 != null && nodePart2.getDiagramNodeTemplate() == nodeTemplate) { notifyConnectionDeleteEvent(new ConnectionDeleteEvent(connPart)); } } } } protected void setModelProperty(final Element modelElement, String propertyName, Object value) { if (propertyName != null) { final ElementType type = modelElement.type(); final PropertyDef property = type.property( propertyName ); if( property == null ) { throw new RuntimeException( "Could not find property " + propertyName + " in " + type.getQualifiedName() ); } if (!(property instanceof ValueProperty)) { throw new RuntimeException( "Property " + propertyName + " not a ValueProperty"); } modelElement.property( (ValueProperty) property ).write( value, true ); } } protected FunctionResult getNodeReferenceFunction(final DiagramNodePart nodePart, final Value<Function> function, LocalizationService ls) { Function f = null; FunctionResult fr = null; if( function != null ) { f = function.content(); } if( f != null ) { f = FailSafeFunction.create( f, Literal.create( String.class ) ); fr = f.evaluate( new ModelElementFunctionContext( nodePart.getLocalModelElement(), ls )); } return fr; } protected void handleConnectionListChange(Element connListParent, ListProperty listProperty) { ElementList<?> newList = connListParent.property(listProperty); List<StandardDiagramConnectionPart> connParts = getDiagramConnections(connListParent); List<Element> oldList = new ArrayList<Element>(connParts.size()); for (StandardDiagramConnectionPart connPart : connParts) { oldList.add(connPart.getLocalModelElement()); } List<Element> deletedConns = CollectionsUtil.removedBasedOnEntryIdentity(oldList, newList); List<Element> newConns = CollectionsUtil.removedBasedOnEntryIdentity(newList, oldList); // Handle deleted connections for (Element deletedConn : deletedConns) { StandardDiagramConnectionPart connPart = getConnectionPart(connListParent, deletedConn); if (connPart != null) { notifyConnectionDeleteEvent(new ConnectionDeleteEvent(connPart)); disposeConnectionPart(connPart); } } // Handle newly created connections for (Element newConn : newConns) { createNewConnectionPart(newConn, connListParent); } } protected void handleModelPropertyChange(final PropertyEvent event) { final Element element = event.property().element(); final ListProperty property = (ListProperty)event.property().definition(); ElementList<?> newList = element.property(property); if (property == this.connListProperty) { handleConnectionListChange(element, property); } else if (property == this.modelProperty) { // 1xn type connection and we are dealing with events on connection list parent List<StandardDiagramConnectionPart> connParts = getDiagramConnections(null); List<Element> oldList = new ArrayList<Element>(); Set<Element> oldConnParents = new HashSet<Element>(); for (StandardDiagramConnectionPart connPart : connParts) { Element connElement = connPart.getLocalModelElement(); Element connParentElement = connElement.parent().element(); if (!(oldConnParents.contains(connParentElement))) { oldConnParents.add(connParentElement); } } Iterator <Element> it = oldConnParents.iterator(); while(it.hasNext()) { oldList.add(it.next()); } List<Element> deletedConnParents = CollectionsUtil.removedBasedOnEntryIdentity(oldList, newList); List<Element> newConnParents = CollectionsUtil.removedBasedOnEntryIdentity(newList, oldList); // connection parents are deleted and we need to dispose any connections associated with them List<StandardDiagramConnectionPart> connPartsCopy = new ArrayList<StandardDiagramConnectionPart>(connParts.size()); connPartsCopy.addAll(connParts); for (StandardDiagramConnectionPart connPart : connPartsCopy) { Element connElement = connPart.getLocalModelElement(); Element connParentElement = connElement.parent().element(); if (deletedConnParents.contains(connParentElement)) { notifyConnectionDeleteEvent(new ConnectionDeleteEvent(connPart)); disposeConnectionPart(connPart); } } // new connection parents are added and we need to listen on their connection list property ModelPath.PropertySegment head = (ModelPath.PropertySegment)this.originalEndpoint2Path.head(); for (Element newConnParent : newConnParents) { newConnParent.attach(this.modelPropertyListener, head.getPropertyName()); handleConnectionListChange(newConnParent, this.connListProperty); } } } protected void addConnectionPart(Element srcNodeModel, StandardDiagramConnectionPart connPart) { this.diagramConnections.add(connPart); } protected void disposeConnectionPart(StandardDiagramConnectionPart connPart) { connPart.dispose(); connPart.detach(this.connPartListener); this.diagramConnections.remove(connPart); } protected StandardDiagramConnectionPart getConnectionPart(Element srcNodeModel, Element connModel) { List<StandardDiagramConnectionPart> connParts = getDiagramConnections(srcNodeModel); for (StandardDiagramConnectionPart connPart : connParts) { if (connPart.getLocalModelElement() == connModel) { return connPart; } } return null; } public void dispose() { removeModelListener(); List<StandardDiagramConnectionPart> connParts = getDiagramConnections(null); for (StandardDiagramConnectionPart connPart : connParts) { notifyConnectionDeleteEvent(new ConnectionDeleteEvent(connPart)); connPart.dispose(); } } protected void notifyConnectionEndpointUpdate(ConnectionEndpointsEvent event) { for( DiagramConnectionTemplateListener listener : this.templateListeners ) { listener.handleConnectionEndpointUpdate(event); } } protected void notifyConnectionAddEvent(ConnectionAddEvent event) { for( DiagramConnectionTemplateListener listener : this.templateListeners ) { listener.handleConnectionAddEvent(event); } } protected void notifyConnectionDeleteEvent(ConnectionDeleteEvent event) { for( DiagramConnectionTemplateListener listener : this.templateListeners ) { listener.handleConnectionDeleteEvent(event); } } // ****************************************************************** // Inner classes //******************************************************************* public static enum ConnectionType { OneToOne, OneToMany, Embedded } }