Appendix C. Extensible XML authoring

C.1. Introduction

Spring supports adding custom schema-based extensions to the basic Spring XML format for defining and configuring objects. This section is devoted to detailing how you would go about writing your own custom XML object definition parsers and integrating such parsers into the Spring IoC container.

To facilitate the authoring of configuration files using a schema-aware XML editor, Spring's extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring's current XML configuration extensions that come with the standard Spring distribution, please first read the appendix entitled Appendix B, XML Schema-based configuration.

Creating new XML configuration extensions can be done by following these (relatively) simple steps:

  1. Authoring an XML schema to describe your custom element(s).

  2. Coding a custom INamespaceParser implementation (this is an easy step, don't worry).

  3. Coding one or more IObjectDefinitionParser implementations (this is where the real work is done).

  4. Registering the above artifacts with Spring (this too is an easy step).

What follows is a description of each of these steps. For the example, we will create an XML extension (a custom XML element) that allows us to configure objects of the type Regex (from the System.Text.RegularExpressions namespace) in an easy manner. When we are done, we will be able to define object definitions of type Regex like this:

<myns:regex id="regex"
            pattern="(^\d{5}$)|(^\d{5}-\d{4}$)"
            options="Compiled"/>

C.2. Authoring the schema

Creating an XML configuration extension for use with Spring's IoC container starts with authoring an XML Schema to describe the extension. What follows is the schema we'll use to configure Regex objects.

<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema id="myns"
           xmlns="http://www.mycompany.com/schema/myns"
           xmlns:xsd="http://www.w3.org/2001/XMLSchema"
           xmlns:objects="http://www.springframework.net"
           xmlns:vs="http://schemas.microsoft.com/Visual-Studio-Intellisense"
           targetNamespace="http://www.mycompany.com/schema/myns"
           elementFormDefault="qualified"
           attributeFormDefault="unqualified"
           vs:friendlyname="Spring Regex Configuration" vs:ishtmlschema="false"
           vs:iscasesensitive="true" vs:requireattributequotes="true"
           vs:defaultnamespacequalifier="" vs:defaultnsprefix=""
           >

  <xsd:import namespace="http://www.springframework.net"/>

  <xsd:element name="regex">
    <xsd:complexType>
      <xsd:complexContent>
        <xsd:extension base="objects:identifiedType">
          <xsd:attribute name="pattern" type="xsd:string" use="required"/>
          <xsd:attribute name="options" type="xsd:string" use="optional"/>
        </xsd:extension>
      </xsd:complexContent>
    </xsd:complexType>
  </xsd:element>

</xsd:schema>    

The emphasized line contains an extension base for all tags that will be identifiable (meaning they have an id attribute that will be used as the object identifier in the container). We are able to use this attribute because we imported the Spring-provided 'objects' namespace. The vs: prefixed elements are for better integration with intellisense in VS.NET.

The above schema will be used to configure Regex objects, directly in an XML application context file using the <myns:regex/> element.

<myns:regex id="usZipCodeRegex"
            pattern="(^\d{5}$)|(^\d{5}-\d{4}$)"
            options="Compiled"/>

Note that after we've created the infrastructure classes, the above snippet of XML will essentially be exactly the same as the following XML snippet. In other words, we're just creating an object in the container, identified by the name 'usZipCodeRegex' of type Regex, with a couple of constructor arguments set.

  <object id="usZipCodeRegex" type="System.Text.RegularExpressions.Regex, System">
    <constructor-arg name="pattern" value="(^\d{5}$)|(^\d{5}-\d{4}$)"/>
    <constructor-arg name="options" value="Compiled"/>
  </object>
[Note]Note

The schema-based approach to creating configuration format allows for tight integration with an IDE that has a schema-aware XML editor. Using a properly authored schema, you can use intellisense to have a user choose between several configuration options defined in the enumeration. The schema for creating IDbProvider instances shows the use of XSD enumerations.

C.3. Coding a INamespaceParser

In addition to the schema, we need an INamespaceParser that will parse all elements of this specific namespace Spring encounters while parsing configuration files. The INamespaceParser should in our case take care of the parsing of the myns:regex element.

The INamespaceParser interface is pretty simple in that it features just two methods:

  • Init() - allows for initialization of the INamespaceParser and will be called by Spring before the handler is used

  • IObjectDefinition Parse(Element, ParserContext) - called when Spring encounters a top-level element (not nested inside a object definition or a different namespace). This method can register object definitions itself and/or return a object definition.

Although it is perfectly possible to code your own INamespaceParser for the entire namespace (and hence provide code that parses each and every element in the namespace), it is often the case that each top-level XML element in a Spring XML configuration file results in a single object definition (as in our case, where a single <myns:regex/> element results in a single Regex object definition). Spring features a number of convenience classes that support this scenario. In this example, we'll make use the NamespaceParserSupport class:

using Spring.Objects.Factory.Xml;

namespace CustomNamespace
{
    [NamespaceParser(
        Namespace = "http://www.mycompany.com/schema/myns",
        SchemaLocationAssemblyHint = typeof(MyNamespaceParser),
        SchemaLocation = "/CustomNamespace/myns.xsd"
        )
    ]
    public class MyNamespaceParser : NamespaceParserSupport
    {
        public override void Init()
        {
            RegisterObjectDefinitionParser("regex", new RegexObjectDefinitionParser());
        }
    }
}

Notice that there isn't actually a whole lot of parsing logic in this class. Indeed... the NamespaceParserSupport class has a built in notion of delegation. It supports the registration of any number of IObjectDefinitionParser instances, to which it will delegate to when it needs to parse an element in it's namespace. This clean separation of concerns allows an INamespaceParser to handle the orchestration of the parsing of all of the custom elements in it's namespace, while delegating to IObjectDefinitionParsers to do the grunt work of the XML parsing; this means that each IObjectDefinitionParser will contain just the logic for parsing a single custom element, as we can see in the next step.

To help in the registration of the parser for this namespace, the NamespaceParser attribute is used to map the XML namespace string, i.e. http://www.mycompany.com/schema/myns, to the location of the XML Schema file as an embedded assembly resource.

C.4. Coding an IObjectDefinitionParser

A IObjectDefinitionParser will be used if the INamespaceParser encounters an XML element of the type that has been mapped to the specific object definition parser (which is 'regex' in this case). In other words, the IObjectDefinitionParser is responsible for parsing one distinct top-level XML element defined in the schema. In the parser, we'll have access to the XML element (and thus it's subelements too) so that we can parse our custom XML content, as can be seen in the following example:

using System;
using System.Text.RegularExpressions;
using System.Xml;
using Spring.Objects.Factory.Support;
using Spring.Objects.Factory.Xml;
using Spring.Util;

namespace CustomNamespace
{
   public class RegexObjectDefinitionParser : AbstractSimpleObjectDefinitionParser { 1

   protected override Type GetObjectType(XmlElement element)
   {
     return typeof (Regex); 2
   }

   protected override void DoParse(XmlElement element, ObjectDefinitionBuilder builder)
   {
       // this will never be null since the schema explicitly requires that a value be supplied
       string pattern = element.GetAttribute("pattern");
       builder.AddConstructorArg(pattern);

       // this however is an optional property
       string options = element.GetAttribute("options");
       if (StringUtils.HasText(options))
       {
          RegexOptions regexOptions = (RegexOptions)Enum.Parse(typeof (RegexOptions), options);
          builder.AddConstructorArg(regexOptions);
       }
   }

   protected override bool ShouldGenerateIdAsFallback
   {
      get { return true; }
   }
}

1

We use the Spring-provided AbstractSingleObjectDefinitionParser to handle a lot of the basic grunt work of creating a single IObjectDefinition.

2

We supply the AbstractSingleObjectDefinitionParser superclass with the type that our single IObjectDefinition will represent.

In this simple case, this is all that we need to do. The creation of our single IObjectDefinition is handled by the AbstractSingleObjectDefinitionParser superclass, as is the extraction and setting of the object definition's unique identifier. The property ShouldGenerateIdAsFallback will generate a throw-away object id incase one is not specified, this is useful when nesting object definitions.

C.5. Registering the handler and the schema

The coding is finished! All that remains to be done is to somehow make the Spring XML parsing infrastructure aware of our custom element; we do this by registering our custom INamespaceParser using a special configuration section handler. The location of the XML Schema in this example has been directly assoicated with the parser though the use of the Namespace attribute.

C.5.1. NamespaceParsersSectionHandler

The custom configuration section handler is of the type Spring.Context.Support.NamespaceParsersSectionHandler and is registered with .NET in the normal manner. The custom configuration section will simply point to the INamespaceParser implementation that has the Namespace attribute. For our example, we need to write the following:

<configuration>

  <configSections>
    <sectionGroup name="spring">
      <section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>
     </sectionGroup>
  </configSections>

  <spring>
    <parsers>
      <parser type="CustomNamespace.MyNamespaceParser, CustomNamespace" />
    </parsers>
  </spring>

</configuration>

C.6. Using a custom extension in your Spring XML configuration

Using a custom extension that you yourself have implemented is no different from using one of the 'custom' extensions that Spring provides straight out of the box. Find below an example of using the custom <regex/> element developed in the previous steps in a Spring XML configuration file.

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net"
         xmlns:myns="http://www.mycompany.com/schema/myns">

  <!-- as a top level object definition -->
  <myns:regex id="usZipCodeRegex"
              pattern="(^\d{5}$)|(^\d{5}-\d{4}$)"/>

  <object id="jobDetailTemplate" abstract="true">
    <property name="regex">
      <!-- as an inner object definition -->
       <myns:regex pattern="(^\d{5}$)|(^\d{5}-\d{4}$)"
                   options="Compiled"/>
    </property>
  </object>

</objects>

C.7. Further Resources

Find below links to further resources concerning XML Schema and the extensible XML support described in this chapter.