Customizing the yFiles FLEX I/O Support

In addition to the global properties described in the section called “GraphML Compatibility Modes”, class GraphMLIOHandler also allows to customize partial aspects of the serialization process. The following sections describe:

IXmlWriter

Interface IXmlWriter is used throughout the yFiles FLEX GraphML framework. The GraphML framework classes create a suitable instance for this interface, depending on the global properties that can be set on the GraphMLIOHandler that can be used everywhere where structured XML output is required. API Excerpt 4.8, “IXmlWriter overview” lists the more common methods on interface IXmlWriter.

API Excerpt 4.8. IXmlWriter overview

// Beginning a new XML element with given parameters.
writeStartElement(localName:String, ns:Namespace = null):IXmlWriter

// Writing an XML attribute node. Calls to this method must be nested within 
// writeStartElement and writeEndElement calls.
writeAttribute(localName:String, value:*, ns:Namespace = null):IXmlWriter 

// Closing an XML element previously opened with writeStartElement.
writeEndElement():IXmlWriter 

// Writing a text node with given content.
writeText(s:String):IXmlWriter 

Using Simple Data Types

Reading data stored in GraphML data attributes and writing data to GraphML attributes can be done by means of the methods from class GraphMLIOHandler as listed in API Excerpt 4.9, “GraphMLIOHandler support for the GraphML default extension mechanism”.

API Excerpt 4.9. GraphMLIOHandler support for the GraphML default extension mechanism

// Register a node scope IMapper for use as an input data target and output 
// data source, respectively.
addNodeAttribute(mapper:IMapper, attrName:String, type:String = "string"):void

// Register an edge scope IMapper for use as an input data target and output 
// data source, respectively.
addEdgeAttribute(mapper:IMapper, attrName:String, type:String = "string"):void

These methods are used to register an IMapper instance that binds the data stored in the attributes to the corresponding graph elements. The attribute name specified must correspond to the attr.name attribute of the GraphML attribute definition key.

Note that RoundtripHandler offers a convenience method for easily adding graph item attributes while reading and writing the graph for server communication. Server communication is described in depth in Chapter 5, Communicating with the Server.

The following fragment shows how to register an IMapper as input target/output source for a node attribute that stores long values.

Example 4.6. Adding a GraphML input/output attribute to GraphMLIOHandler

// Create a mapper.
var nodeMapper:IMapper = new DictionaryMapper();
var ioh:GraphMLIOHandler = new GraphMLIOHandler();

// Register the mapper with the GraphML I/O handler. The deserializer will be 
// looking for data that has attr.name "nodeAttr".
ioh.addNodeAttribute(nodeMapper, "nodeAttr", GraphMLConstants.TYPE_LONG);

Example 4.7, “Outline of a GraphML file with GraphML attribute of simple type” is an excerpt of a GraphML file that shows both the resulting GraphML attribute declaration and also the definition of its values with each of the nodes.

Example 4.7. Outline of a GraphML file with GraphML attribute of simple type

<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="...">
  ...
  <!-- Definition of a GraphML attribute to store additional data for a -->
  <!-- graph's nodes. -->
  <key id="d0" for="node" attr.name="color" attr.type="string"/>
  
  <graph id="G" edgedefault="directed">
    ...
    <!-- A node that has a <data> element referring to the GraphML attribute -->
    <!-- "d0." The node's value (of type string) is "green." -->
    <node id="n0">
      <data key="d0">green</data>
    </node>
    <node id="n1">
      <data key="d0">black</data>
    </node>
    ...
  </graph>
</graphml>

There is a Knowlegde Base article that explains in detail the necessary steps for transferring custom simple data between a yFiles FLEX client and the server.

Support for Structured Data

To store structured element content, i.e., more than just simple data types with a GraphML attribute, interfaces IInputHandler and IOutputHandler can be implemented.

The methods in the IInputHandler and IOutputHandler interfaces use so-called "contexts" to access the current state of the parse and write process. For parsing, the framework provides the class GraphMLParseContext. For writing, class GraphMLWriteContext is used. Both classes extend GraphMLContext. Besides other convenience methods, both classes provide access to both the current nesting of graph elements and the current graph element that is read or written.

API Excerpt 4.10. Stack access methods in GraphMLContext

// Returns the current nesting of graphs and graph elements as an ordered 
// read-only list.
get containers():List
// Returns the most current graph element within the container hierarchy.
get lastContainer():Object
// Returns the last container of the given type.
getContainer(clazz:Class):Object

For (de)serialization of complex data, yFiles FLEX provides the IInputHandler and IOutputHandler together with their abstract base class implementations AbstractInputHandler and AbstractOutputHandler. Note that it is usually much easier to extend these abstract base classes instead of implementing the interfaces.

Class GraphMLIOHandler provides the methods necessary to register custom input or output handlers.

API Excerpt 4.11. GraphMLIOHandler handler registration

// Adds a custom input handler to this GraphMLIOHandler.
addInputHandler(handler:IInputHandler):void 

// Adds a custom output handler for the given element scope to this 
// GraphMLIOHandler.
addOutputHandler(outputHandler:IOutputHandler, scope:String):void

To create a custom input handler, it is usually sufficient to inherit from AbstractInputHandler and implement acceptKey and parseItemData methods, as shown in API Excerpt 4.12, “Custom input handler creation and usage” that creates an input handler for node scope.

API Excerpt 4.12. Custom input handler creation and usage

public class MyInputHandler extends AbstractInputHandler {
  // Accept if the keyElement has node scope, name "myattr", and complex 
  // attribute type.
  override public function acceptKey(keyElement:XML, 
                                     scopeType:String):Boolean {
    var attrName:String = keyElement.attribute(GraphMLConstants.ATTR_NAME);
    var attrType:String = keyElement.attribute(GraphMLConstants.ATTR_TYPE);
    
    return scopeType == GraphMLConstants.SCOPE_NODE && 
                        attrName == "myattr" && 
                        attrType == GraphMLConstants.ATTR_TYPE_COMPLEX;
  }
  
  // Parse the actual data for the current node
  override protected function parseItemData(
    context:GraphMLParseContext, graph:IGraph, item:Object, 
    defaultMode:Boolean, element:XML):void {
      var node:INode = item as INode;
      if (null == node || null == graph || null == elment || 
          element.children().length() == 0) {
        return;
      }
      
      // Do something...
  }
}

// Register handler.
var ioh:GraphMLIOHandler = new GraphMLIOHandler();
ioh.addInputHandler(new MyInputHandler());

Creating a custom output handler is done by inheriting from AbstractOutputHandler and implementing the printKeyOutput, printKeyAttributes(), and printItemDataOutput() methods. API Excerpt 4.13, “Custom output handler creation and usage” shows an how to create an output handler for node scope.

API Excerpt 4.13. Custom output handler creation and usage

 
public class MyOutputHandler extends AbstractOutputHandler {    
  // Declare the custom attribute.
  override public function printKeyAttributes(context:GraphMLWriteContext, 
                                              writer:IXmlWriter):void {
      writer.writeAttribute(GraphMLConstants.ATTR_NAME, "myattr");
      writer.writeAttribute(GraphMLConstants.ATTR_TYPE, 
                            GraphMLConstants.ATTR_TYPE_COMPLEX);
  }
  
  // 
  override public function printKeyOutput(context:GraphMLWriteContext, 
                                          writer:IXmlWriter):void {
    // Do nothing.
  }
  
  override public function printItemDataOutput(
    context:GraphMLWriteContext, graph:IGraph, item:Object, 
    writer:IXmlWriter):void {
      var node:INode = item as INode;
      if (null != node) {
        // Do something.
      }
  }
}

// Register handler.
var ioh:GraphMLIOHandler = new GraphMLIOHandler();
ioh.addOutputHandler(new MyOutputHandler(), GraphMLConstants.SCOPE_NODE);

yFiles FLEX already provides several predefined input and output handlers for most common use cases, such as reading and writing an element's geometry or its style. These handlers read and write the data for the predefined data keys listed in Table 4.3, “Predefined combinations for XML attributes for and attr.name”.

The following tables list the predefined handlers for reading and writing element geometry, style information, and label information, respectively.

Table 4.4. Predefined input/output handlers (element geometry)

Input Handler Output Handler Combination of Values
ReadNodeLayoutHandler, ReadEdgeLayoutHandler, ReadPortLayoutHandler WriteNodeLayoutHandler, WriteEdgeLayoutHandler, WritePortLayoutHandler for= "node" | "edge" | "port" attr.name= "geometry" attr.type= "complex"

Table 4.5. Predefined input/output handlers (style information)

Input Handler Output Handler Combination of Values
NodeStyleInputHandler, EdgeStyleInputHandler NodeStyleOutputHandler, EdgeStyleOutputHandler for= "node" | "edge" attr.name= "style" attr.type= "complex"

Table 4.6. Predefined input/output handlers (label information)

Input Handler Output Handler Combination of Values
LabelInputHandler LabelOutputHandler for= "node" | "edge" attr.name= "labels" attr.type= "complex"

Note that these handlers should not be directly instantiated. Instead, they all have a public instance property that allows access to the static instance of these handlers.

Support for Custom Serializers and Deserializers

General Serializer and Deserializer Concepts

The previous sections focused on methods to read or write the whole content of a graphml attribute at once. In order to (de)serialize only fragments of an attribute, such as a specific property of an object, yFiles FLEX offers a general serializer and deserializer framework. This concept is used extensively throughout all predefined handler classes, thus allowing to

  • provide reusable (de)serializers that can be bound directly to an object or class instead of being globally registered,
  • provide a high degree of modularization that corresponds to the overall architecture of yFiles FLEX,
  • share instances to preserve object identity upon (de)serialization.

This section introduces the general aspects of the (de)serializer mechanism, while the next section shows how to use this mechanism to read and write custom item styles.

Interface ISerializer is the main interface for object serialization. Instead of implementing the interface, however, it is strongly advised to extend abstract base class AbstractSerializer in order to create custom serializers.

Creating a custom serializer for a given class using AbstractSerializer as the base type is done as follows:

  1. The elementName property needs to be implemented as well as serializeContent(). If a custom namespace should be used, the xmlNamespace property should be overriden as well. By default, AbstractSerializer uses the yWorks namespace. serializeContent() is used by the framework for writing all content as child nodes of a new XML node with name as obtained from elementName and namespace URI as obtained from xmlNamespace.
  2. the canHandle() method should be overridden as well. The framework only ensures that an object that is given for serialization is not null. Usually, a custom serializer should check if the object to be serialized is of the correct type.

In order to actually use a custom serializer, it is important to understand the serializer look-up mechanism in yFiles FLEX. The GraphML framework uses a three-level look-up mechanism:

  1. If the object to serialize implements ILookup, the framework queries an ISerializer instance from there.
  2. If 1.) was not successful, the framework tries to find a suitable serializer among all serializers that have been registered using the registerSerializer() method. The serializers are checked in their order of registration and the first (if any) serializer whose canHandle method returns true for the current object is returned.
  3. If not successful, a given object is not serialized.

Interface IDeserializer is the main interface for object deserialization.

Creating a custom deserializer for a given class is done as follows:

  1. In the deriving class, the elementName and xmlNamespace properties need to be implemented as well as the deserialize() and canHandle methods.

In order to actually use a custom deserializer, it needs to be registered with the GraphMLIOHandler instance using the registerDeserializer() method. Deserializers are checked in their order of registration and the first (if any) deserializer whose canHandle method returns true for a given XML node is used.

Reading and Writing Custom Styles

This section gives an overview of how to use the serializer and deserializer mechanism for a custom node style (edge and label style work similarly).

Creating and using a serializer for a custom node style using AbstractSerializer as the base type is done as follows:

  1. The elementName and xmlNamespace properties need to be implemented as well as the serializeContent() and canHandle() methods.
    private var myNamespace:Namespace = 
                  new Namespace("myPrefix", "http://mydomain.com");
    
    override protected function serializeContent(
      context:GraphMLWriteContext, subject:Object, writer:IXmlWriter):void {
        var style:MyNodeStyle = subject as MyNodeStyle;
        if (null == style) {
          return;
        }
        writer.writeAttribue("someStyleProperty", style.someStyleProperty);
        //...
    }
    
    override public function get elementName():String {
      return "MyNodeStyle";
    }
    
    override public function get xmlNamespace():Namespace {
      return myNamespace;
    }
    
    // Serialize only subjects of type MyNodeStyle.
    override public function canHandle(context:GraphMLWriteContext, 
                                       subject:Object):Boolean {
      return super.canHandle(subject) && subject is MyNodeStyle;
    }
    
  2. The serializer needs to be registered. For example, this can be done using the ILookup approach. The framework queries the style renderer for an ISerializer implementation:
    public class MyStyleRenderer extends AbstractNodeStyleRenderer {
      //...
      override public function lookup(type:Class):Object {
        if (type == ISerializer) {
          return new MyNodeStyleSerializer();
        }
        return super.lookup(type);
      }
    }
    
    public class MyNodeStyle implements INodeStyle {
      private var renderer:MyStyleRenderer;
      
      public function MyNodeStyle() {
        this.renderer = new MyStyleRenderer();
      }
    }
    

Creating and using a custom deserializer for a given class is done as follows:

  1. In the deriving class, the elementName and xmlNamespace properties need to be implemented as well as the deserialize() and canHandle() methods.
    public class MyNodeStyleDeserializer implements IDeserializer {
      private var myNamespace:Namespace = 
                    new Namespace("myPrefix", "http://mydomain.com");
      
      public function deserialize(context:GraphMLParseContext, element:XML):Object {
        var style:MyNodeStyle = new MyNodeStyle();
        var attribute:XMLList = element.attribute("someDoubleValue");
        if (attribute != null) {
          style.someDoubleValue = Number(attribute);
        }
        return style;
      }
      
      public function canHandle(context:GraphMLParseContext, element:XML):Boolean {
        if (null == element) {
          return false;
        }
        return element.localName() == elementName && 
               element.namespace() == xmlNamespace;
      }
      
      public function get elementName():String { return "MyNodeStyle"; }
      public function get xmlNamespace():Namespace { return myNamespace; }
    }
    
  2. The deserializer needs to be registered.
     
    var ioh:GraphMLIOHandler = new GraphMLIOHandler();
    // Register the deserializer for the custom style.
    ioh.registerDeserializer(new MyNodeStyleDeserializer());
    

Reflection Based Serialization

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 4.8. Adding a package-namespace pair to the SymbolicPackageNameRegistry

SymbolicPackageNameRegistry.add(
        "demo.style", 
        "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 functions. It will not serialize public variables or readonly properties.

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. They will only be invoked if no other serializer / deserializer which can handle the current object is found.

Example 4.9, “Class to be serialized with the ReflectionBasedSerializer” demonstrates how to create a class which can be handled by the ReflectionBasedSerializer. Its package has to be registered as shown in Example 4.8, “Adding a package-namespace pair to the SymbolicPackageNameRegistry”. The GraphML output which is generated for this class is shown in Example 4.10, “GraphML generated by ReflectionBasedSerializer”.

Example 4.9. Class to be serialized with the ReflectionBasedSerializer

package demo.style
{
    public class Person
    {       
        // property name has getter and setter
        private var _name:String;   
        public function get name():String {
            return _name;
        }
        public function set name(value:String):void {
            this._name = value;
        }
        // property vip has getter and setter
        private var _vip:Boolean;
        public function get vip():Boolean {
            return _vip;
        }
        public function set vip( value:Boolean ):void {
            this._vip = value;
        }
        // property pen has a complex type 
        private var _pen:Stroke;
        public function get pen():Stroke {
            return _pen;
        }
        public function set pen(value:Stroke):void {
            this._pen = value;
        }
        // property onlyGetter has only a getter
        private var _onlyGetter:String;  
        public function get onlyGetter():String {
            return _onlyGetter;
        }
        // variable id has neither getter nor setter but is public
        public var id:String;    
    }
}

Example 4.10. 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="Red" width="3"/>
    </aaa:Person.pen>
</aaa:Person>

Note that the uri which is registered in the SymbolicPackageNameRegistry is used as namespace uri for the generated elements. The simple type properties name and vip are serialized as attributes, the pen property, which is of the complex type Stroke, is serialized as child element of the Person element. Also note that the readonly onlyGetter property and the public variable id are not serialized.

Server Side Reflection Based Serializer and Deserializer

The handling of the reflection based serializer on a yFiles Java server is described in detail in the section called “Reflection Based Serialization”.

The handling of the reflection based serializer on a yFiles .NET server is described in detail in the section called “Reflection Based Serialization”.