Low-level Customizations
Internally, reading and writing the data in the <data>
elements is handled by input and output handlers.
Implementing the interfaces IInputHandler and IOutputHandler
provides the most flexibility for handling additional data.
Roughly speaking, the
IInputHandler.parseData
method is invoked for each <data>
element that is associated with a <key>
the handler is designed to handle.
The typical procedure is to get the current element
(the graph element encoded by the <data>
element’s parent element), parse the passed XML node
(the <data>
element’s child node), and associate the created object with the graph item.
The way this is done is up to the developer.
On the other hand, the
IOutputHandler.writeValue
method is invoked for each graph item the handler is designed to handle. It should write the content for
a <data>
element that is associated with a <key>
the handler is supposed to handle. The
current graph item can be retrieved from the
IWriteContext as well as the IXmlWriter, which should be used
to write XML as a child node of the <data>
element.
Again, the implementation is up to the developer.
Implementing Custom I/O Handlers
Implementing a custom output handler is greatly simplified by extending the abstract base class OutputHandlerBase<TKey,TData>:
- In the constructor: invoke the super constructor to provide the name and scope for the
<key>
definition. - Implement the abstract method getValue to retrieve the object that should be written for the given graph item.
- Implement the abstract method writeValueCore to write the given data.
class MyOutputHandler extends OutputHandlerBase {
constructor() {
super(
IEdge,
MyTextHolder,
KeyScope.EDGE,
'my-custom-edge-data',
KeyType.COMPLEX
)
}
writeValueCore(context, data) {
// serialize the data as XML
const writer = context.writer
writer.writeStartElement('MyTextHolder', 'MyNamespace')
writer.writeCData(data.text)
writer.writeEndElement()
}
getValue(context, key) {
// get the data which is associated with the current graph item (the key)
return MyDataStore.getTextHolderForEdge(key)
}
}
class MyOutputHandler extends OutputHandlerBase<IEdge, MyTextHolder> {
constructor() {
super(
IEdge,
MyTextHolder,
KeyScope.EDGE,
'my-custom-edge-data',
KeyType.COMPLEX
)
}
writeValueCore(context: IWriteContext, data: MyTextHolder): void {
// serialize the data as XML
const writer = context.writer
writer.writeStartElement('MyTextHolder', 'MyNamespace')
writer.writeCData(data.text)
writer.writeEndElement()
}
getValue(context: IWriteContext, key: IEdge): MyTextHolder | null {
// get the data which is associated with the current graph item (the key)
return MyDataStore.getTextHolderForEdge(key)
}
}
Data that can be serialized by the GraphMLIOHandler can be written using IWriteContext’s serialize<T> method.
Implementing a custom input handler is greatly simplified by extending the abstract base class InputHandlerBase<TKey,TData>:
- Implement the method parseDataCore to return the object deserialized from the given XML node.
- Implement the method setValue to associate the given data with the given graph item (key).
class MyInputHandler extends InputHandlerBase {
constructor() {
super(IEdge, MyTextHolder, ParsePrecedence.DEFAULT)
}
parseDataCore(context, node) {
// gets the child of the <data> element to parse
if (node instanceof Element) {
const element = node
if (
element.localName === 'MyTextHolder' &&
element.namespaceURI === 'MyNamespace'
) {
const text = element.textContent
return new MyTextHolder(text)
}
}
throw new Error('Invalid XML')
}
setValue(context, key, data) {
// after the data has been deserialized associate it with the current graph item (key)
MyDataStore.storeTextHolderForEdge(key, data)
}
}
class MyInputHandler extends InputHandlerBase<IEdge, MyTextHolder> {
constructor() {
super(IEdge, MyTextHolder, ParsePrecedence.DEFAULT)
}
parseDataCore(context: IParseContext, node: Node): MyTextHolder | null {
// gets the child of the <data> element to parse
if (node instanceof Element) {
const element = node
if (
element.localName === 'MyTextHolder' &&
element.namespaceURI === 'MyNamespace'
) {
const text = element.textContent
return new MyTextHolder(text)
}
}
throw new Error('Invalid XML')
}
setValue(context: IParseContext, key: IEdge, data: MyTextHolder) {
// after the data has been deserialized associate it with the current graph item (key)
MyDataStore.storeTextHolderForEdge(key, data)
}
}
Data that can be deserialized by the GraphMLIOHandler can be parsed using IWriteContext’s deserialize<T> method.
Registering Custom I/O Handlers
Input and output handlers are registered with a GraphMLIOHandler during initialization using event handlers. You can register event handlers for both output and input handlers using the query-output-handlers and query-input-handlers methods, respectively. The events are invoked by GraphMLIOHandler dynamically, that is, at the time of writing or reading.
graphMLIOHandler.addEventListener('query-output-handlers', (evt) => {
if (evt.scope === KeyScope.EDGE) {
// register the handler only for the matching scope
evt.addOutputHandler(myGetCustomEdgeOutputHandler())
}
})
The event is dispatched for each scope before writing. Implementations should check the scope before adding the output handler. Note that the output handler is responsible for providing the correct attributes for the key element. OutputHandlerBase<TKey,TData>’s constructors, therefore, require at least the name as a parameter.
graphMLIOHandler.addEventListener('query-input-handlers', (evt) => {
if (
evt.handled ||
!GraphMLIOHandler.matchesName(evt.keyDefinition, 'my-custom-edge-data') ||
!GraphMLIOHandler.matchesScope(evt.keyDefinition, KeyScope.EDGE)
) {
// do nothing if
// evt are already handled or
// 'attr.name' is not "my-custom-edge-data"
// or scope is not edge
return
}
evt.addInputHandler(myGetCustomEdgeInputHandler())
evt.handled = true
})
The event is dispatched for each <key>
element during parsing. Implementations should check
whether the event is already handled
by another input handler. Also, they have to check whether the <key>
element’s name and scope attributes
match before
adding
the input handler. After the handler has been added,
handled should be set to true
.