Manipulating yFiles FLEX Styles, Labels, and Label Models

As explained in Chapter 6, Using yFiles FLEX with a yFiles Server, the yFiles FLEX client uses a different concept for defining the visual properties of graph items than yFiles Java. Therefore, the yFiles FLEX server API contains a framework for reading and writing the yFiles FLEX GraphML format along with compatibility classes that model the client's styles, labels, and label models. The server API that provides this functionality is contained in package com.yworks.yfiles.server.graphml.flexio and its sub-packages.

Please see the section called “Using the yFiles FLEX Native GraphML Extension with a yFiles for Java Server” for the required configuration to use the yFiles FLEX format compatibility layer on the server.

Reading and writing custom styles and label models on the server

In order to read, manipulate, and write custom styles on the server, three classes are required:

  • A custom DeserializationHandler implementation that is able to read the serialized style.
  • A custom SerializationHandler implementation that writes the GraphML data of the custom style.
  • A class that serves as a data object of the custom style on the server. This class should have the same state properties as the style implementation on the client, but as opposed to the client implementation, it does not provide any functionality.

In the following, examples for all of these three classes are provided. The example implementations are taken from the custom style demo application that comes with yFiles FLEX. The custom style has fill, stroke, and "percentage" properties, all of which influence the way the style is rendered on the client. The following GraphML serialization format is used for the custom style:

<y:CustomStyle>
  <y:Percentage value="0.8" />
  <y:Pen .. />
  <y:Brush.. />
</y:CustomStyle>

A data object for a custom style is a very simple data class that provides getters and setters for all properties of the custom style. Instances of the data object class are created by the corresponding deserializer implementation.

Example 7.1. Custom style data object

/**
 * A simple data object that holds the state information of
 * the custom style.
 */
public class CustomStyle implements INodeStyle {
  private IStroke stroke;
  private IFill fill;
  private double percentage;

  public IStroke getStroke() {
    return stroke;
  }
  public void setStroke(IStroke stroke) {
    this.stroke = stroke;
  }

  public IFill getFill() {
    return fill;
  }
  public void setFill(IFill fill) {
    this.fill = fill;
  }

  public double getPercentage() {
    return percentage;
  }
  public void setPercentage(double percentage) {
    this.percentage = percentage;
  }
}

A custom deserializer should extend AbstractDeserializer and override deserializeNode. It's important not to override deserialize instead, because this would prevent reference sharing from working correctly. The deserializer creates an instance of the corresponding data object and sets its properties by reading the GraphML data.

Example 7.2. Custom style deserializer

// Simple deserializer implementation that reads the custom style
// and creates a corresponding CustomStyle data object that can be used to
// manipulate the style properties.
// 
// Because this deserializer extends AbstractDeserializer, we don't need
// to implement canHandle.
// The default implementation will return true, if
// the element name and namespace of an xml element match
// the values returned by getElementName and getXmlNamespaceURI.
//
// The framework already contains deserializers for fill and strokes instances.
// Therefore, we can just delegate the deserialization of the fill and stroke 
// using FlexIOTools.deserialize
public class CustomStyleDeserializer extends AbstractDeserializer {
  public Object deserializeNode(Node xmlNode, GraphMLParseContext context)
                                throws GraphMLParseException {
    // Create a new instance of the data object that will keep the state
    // of the deserialized custom style
    CustomStyle style = new CustomStyle();

    // Deserialize the fill using 
    // FlexIOTools.deserialize, which will delegate the deserialization to 
    // an DeserializationHandler that can deserialize fills.
    Node fillNode = 
      XmlSupport.getChildNode(xmlNode, Constants.Y_BRUSH, 
                              NamespaceConstants.YFILES_JAVA_NS);
    IFill fill = (IFill) FlexIOTools.deserialize(context, fillNode);
    if (null != fill) {
      style.setFill(fill);
    }

    // Deserialize the stroke using 
    // FlexIOTools.deserialize, which will delegate the deserialization to 
    // an DeserializationHandler that can deserialize strokes
    Node strokeNode = 
      XmlSupport.getChildNode(xmlNode, Constants.Y_PEN, 
                              NamespaceConstants.YFILES_JAVA_NS);
    IStroke stroke = (IStroke) FlexIOTools.deserialize(context, strokeNode);
    if (null != stroke) {
      style.setStroke(stroke);
    }

    // Read the percentage attribute
    Node percentageNode = 
      XmlSupport.getChildNode(xmlNode, "Percentage", 
                              NamespaceConstants.YFILES_JAVA_NS);
    if (null != percentageNode) {
      String value = XmlSupport.getAttributeValue(percentageNode, "value");
      if (null != value) {
        style.setPercentage(Double.parseDouble(value));
      }
    }
    return style;
  }

  public String getElementName(GraphMLParseContext context) {
    return "CustomStyle";
  }

  public String getXmlNamespaceURI(GraphMLParseContext context) {
    return NamespaceConstants.YFILES_JAVA_NS;
  }
}

Registering custom serializers and deserializers

All SerializationHandler and DeserializationHandler instances have to be registered with the GraphMLHandler that is used for the GraphML (de)serialization process. Custom serializers and deserializers can also be added to the GraphRoundtripSupport instance that is used for client-server communication. Each time the graph is read or written with the roundtrip support, all serializers and deserializers that have been added using addSerializer(y.io.graphml.output.SerializationHandler) and addDeserializer(y.io.graphml.input.DeserializationHandler) are registered with the GraphMLHandler in addRegisteredHandlers(y.io.graphml.GraphMLHandler).

Example 7.3. Registering custom serializer and deserializer instances

// Register a serializer and a deserializer
// for a custom style with the 
//
this.support = new GraphRoundtripSupport();
support.addSerializer(new CustomStyleSerializer());
support.addDeserializer(new CustomStyleDeserializer());

Reflection Based Serialization

Similar to the Flex side, serializing complex objects can be faciliated using the ReflectionBasedSerializer. This serializer uses reflection to serialize objects of different class types.

The ReflectionBasedSerializer handles all classes which are in a package which is registered in the static SymbolicPackageNameRegistry together with a namespace URI.

Example 7.4. Adding a package-namespace pair to the SymbolicPackageNameRegistry

SymbolicPackageNameRegistry.add(
        "com.yworks.yfiles.server.graphml.demo.support", 
        "http://www.yworks.com/demo/style");

The namespace URI which will be used for the elements representing the serialized object in the GraphML.

The ReflectionBasedSerializer serializes all public read- and writable properties, i.e. properties which have public getter and setter methods which are named according to the JavaBeans conventions. It will not serialize public variables. Table 7.1, “Properties in Java, Flex and GraphML” shows how to handle a property with the identifier "property".

Table 7.1. Properties in Java, Flex and GraphML

Property type Flex GraphML Java
String, numeric property Attribute: property="value" getProperty()/setProperty()
boolean property Attribute: property="true|false" isProperty()/setProperty()
Complex property Child element: <Class.property>[value]</Class.property> getProperty()/setProperty()

The corresponding deserializer is the ReflectionBasedDeserializer. It handles all XML elements which are defined in a namespace whose URI is stored in the SymbolicPackageNameRegistry.

The ReflectionBasedSerializer and ReflectionBasedDeserializer are registered by default. The serializer and deserializer will only be invoked if no other serializer / deserializer which can handle the current object is found.

Example 7.5, “Class to be serialized with the ReflectionBasedSerializer” shows the Java class Person which corresponds to the Flex class Person shown as example in the section called “Reflection Based Serialization”.

Example 7.5. Class to be serialized with the ReflectionBasedSerializer

package com.yworks.yfiles.server.graphml.demo.support;

import com.yworks.yfiles.server.graphml.flexio.compat.Stroke;

public class Person {

  private String name;
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  private boolean vip;
  public boolean isVip() {
    return vip;
  }
  public void setVip(boolean vip) {
    this.vip = vip;
  }
  private Stroke pen;
  public Stroke getPen() {
    return pen;
  }
  public void setPen(Stroke pen) {
    this.pen = pen;
  }
}

The ReflectionBasedSerializer generates the GraphML which is shown in Example 7.6, “GraphML generated by ReflectionBasedSerializer” .

Example 7.6. GraphML generated by ReflectionBasedSerializer

<aaa:Person name="Another Person" vip="true" 
            xmlns:aaa="http://www.yworks.com/demo/style">
    <aaa:Person.pen>
        <y:Pen name="SolidColor" color="#ffff0000" width="3"/>
    </aaa:Person.pen>
</aaa:Person>

Class StyledLayoutGraph

Class StyledLayoutGraph extends LayoutGraph to provide methods for working with yFiles FLEX styles and labels on the server. GraphRoundtripSupport's createRoundtripGraph() method will always return a StyledLayoutGraph instance.

When the graph is read using a default GraphRoundtripSupport instance, the style and label data is parsed into data providers that are registered with the graph. Using StyledLayoutGraph, styles and labels can be accessed without having to deal with the underlying data providers. The methods provided by FlexIOTools for accessing styles and labels are listed below:

INodeStyle getStyle(Node node)
Description Get a node's style
void setStyle(Node node, INodeStyle style)
Description Set the style for a node instance
IEdgeStyle getStyle(Edge edge)
Description Get the style of an edge
void setStyle(Edge edge,IEdgeStyle style)
Description Set the style for an edge instance
void addLabel(Node node, Label label)
Description Add a label to a node
void addLabel(Edge edge, Label label)
Description Add a label to an edge
List getLabels(Node node)
Description Get all labels that belong to a node
List getLabels(Edge edge)
Description Get all labels that belong to an edge

Please see the section called “Working with Labels on the Server” for specifics on working with labels on the server.

User tags which are accessed using the ITagOwner interface at the client can be set/retrieved via convenience methods offered by StyledLayoutGraph:

Object getUserTag(Object item)
Description Get the user tag of the provided node, edge or label.
static void setUserTag(Object item, Object tag)
Description Set the user tag of the provided node, edge or label.

Class FlexIOTools

The implementation of custom serializers and deserializers is facilitated by the convenience methods serialize and deserialize of class FlexIOTools:

static void serialize(GraphMLWriteContext context, Object subject, XmlWriter writer)
Description Try to serialize the given instance using the context and silently ignores GraphMLWriteExceptions.
static boolean trySerialize(Object item, GraphMLWriteContext context)
Description Tries to serialize the given item using the context and returns if the serialization was successful.
static boolean canBeSerialized(Object item, GraphMLWriteContext context)
Description Tests and returns if the given item can be serialized using the context, i.e. an appropriate serialization handler can be found and used.
static Object deserialize(GraphMLParseContext context, Node xmlNode)
Description Tries to delegate deserialization to the context and silently ignores GraphMLParseExceptions.
static String getARGBString(Color c)
Description Returns the ARGB string (e.g. #FFCC00FF) of a java.awt.Color instance.
static Color parseNamedOrARGBColor(String colorStr)
Description Parse the given argb string (e.g. #FFCC00FF) or named color (e.g. "Lime") into a java.awt.Color.