Thursday, August 6, 2015

Spring Custom Tags with Spring's Extensible XML Authoring

Spring Custom Tags with Spring's Extensible XML Authoring

In situations where we have our custom xml in place and we want to inject(plugin) our xml configuration in spring context ,  Spring's Extensible XML Authoring makes it possible to do the same. With Spring's Extensible XML Authoring we can define our own namespace and have our own custom elements with custom attributes. I am going to show how can we configure our custom xml elements in a spring bean context. Prior to go through this blog , it is required to have basic knowledge of XML and Spring bean configurations.     

Below jars are required :

1) org.springframework.beans-3.0.0.RELEASE.jar
2) org.springframework.core-3.0.0.RELEASE.jar
3) org.springframework.context-3.0.0.RELEASE.jar
4) commons-logging-1.1.jar


Below I have defined a custom element "ce:customElement" which have custome child element "ce:component" which can occur more than once with one attribute "componentName". Below is snippet of how we write the custom element in a spring config file .



<ce:customElement id="customEl" >
        <ce:component componentName="component1"  >
        </ce:component>
</ce:customElement>


Below are the steps that we will follow to achieve our goal :
1) Define customElement XSD .
2) Write Namespace Handler for customElement .
3) Write Parser for customElement XSD which will register to Spring BeanDefinition.
4) Create spring.handlers and spring.schemas to map the schemas and handlers.
5) Write one bean which will be injected to our custom child element "component" .
6) Finally are ready to use the custom elements to configure in spring bean context file.

Define customElement XSD

I have defined an XSD below with custom element.

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://customschemanamespace.springextn.prasenjit.com/customElement"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"
 targetNamespace="http://customschemanamespace.springextn.prasenjit.com/customElement"
 elementFormDefault="qualified" attributeFormDefault="unqualified">

 <xsd:import
  schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"
  namespace="http://www.springframework.org/schema/beans" />
 <xsd:element name="customElement">
  <xsd:complexType mixed="true">
   <xsd:complexContent>
    <xsd:extension base="beans:identifiedType">
     <xsd:sequence>
      <xsd:element ref="component" minOccurs="0" maxOccurs="unbounded"></xsd:element>
     </xsd:sequence>
    </xsd:extension>
   </xsd:complexContent>
  </xsd:complexType>
 </xsd:element>

 <xsd:element name="component">
  <xsd:complexType>
   <xsd:complexContent>
    <xsd:extension base="beans:identifiedType">
     <xsd:group ref="beans:beanElements" />
     <xsd:attributeGroup ref="beans:beanAttributes"/>
     <xsd:attribute name="componentName" type="xsd:string" use="required" />
    </xsd:extension>
   </xsd:complexContent>
  </xsd:complexType>
 </xsd:element>

</xsd:schema>

Above xsd I have 2 custom elements "customElement" and "component" . The element named "customElement" has child element "component" which can occur 0 and more times.

Namespace Handler for handling the Custom Namespace

package com.prasenjit.springextn.customschemanamespace.parser;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class CustomElementNameSpaceHandler extends NamespaceHandlerSupport {
    private static final String NAME_CUSTOM_FOO_ELEMENT = "customElement";

    public void init() {
     registerBeanDefinitionParser(NAME_CUSTOM_FOO_ELEMENT, new CustomElementSchemaParser());
    }
    
}

BeanDefinitionParser for Custom Namespace

package com.prasenjit.springextn.customschemanamespace.parser;
import java.util.List;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import com.prasenjit.springextn.customschemanamespace.Component;
import com.prasenjit.springextn.customschemanamespace.CustomElement;

public class CustomElementSchemaParser extends AbstractBeanDefinitionParser {
    private static final String NAME_CUSTOM_FOO_ELEMENT = "customElement";

    @Override
    protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
      BeanDefinitionBuilder customElement = BeanDefinitionBuilder.rootBeanDefinition(CustomElement.class);
      List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
         System.out.println(childElements.size());
         ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
         for (Element element2 : childElements) {
         BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class); 
         component.getBeanDefinition().getPropertyValues().addPropertyValue("componentName", element2.getAttribute("componentName"));
    System.out.println(element2.getAttribute("componentName"));
    List<Element> childElements1=DomUtils.getChildElementsByTagName(element2, "property");
    System.out.println(childElements1.size());
    for (Element element3 : childElements1) {
     parserContext.getDelegate().parsePropertyElement(element3, component.getBeanDefinition());
   }
    children.add(component.getBeanDefinition());
   }
         
        customElement.getBeanDefinition().getPropertyValues().addPropertyValue("components", children) ;
        parserContext.getRegistry().registerBeanDefinition(NAME_CUSTOM_FOO_ELEMENT, customElement.getBeanDefinition());
        return (AbstractBeanDefinition)customElement.getBeanDefinition();
    }

}

spring handlers and schemas file

Once we are done with the above steps which involves coding and defining XSD schema , we have to define two more files for spring to plugin custom namespace handler and the XSD schema file which we have written above . Create the below files spring.handlers and spring.schemas and place the files under META-INF directory of your project . Make sure META-INF is also included in your classpath or bundled in jar .

spring.handlers
http\://customschemanamespace.springextn.prasenjit.com/customElement=com.prasenjit.springextn.customschemanamespace.parser.CustomElementNameSpaceHandler

spring.schemas
http\://customschemanamespace.springextn.prasenjit.com/customElement/customElement.xsd=META-INF/CustomElement.xsd

My CustomElement  which will be created as bean while parsing will be registered to Spring Bean context.

CustomElement.java

package com.prasenjit.springextn.customschemanamespace;
import java.util.List;

public class CustomElement {
   
    private List<Component> components;
    
 public List<Component> getComponents() {
  return components;
 }

 public void setComponents(List<Component> components) {
  this.components = components;
 }

}

Component.java

package com.prasenjit.springextn.customschemanamespace;
public class Component {

    private String componentName;
    private IModule module;
 public String getComponentName() {
  return componentName;
 }
 public void setComponentName(String componentName) {
  this.componentName = componentName;
 }
 public IModule getModule() {
  return module;
 }
 public void setModule(IModule module) {
  this.module = module;
 }
   

}


Below I have created some beans which will be injected in "component" as a property. 

package com.prasenjit.springextn.customschemanamespace;

public interface IModule {

 public int getCount();

 public String getName();

}

Module.java

package com.prasenjit.springextn.customschemanamespace;

public class Module implements IModule{
    private int count;
    private String name;
 public int getCount() {
  return count;
 }

 public void setCount(int count) {
  this.count = count;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

        // getter and setters omitted
}

Final Step

We will define our new custom tag in a spring context file . Then we will write a test program to Demo the configurations .

customelements.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:ce="http://customschemanamespace.springextn.prasenjit.com/customElement"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://customschemanamespace.springextn.prasenjit.com/customElement http://customschemanamespace.springextn.prasenjit.com/customElement/customElement.xsd">
     
     <bean class="com.prasenjit.springextn.customschemanamespace.Module" id="module1">
                 <property name="count" value="100" />
                 <property name="name" value="module1" />
      </bean>
            
    <ce:customElement id="customEl" >
        <ce:component componentName="component1"  >
          <property name="module" ref="module1">
         </property>
        </ce:component>
        <ce:component componentName="component2"  >
         <property name="module">
             <bean class="com.prasenjit.springextn.customschemanamespace.Module">
                 <property name="count" value="10" />
                 <property name="name" value="module2" />
             </bean>
         </property>
        </ce:component>
    </ce:customElement>
    
</beans>

Test the configurations we did using custom tag with below code :

package com.prasenjit.springextn.customschemanamespace;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestCustomSpringTag {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext("classpath:com/prasenjit/springextn/customschemanamespace/customelements.xml");
        CustomElement customEl = (CustomElement) appContext.getBean("customEl");
        System.out.println("count = " + customEl.getComponents().size());
        if (!customEl.getComponents().isEmpty()) {
   for (Component component : customEl.getComponents()) {
    System.out.println(component.getComponentName());
    System.out.println(component.getModule().getCount()+"::"+component.getModule().getName());
   }
  }
    }
}

Run the above Test code to check the configuration works properly or not . I will be uploading my source codes shown in this blog soon .

Please add valuable feedback/comments about this post .

Thanks.


References:

The below articles and websites are used for reference .

  • http://docs.spring.io/spring-framework/docs/3.0.7.RELEASE/reference/extensible-xml.html
  • http://cscarioni.blogspot.in/2012/04/creating-custom-spring-3-xml-namespace.html