Web-based XML Editing with W3C XML Schema and XSLT, Part 2
June 25, 2003
Inserting Elements
In an earlier article we talked about an approach to automatic, form-based GUI generation based on XML Schema -- an approach which uses a single XSLT stylesheet, through which editing XML instance documents is made possible. Open issues remained as how to add new elements to an instance document and how to create an initial instance of our schema.
One of the most common uses for schemas is to verify that an XML document is valid according to a set of rules. A schema can be used to give the order in which elements must appear or their type. We are also able to define the cardinality of the elements in the schema. This information can be used to insert (or delete) elements to (or from) an instance document while keeping the instance document valid.
This article describes a concept in which elements can be inserted into an XML instance document through an automatically created form-based GUI, based on the XML Schema of the instance document.
Editing an XML instance document
In "Web-based XML Editing with W3C XML Schema and XSLT" the concept of editing existing instance documents, using the corresponding schema, was described. The following figure represents the concept.
Figure 1.Editing an XML instance document
In this concept the value of the elements in the instance document can be edited, but it does not provide a way to insert (or delete) elements into the instance document.
We take a look at our previous example (with a few changes marked as bold) of a schema (further labeled as XSD) called Person.xsd in which we define the elements a person can have plus the cardinality of each element:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema version="1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="person"> <xsd:complexType> <xsd:sequence> <xsd:element name="name" type="xsd:string"/> <xsd:element name="phone" type="xsd:string" maxOccurs="3"/> <xsd:element name="date_of_birth" type="xsd:date"/> <xsd:element name="course" minOccurs="0" maxOccurs="3"> <xsd:complexType> <xsd:sequence> <xsd:element name="course_name" type="xsd:string"/> <xsd:element name="course_code" type="xsd:string" minOccurs="0"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>
We imagine also that we have again created an instance document for an imaginary person called Marc (Marc.xml) using the Person.xsd:
<?xml version="1.0" encoding="UTF-8"?> <person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Person.xsd"> <name>Marc</name> <phone> 0153816546 </phone> <phone> 0630458920 </phone> <date_of_birth> 1978-01-21 </date_of_birth> <course> <course_name> TCP/IP </course_name> <course_code> T465 </course_code> </course> <course> <course_name> Java Programming </course_name> <course_code> J867 </course_code> </course> </person>
Inserting elements which appear in the instance document
Imagine you have an instance document which has a certain number of an element type. You want to insert an extra element of the element type into your document. The general idea for inserting elements through our GUI is as follows:
- The GUI places a plus (+) sign after the element if the XSD allows it
- The user can click on the plus sign requesting the insertion of that element
- A request is sent to the servlet with the name of the element and the location where the new element has to be inserted
- The servlet inserts the element and validates the document
- If the instance document (with the new element) is valid, it will be transformed into the GUI
- The user can now fill the value of the inserted element and save the document.
Now, using an example, we will work out the way the concept can be approached using XSLT and XML Schema.
In the Person.xsd example we defined a
course
element to have a minOccurs
of 0 and and a
maxOccurs
of 3. This way Marc.xml
can have at most 3 courses.
Having this cardinality information in our schema, we would like to find a way to insert elements to our XML file through the GUI while keeping the instance document valid.
To be able to insert elements, we will use the cardinality (minOccurs
and
maxOccurs
) defined in the XSD document and put a plus (+) sign after each
element which has a cardinality higher than the number of elements present in the
XML
instance document. For instance, for Marc.xml,
because Marc has two courses and the maxOccurs
of course
is
defined to be 3 the GUI will get a plus sign after each course.
The first thing we need to do is to extend our MetaXSLGUI which was presented in the previous article, so that the generated XSLGUI knows where and when to generate plus signs. The following is what our XSLGUI would look like after the extension:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:strip-space elements="*"/> ... <xsl:template match="course"> <xsl:param name="path"/> <xsl:variable name="index"> <xsl:number count="course"/> </xsl:variable> <xsl:variable name="maxOccurs" select="3"/> <xsl:variable name="nodeCount" select="count(../course)"/> <b>course </b> <xsl:if test="$nodeCount < $maxOccurs"> <a> <xsl:attribute name="href"> <xsl:value-of select="concat('javascript:submitForm(', "'",$path, '/course[', $index,']', "'", ',', "'",'insert', "'",');')"/> </xsl:attribute> <xsl:attribute name="onClick"> <xsl:value-of select="concat('submitForm(',"'", $path,'/course[', $index, ']', "'", ',', "'", 'insert', "'", '); return false;')"/> </xsl:attribute> + </a> </xsl:if> <xsl:apply-templates select="*"> <xsl:with-param name="path" select="concat($path,'/course[', $index,']')"/> </xsl:apply-templates> </xsl:template> ... <xsl:template match="phone"> <xsl:param name="path"/> <xsl:variable name="index"> <xsl:number count="phone"/> </xsl:variable> <xsl:variable name="maxOccurs" select="3"/> <xsl:variable name="nodeCount" select="count(../phone)"/> <b>phone: </b> <!-- The output of the following will look like this: <input name="/person/phone[2]" value="0630458920"/> --> <input> <xsl:attribute name="name"> <xsl:value-of select="concat($path,'/phone[', $index,']')"/> </xsl:attribute> <xsl:attribute name="value"> <xsl:value-of select="text()"/> </xsl:attribute> </input> <xsl:if test="$nodeCount < $maxOccurs"> <!-- The output of the following will look like: <a href="javascript:submitForm( '/person/phone[2]', 'insert');" onClick="submitForm(' /person/phone[2]', 'insert'); return false;"> +</a> --> <a> <xsl:attribute name="href"> <xsl:value-of select="concat('javascript:submitForm(', "'",$path, '/phone[', $index, ']', "'", ',', "'", 'insert', "'", ');')"/> </xsl:attribute> <xsl:attribute name="onClick"> <xsl:value-of select="concat('submitForm(',"'", $path,'/phone[', $index,']', "'", ',', "'", 'insert', "'",'); return false;')"/> </xsl:attribute> + </a> </xsl:if> </xsl:template> ... </xsl:stylesheet>
Here is the extended MetaXSLGUI.
The value of the maxOccurs
attribute in the XSD is passed to the template of
the element. On every match the number of elements in that position in the document
is
counted and set as nodeCount
. Then a check is done to see if
nodeCount
is smaller than the maxOccurs
of the element. If that
is the case the element will get a plus sign. The plus sign is then a link which submits
our
form with the appropriate parameters. (We could use the same trick to add minus signs
to
remove elements from the instance document.)
When the user clicks on the plus sign
next to the second course, a request will be sent to the server with the XPath position
of
the element for which an insert is requested. It is possible to insert the element
before or
after the element with the plus sign. In this example we insert the element before
the
element with the plus sign. Using the XPath notation we know that the new course element
has
to be inserted before the second course
in the person
element. The
server makes an XUpdate document with an insert-before command:
<?xml version="1.0" encoding="UTF-8"?> <xu:modifications xmlns:xu="http://www.xmldb.org/xupdate"> <xu:insert-before select="/person/course[2]"> <insert-course/> </xu:insert-before> </xu:modifications>
We don't insert the course element itself but, rather, an insert-course
element instead. The reason for that is because at the server side we do not have
any
knowledge of the structure of the element that has to be inserted. All we know from
the
incoming request is that the user requests to insert a course
element before
the second course element (/person/phone[2]
).
This XUpdate document will be executed on a temporary copy of the original (Marc.xml) instance document. Now we have an XML document (labeled as XML+) with an additional tag which is not defined in our XSD but is merely meant as a temporary help tag:
<?xml version="1.0" encoding="UTF-8"?> <person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Person.xsd"> <name>Marc</name> <phone>0153816546</phone> <phone>0630458920</phone> <date_of_birth>1978-01-21</date_of_birth> <course> <course_name>TCP/IP</course_name> <course_code>T465</course_code> </course> <insert-course/> <course> <course_name>Java Programming</course_name> <course_code>J867</course_code> </course> </person>
See the following figure.
Figure 2. Inserting elements
To be able to transform this XML+ to our XML document, we have to have an XSLT which
knows
how to substitute an <insert-elementName/>
tag with the element tag it
should really be. This XSLT (called XSL+) is created using the XSD. In this XSLT for
every
element of the XSD the corresponding match is created with the element(s) which has
to
substitute the insert-tag in XML+. The XSL+ has the function of substituting the insert
tag
while keeping the original structure and values of the elements intact
The following shows parts of the XSL+:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:strip-space elements="*"/> <xsl:template match="person"> <xsl:element name="person"> <xsl:apply-templates select="*"/> </xsl:element> </xsl:template> ... <xsl:template match="insert-phone"> <xsl:element name="phone"/> </xsl:template> <xsl:template match="insert-course"> <xsl:element name="course"> <xsl:element name="course_name"/> </xsl:element> </xsl:template> <xsl:template match="insert-person"> <xsl:element name="person"> <xsl:element name="name"/> <xsl:element name="date_of_birth"/> <xsl:element name="phone"/> </xsl:element> </xsl:template> ... </xsl:stylesheet>
The XML+ with the insert-course
tag will be transformed to a valid instance
document which is the same as the original XML document plus a course
element.
Marc.xml would now look like:
<?xml version="1.0" encoding="UTF-8"?> <person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="Person.xsd"> <name>Marc</name> <phone>0153816546</phone> <phone>0630458920</phone> <date_of_birth>1978-01-21</date_of_birth> <course> <course_name>TCP/IP</course_name> <course_code>T465</course_code> </course> <course> <course_name/> </course> <course> <course_name> Java Programming </course_name> <course_code> J867 </course_code> </course> </person>
Only the course_name
is added into the course
element. This is
because course_name
has a minOccurs of 1 (by default) and
course_code
has a minOccurs of 0, which means that it is not a mandatory
element.
After validation this XML document can be transformed into a GUI using our
XSLGUI, just like any other instance document.
MetaXSL+
What we need is an XSLT stylesheet which takes our XSD and transforms it into the corresponding XSL+. The concept applied is the same as the concept used for the generation of XSLGUI. The stylesheet which generates the XSL+ is called MetaXSL+ here.
Figure 3. XML Schema to XSL+ processor
The basic task of the MetaXSL+ is as follows:
- For every element make an
xsl:template
with thematch
attribute equal to the name of the element. - If the element is a datatype element, add it and the functions needed to produce the
value of the element to this
xsl:template
- If the element is complexType, add the
xsl:apply-templates select="*"
function - For every element make an
xsl:template
with thematch
attribute equal toinsert-elementName
where elementName is the name of the element. - If the element is complexType, add each child element (recursively) N times where N is the minOccurs of the child element
Creating new instance documents
We saw that the XSL+ is able to transform an insert-elementName
tag into the
corresponding element with the correct body. Now making an instance of Person.xsd is nothing else but a
insert-person
tag in a document (bootstrap). All we need is an XSLT which
finds the first element of our XSD (person) and creates an XML+ document which consists
of
only one line namely:
<insert-person>
The following XSLT does the trick:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsl:output method="xml" indent="yes"/> <xsl:strip-space elements="*"/> <xsl:template match="xsd:element"> <xsl:variable name="inserttag" select="concat('insert-', @name)"/> <xsl:element name="{$inserttag}"/> </xsl:template> </xsl:stylesheet>
Using the same cycle as described above this new XML+ document will be transformed into an instance document using the XSL+. The result will be
<xsl:element name="person"> <xsl:element name="name"/> <xsl:element name="date_of_birth"/> <xsl:element name="phone"/> </xsl:element>
which is an initial instance of Person.xsd.
Inserting elements which do not appear in the instance document
The question now is how can we add an element which does not appear in the new instance
document (an element with "minOccurs=0") such as the course
in a new person
instance or the course_code
in a new course
element.
There are different approaches possible:
- When making a new instance document, add all the elements to the document, including elements with a minOccurs equal to zero. This approach is an easy way out, and even though the created instance document is a valid document, it is not what we expect: an empty element is not equal to not having that element in the instance document.
- Adjust the XSLGUI so that it generates plus signs after each element. In our
person example we would have a plus sign for
course
aftername
, one afterdate_of_birth
and one afterphone
. This way if the user clicks on either of the first two plus signs the course element will be inserted at a place which is not allowed according to our XSD. Aninsert-course
after the phone element will be valid. For an instance document with a huge number of elements this approach can be frustrating for the user as there will probably be lots of attempts before the user can find a valid location. - Find a way through trial and error (validation) to know exactly where plus signs are meaningful and give that knowledge to the XSLGUI stylesheet. Conceptually it is an elegant solution but technically it is quite an effort to implement.
- Another solution and the one chosen in this article is to produce plus signs for the children of a complexType element at that level as follows. For all the children, if the child has a minOccurs equal to 0, and if the number of element occurrences in the document is equal to 0, produce a plus sign at the level of the complexType element containing the children.
In our example person
is a complexType and its child course
has a
minOccurs equal to 0. The XSLGUI will be generated as follows:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:strip-space elements="*"/> <xsl:template match="person"> <xsl:param name="root"> /person </xsl:param> <xsl:variable name="index"> 1 </xsl:variable> <html> <head> <title>person</title> </head> <body> <form name="xmsForm" action="xms.InputPage" method="post"> <!-- The output of the following will look like: <a href="javascript:submitForm( '/person[1]/course', 'insertfirstposition');" onClick="submitForm( '/person[1]/course', 'insertfirstposition'); return false;"> Add course + </a> --> <xsl:variable name="courseCount" select="count(course)"/> <xsl:if test="$courseCount='0'"> <a> <xsl:attribute name="href"> <xsl:value-of select="concat('javascript:submitForm(', "'",'/person[', $index,']','/course', "'",',', "'", 'insertfirstposition', "'",');')"/> </xsl:attribute> <xsl:attribute name="onClick"> <xsl:value-of select="concat('submitForm(', "'",'/person[', $index,']','/course', "'",',', "'", 'insertfirstposition', "'",'); return false;')"/> </xsl:attribute>Add course + </a> </xsl:if> <xsl:apply-templates> <xsl:with-param name="path" select="$root"/> </xsl:apply-templates> <input type="submit" name="action" value="save"/> </form> </body> </html> </xsl:template> ... </xsl:stylesheet>
Now at the person level (top level), if the user clicks on the plus sign to insert
a
course, a request will be sent to the server with the action
insertfirstposition
and the XPath parameter will be
/person/course
. We know that the new instance document does not contain any
course elements (otherwise we would not get a plus sign for course
), so there
is no way we could know where to insert a course element. All we know is that it has
to be
inserted at /person
. Then, beginning at the first position in
/person
, insert the course
and validate the document. If valid,
we will use the XSLGUI to produce the GUI; otherwise, we will insert the element
at the next position till we find a valid position for the course
. This
approach inserts the element at the first allowed location in the original instance
document. Note that it might be desirable to insert the element not at the first possible
location but somewhere else. This could become an issue in very complex combinations
of
elements.
The XUpdate document used to insert the new element at different positions is as follows:
<?xml version="1.0" encoding="ISO-8859-1"?> <xu:modifications xmlns:xu="http://www.xmldb.org/xupdate"> <xu:append select="/person[1]" child=index> <xu:element name="insert-course"/> </xu:append> </xu:modifications>
Where index starts at 0 and increases by one till there is a valid position found.
Conclusion
This article described a concept in which elements can be inserted into an XML instance document through an auto-generated, form-based GUI, based on the XML Schema of the instance document and XSLT. The capability of editing and inserting or removing elements using the corresponding XML Schema makes it a complete and functional approach for the implementation of an XML web-based editor.