Joseph Coffland
jcofflan@users.sourceforge.net
September 15, 2006
Copyright ©2004, Joseph E. Coffland Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included with this documentation, GNU Free Documentation License.
SinaXe stands for SinaXe Is Not An XML Editor. SinaXe provides a set of graphical components which can be easily mapped on to arbitrary XML. Full featured XML editors are constructed in days instead of months and are easily modified to keep up with changing formats.
The rest of this document explains how to use the SinaXe language to map SinaXe components on to XML data.
A SinaXe program is a network of components called a composite. The component is the most basic element. Components generally have a graphical user interface and a position in the composite layout, but not always. When SinaXe starts it first initializes all its components. At this point a component can read its properties, register queries, subscriptions, variables and input and output ports with the run-time system. However, no communication is allowed at initialization time.
After initialization components may receive events from input ports, subscriptions or input devices such as a mouse or keyboard. The component can react to these events by running registered queries, emitting events on output ports or possibly changing the value of a registered variable.
Mappings are used to connect components to other components and to the XML data they operate on and to form the external interfaces of composite components. There are several types of mappings. The most important mappings are between component input and output ports. These mappings are called event maps. Other kinds of mappings include query maps, property maps and variable maps. Query maps are used to map requests for data to the actual XML data the component will operate on. Property and variable maps are used to defer mapping to the parent composite component, thus forming the composite's external interface. Event and query maps can also add to the parent's interface.
Queries are mapped to XPath statements. When a component calls a query it provides a context for that query. The XPath statement will execute on this context to extract the query result. This enables components to read XML data. Output ports can be mapped to XUpdate statements which modify the XML. Output events can also carry data which serves as the context for the XUpdate statement. These two languages, XPath and XUpdate, provide the glue between components and XML data and make it possible for components to operate on previously unknown data formats.
Composite components can also act just like regular components. Composites may have ports, properties, queries and variables just like regular components, but these are really mapped internally to its child components. Since every SinaXe program is simply a composite component it is possible to treat complex applications as simple components. You can make new applications by combining and mapping existing SinaXe applications.
To summarize, a SinaXe program is a composite component. Composites are collections of child components, maps and possibly other composites. Components can be configured with properties and can execute queries, send and receive events from ports and set the values of their variables. Components can be connected with maps. Maps can also map queries and output events on to XML data. Ports, properties, queries and variables can all be mapped to their parent composite component's external interface.
The next section contains a simple example of a SinaXe program which demonstrates some of these concepts.
<composite name="hello_world">
<component name="frame" src="class:org.sinaxe.components.SinaxeEditorFrame">
<property name="title" value="Hello World!"/>
<property name="size">
<property name="width" value="300"/>
<property name="height" value="100"/>
</property>
</component>
<map src="frame.closing" dst="frame.exit"/>
<map src="init" dst="frame.show"/>
</composite>
The above example will open a window with the title "Hello World!". The property title is used to configure this title. The size property is used to set the frame's dimensions. Each particular component may have different properties, ports, queries and variables. These should be described in the components documentation.
The example maps the event init to the component's input port show. init is a special event generated by the SinaXe run-time which starts the program. It also maps the frame's closing event to frame.exit. This mapping causes the program to end when the frame closes.
This example shows a very simple SinaXe program, but it only demonstrates a small part of the picture. we aren't manipulating any XML data yet. The example in the next section demonstrates more of SinaXe's features.
<composite name="list_editor">
<!-- Menu -->
<component name="openitem" src="class:org.sinaxe.components.SinaxeMenuItem">
<property name="text" value="Open"/>
</component>
<component name="saveitem" src="class:org.sinaxe.components.SinaxeMenuItem">
<property name="text" value="Save"/>
</component>
<component name="exititem" src="class:org.sinaxe.components.SinaxeMenuItem">
<property name="text" value="Exit"/>
</component>
<component name="filemenu" src="class:org.sinaxe.components.SinaxeMenu">
<property name="text" value="File"/>
<property name="menu">
<property name="item" value="openitem"/>
<property name="item" value="saveitem"/>
<property name="separator"/>
<property name="item" value="exititem"/>
</property>
</component>
<!-- Main Frame -->
<component name="frame" src="class:org.sinaxe.components.SinaxeEditorFrame">
<property name="title" value="List Editor"/>
<property name="size">
<property name="width" value="400"/>
<property name="height" value="400"/>
</property>
<property name="menubar">
<property name="menu" value="filemenu"/>
</property>
<property name="child" value="layout"/>
<map src="closing" dst="exit"/>
</component>
<map src="init" dst="frame.show"/>
The property menubar points to the previously created filemenu designating it as the frame's menu. The child property puts a child component in the frame, in this case the child is named layout. The layout component will come later.
<!-- File Loader -->
<component name="fileloader" src="class:org.sinaxe.components.SinaxeFileLoader">
<property name="parent" value="frame"/>
</component>
<!-- Menu Actions -->
<map src="openitem.action" dst="fileloader.open"/>
<map src="saveitem.action" dst="fileloader.save"/>
<map src="exititem.action" dst="frame.exit"/>
The menu actions are mapped to the file loader's open and save input ports and the frame's exit port.
<!-- List -->
<component name="list" src="class:org.sinaxe.components.SinaxeList">
<property name="subscribe"/>
</component>
<map src="fileloader.opened" dst="list.load"/>
<!-- Text Field -->
<component name="text" src="class:org.sinaxe.components.SinaxeTextField"/>
<map src="list.selectionchanged" dst="text.load"/>
<!-- Buttons -->
<component name="updatebutton" src="class:org.sinaxe.components.SinaxeButton">
<property name="text" value="Update"/>
</component>
<component name="addbutton" src="class:org.sinaxe.components.SinaxeButton">
<property name="text" value="Add"/>
</component>
<component name="delbutton" src="class:org.sinaxe.components.SinaxeButton">
<property name="text" value="Delete"/>
</component>
Notice that the XML coming from the file loader is mapped to the list component. Also, the list's selectionchanged port is directed to the text field. This causes the text of the currently selected list element to be displayed in the text field.
<!-- XML Mappings -->
<map src="list.text">@value</map>
<map src="list.list">list/item</map>
<map src="text.text">@value</map>
<map src="updatebutton.action">
<xupdate:update select='$list.selection/@value'>
${$text.value}
</xupdate:update>
</map>
<map src="addbutton.action">
<xupdate:append select='$list.context/list'>
<xupdate:element name="item">
<xupdate:attribute name="value">${$text.value}</xupdate:attribute>
</xupdate:element>
</xupdate:append>
</map>
<map src="delbutton.action">
<xupdate:remove select='$list.selection'/>
</map>
To understand the above mappings it helps to understand the XML data they are designed to manipulate. Below is some possible input data. Note what follows is not part of the SinaXe program.
<list> <item value="A"/> <item value="B"/> <item value="C"/> <item value="D"/> </list>
In the previous SML code, the list component's list query is mapped to the item element children of the list element with the XPath statement list/item. Also, the text queries of both the list and text field components are mapped to the value attribute of the item element. It is important to know that the list component uses the item elements as the context to its text query.
Finally the three buttons are mapped to XUpdate statements. The update button selects the value attribute of the currently selected item elements and changes their text to the value stored in the text field's value variable. The add button appends a new item element using the text in the text field's value variable. The delete button removes all the selected list elements.
<!-- Layout -->
<component name="layout" src="class:org.sinaxe.components.SinaxePackerLayout">
<property name="layout">
<panel layout="fill=both">
<panel layout="side=top;fill=x">
<port component="text" layout="side=left;fill=x;expand=true"/>
<port component="updatebutton" layout="side=left"/>
<port component="addbutton" layout="side=left"/>
<port component="delbutton" layout="side=left"/>
</panel>
<port component="list" layout="side=bottom;expand=true;fill=both"/>
</panel>
</property>
</component>
<html>
<body>
<ol>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ol>
</body>
</html>
The above example data is in XHTML format. Note that SinaXe cannot operate directly on HTML because it is not XML. However, it is very easy to convert HTML to XHTML with free tools like tidy.
To edit this new format with our list editor we replace the XML mapping code in our example with the following:
<!-- XML Mappings -->
<map src="list.text">text()</map>
<map src="list.list">html/body/ol/li</map>
<map src="text.text">text()</map>
<map src="updatebutton.action">
<xupdate:remove select='$list.selection/text()'/>
<xupdate:append select='$list.selection'>
<xupdate:text>${$text.value}</xupdate:text>
</xupdate:append>
</map>
<map src="addbutton.action">
<xupdate:append select='$list.context/html/body/ol'>
<xupdate:element name="li">
<xupdate:text>${$text.value}</xupdate:text>
</xupdate:element>
</xupdate:append>
</map>
<map src="delbutton.action">
<xupdate:remove select='$list.selection'/>
</map>
A the component element is only required to have two attributes, name and src. The name attribute must be a unique name within the components parent composite. The name attribute is used to identify the component. The src attribute is used to load the actual component code. There are currently two forms supported. Either the class:<class path> form or the sml:<sml file> form. The first loads a Java class. The second loads an SinaXe program as a component.
The ports, properties, queries and variables of a particular component are dependent on that component. Consult the documentation or source code of the component of interest.
<property name="text" value="A"/>
<property name="clickable"/>
<property name="menu">
<property name="item" value="openitem"/>
<property name="item" value="saveitem"/>
<property name="separator"/>
<property name="item" value="exititem"/>
</property>
Components may choose to create subscriptions from queries. A subscription is always associated with the query from which it was derived. Once a component has created a subscription it can run the query on that subscription with a particular context. The results of the query are returned as normal. From that point on if the XML data changes in any way that would change the results of the query, the component will be automatically notified of such changes. The subscription will remain active until the component either releases the subscription or reruns the query with a different context.
The benefit of subscriptions is that components automatically update their view of the data when it is changed. The down side is that if a subscription change event causes the component to fire new events a chain reaction of possibly unnecessary events can result. For this reason subscriptions should generally not actuate new events. Many components do not automatically subscribe to their data unless they are configured with the subscribe property. This is merely a convention and not every component follows it.
<component name="A" src="class:...."/>
<component name="B" src="class:....">
<map src="aquery">$A.value</map>
</component>
However, the following is valid:
<component name="A" src="class:...."/> <component name="B" src="class:...."/> <map src="B.aquery">$A.value</map>
Event mappings are used for three distinct purposes. To map events from one component's output port to another components input port, to map an input or an output port from a component to its parent's interface, or to map an output port to an XUpdate statement. Examples of these three uses are given below:
<composite name="parent">
<component name="A" src="class:..."/>
<component name="B" src="class:..."/>
<!-- Connecting components -->
<map src="A.out" dst="B.in"/>
<!-- Mapping to the parent interface -->
<map src="input" dst="A.in"/>
<map src="B.out" dst="output"/>
<!-- Mapping to XUpdate -->
<map src="A.update">
<xupdate:remove select='.'/>
</map>
</composite>
Property and variables mappings are used only to map properties and variables up to the parent component. An example follows:
<composite name="parent">
<component name="A" src="class:..."/>
<map src="A.aproperty" property="aproperty"/>
<map src="value" variable="A.value"/>
</composite>
Notice that variable mappings run in the opposite direction as property mappings. This is because properties are requested from the component whereas variables are queried from outside the composite.
Query mappings take two forms. A query may either be mapped to its parent's interface or directly to an XPath statement. A components query mapping can also occur inside the components definition or out. Examples follow:
<composite name="parent">
<component name="A" src="class:...">
<!-- A mapping to an XPath statment from with in the component definition. -->
<map src="aquery">selects/some/nodes</map>
<component>
<!-- An XPath mapping outside the component definition -->
<map src="A.anotherquery">select/some/@attribute</map>
<!-- A query mapping to the parent -->
<map src="A.yetanotherquery" query="parentquery"/>
</composite>
To summarize map elements always have a src attribute and may have one of the dst, property, variable or query attributes or none. If the second attribute is not present the mapping is either to an XPath in the case of a query mapping or an XUpdate in the case of an out port mapping. Any other combinations are invalid.
<composite name="parent">
<component name="ReallyAComposite" src="sml:AComposite.sml"/>
<composite name="AChildComposite">
<component name="A" src="class:..."/>
<component name="B" src="sml:..."/>
</composite>
</composite>
Special layout components are provided to allow more complex layouts. These components are able to read a special layout property which is allowed to contain panel and port elements. The top most element of the layout property must either be a single port or a panel. Panels can contain ports and other panels. Ports refer to components in the current composite component. A component can only appear in one place in the layout. The component attribute of the port element specifies which component shall be displayed in that port. Both ports and panels have a layout property. The value of this property is dependent on the specific layout component in use, but it is used to configure the layout. Currently there are two layout components; SinaxePackerLayout and SinaxeGridLayout.
SinaXe GUI components are implemented as Java classes which implement the SinaxeComponent interface. All SinaXe components must implement this interface. In the current implementation of SinaXe most of the SinaxeComponents are merely wrappers for Java Swing components and therefore extend a Swing component directly. The SinaxeComponent interface is listed below.
public interface SinaxeComponent {
public void sinaxeInit(SinaxeRuntime runtime, SinaxeProperty properties)
throws SinaxeException;
public void sinaxeExit() throws SinaxeException;
public Component sinaxeGetComponent() throws SinaxeException;
public String sinaxeGetDocumentation();
public Object sinaxePointToContext(Point source) throws SinaxeException;
}
The call to sinaxeInit passes two parameters, the SinaxeRuntime and the SinaxeProperty tree. The component must register its event input and output ports, variables, and queries with the SinaxeRuntime object during the call to sinaxeInit. In addition the component should examine its properties and configure itself accordingly. The SinaxeProperty structure is an object based view of the XML properties specified in the SinaXe markup.
Since some properties, ports, variables and queries are common to a group of components or even all components, some general purpose initialization functions are provided as static methods of the SinaxeUtil class. These default initializations are performed by calling a SinaxeUtil.loadDefaultInits method. For example, mouse events which my be generated by any GUI component are registered here.
Both the SinaxeRuntime and SinaxeProperty objects are described in more detail below.
To access the Java Component interface of another SinaxeComponent a call to SinaxeRuntime.getNeighborGraphic is necessary. Given the name of the neighboring SinaxeComponent this will return the Component interface, null, or generate a run-time error if the SinaxeComponent is not found. The path to the neighboring component is specified by the SinaXe markup via a property.
public interface SinaxeRuntime extends SinaxeDataTypes {
public String getFullName();
public String getName();
public void exit(int exitCode);
public Component getNeighborGraphic(String name, RuntimeComponentBase context) throws SinaxeException;
public Component getNeighborGraphic(SinaxeProperty prop) throws SinaxeException;
public SinaxeVariable lookupVariable(String name) throws SinaxeException;
public SinaxeVariable registerVariable(String name) throws SinaxeException;
public SinaxeInPort getInPort(String name) throws SinaxeException;
public SinaxeOutPort getOutPort(String name) throws SinaxeException;
public SinaxeInPort registerInPort(String name, int datatype) throws SinaxeException;
public SinaxeOutPort registerOutPort(String name, int datatype) throws SinaxeException;
public SinaxeQuery getQuery(String port) throws SinaxeException;
public SinaxeQuery getQuery(String port, boolean required) throws SinaxeException;
public SinaxeSubscription getSubscription(String port) throws SinaxeException;
public SinaxeSubscription getSubscription(String port, boolean required) throws SinaxeException;
public void registerFunction(SinaxeFunction function) throws SinaxeException;
public void issueWarning(String warning, Exception cause);
public void issueWarning(String warning);
public void issueError(String error, Exception cause);
public void issueError(String error);
public void issueFatalError(String error, Exception cause);
public void issueFatalError(String error);
}
Events can be sent over output ports using the SinaxeOutPort handle directly. To receive events from an input port the SinaxeComponent must register a SinaxePortListener with the SinaxeInPort handle. For example, to register an input port with the name "in", which can only receive a number or a string you would write the following:
inPort = runtime.registerInPort("in", DT_NUMBER | DT_STRING);
inPort.addListener(new SinaxePortListener() {
public void sinaxePortEvent(SinaxeInPort port, Object data) {
if (data instanceof String) {
// Do something
} else if (data instanceof Number) {
// Do something else
}
}
});
Once a subscription is registered a SinaxeSubscriptionListener callback must be registered with the subscription handle. Then, before any callback will be received the subscription must be initialized by running a query on the subscription. This will cause the subscription to subscribe to the relevant XML data points which will cause callbacks to be called when the query result has changed. Subscriptions should be used with care as they can cause unexpected chain-reactions with in the SinaXe program and result in degraded performance.
Property names do not have to be unique. The get function will return a SinaxeProperty or null if it does not exist. The require function will generate an error if the specified property does not exist. The has function returns true of the named property exists. getIterator can be used to get either a list of all the child properties or only those matching the specified name.