Thursday, September 6, 2012

Spring Provisional/Conditional Bean Loading (Part 2)

In Part1 I explained two solutions for dynamically loading beans based on environments.  In this Part 2, I (quickly) extend the solution to handle conditional Bean Definition File imports.  Below is my new main.xml with a new custom tag, <profile:importIf>.  This new tag, along with the <profile:if> tag, will allow me to control individual bean and entire bean definition file loads via properties files.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:oxm="http://www.springframework.org/schema/oxm" xmlns:lang="http://www.springframework.org/schema/lang"
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:profile="http://icfi.com/springbeans/profile"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
         http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
         http://icfi.com/springbeans/profile http://icfi.com/springbeans/profile/profile.xsd">

 <import resource="beans.xml" />
 <import resource="beans2.xml" />
 <profile:if test="${Spring.ENV=='TEST'}" src="config.properties">
  <bean id="testBean" class="com.icfi.spring.init.beans.TestBean"
   name="ibean">
   <property name="valueOne" value="This is TEST." />
  </bean>
 </profile:if>

 <profile:if test="${Spring.ENV=='PROD'}" src="config">
  <bean id="prodBean" class="com.icfi.spring.init.beans.ProdBean"
   name="ibean">
   <property name="valueOne" value="This is PROD." />
  </bean>
 </profile:if>

 <profile:importIf test="${Spring.ENV=='DEV'}" src="config.properties"
  resource="context/DEV-beans.xml" />
</beans>
To make this work I have to modify some additional artifacts, profile.xsd, ProfileBeanNamespaceHandler.java, and ProfileBeanDefinitionParser.java.  These new artifacts are seen below.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://icfi.com/springbeans/profile"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans"
 targetNamespace="http://icfi.com/springbeans/profile"
 elementFormDefault="qualified" attributeFormDefault="unqualified">

 <xsd:element name="if">
  <xsd:complexType>
   <xsd:sequence>
    <xsd:any minOccurs="0" />
   </xsd:sequence>
   <xsd:attribute name="test" type="xsd:string" use="required" />
   <xsd:attribute name="src" type="xsd:string" use="required" />
  </xsd:complexType>
 </xsd:element>
 
 <xsd:element name="importIf">
  <xsd:complexType>
   <xsd:sequence>
    <xsd:any minOccurs="0" />
   </xsd:sequence>
   <xsd:attribute name="test" type="xsd:string" use="required" />
   <xsd:attribute name="src" type="xsd:string" use="required" />
   <xsd:attribute name="resource" type="xsd:string" use="required" />
  </xsd:complexType>
 </xsd:element>

</xsd:schema>

package com.icfi.springbeans.profile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class ProfileBeanNamespaceHandler extends NamespaceHandlerSupport {
 private static Logger log = LoggerFactory
 .getLogger(ProfileBeanNamespaceHandler.class);
 
 public void init() {
  log.debug(this.getClass().getSimpleName()+"::initStart");
  super.registerBeanDefinitionParser("if",
    new ProfileBeanDefinitionParser());
  super.registerBeanDefinitionParser("importIf",
    new ProfileBeanDefinitionParser());
  log.debug(this.getClass().getSimpleName()+"::initEnd");
 }
}

package com.icfi.springbeans.profile;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class ProfileBeanDefinitionParser implements BeanDefinitionParser {
 private static Logger log = LoggerFactory
   .getLogger(ProfileBeanDefinitionParser.class);

 private ResourceBundle bundle;

 /** SpEL prefix: "${" */
 public static final String PREFIX = "${";

 /** SpEL suffix: "}" */
 public static final String SUFFIX = "}";

 /**
  * Parse the "if" element, check for the required "test" and "src"
  * attributes.
  */
 public BeanDefinition parse(Element element, ParserContext parserContext) {
  try {
   if (DomUtils.nodeNameEquals(element, "if")
     || DomUtils.nodeNameEquals(element, "importIf")) {
    String test = element.getAttribute("test");
    String src = element.getAttribute("src");

    if (StringUtils.isNotEmpty(src)) {
     if (src.indexOf(".") > 0) {
      src = src.substring(0, src.indexOf("."));
     }

     bundle = ResourceBundle.getBundle(src);
    } else {
     throw new IllegalArgumentException(
       "src attribute not found.");
    }

    if (StringUtils.isNotEmpty(test)) {
     Map<String, String> map = this.getExpressionMap(test);

     String left = this.bundle.getString(map.get("left"));
     String right = map.get("right");

     if (left != null && right != null && left.equals(right)) {
      if (DomUtils.nodeNameEquals(element, "if")) {
       Element beanElement = DomUtils
         .getChildElementByTagName(element, "bean");
       return registerBean(beanElement, parserContext);
      } else if (DomUtils.nodeNameEquals(element, "importIf")) {
       String resource = element.getAttribute("resource");

       InputStream is = parserContext.getReaderContext()
         .getResourceLoader().getResource(resource)
         .getInputStream();

       Document doc = this.parse(is);
       NodeList elements = doc
         .getElementsByTagName("bean");
       for (int x = 0; x < elements.getLength(); x++) {
        Element bean = (Element) elements.item(x);
        this.registerBean(bean, parserContext);
       }
      }
     }
    } else {
     throw new IllegalArgumentException(
       "test attribute not found.");
    }
   }
  } catch (Exception e) {
   log.error(e.getMessage());
  }

  return null;
 }

 private Map<String, String> getExpressionMap(String value) {
  Map<String, String> map = new HashMap<String, String>();

  if (StringUtils.isEmpty(value)) {
   return null;
  }

  String entire = value.substring(PREFIX.length(), value.length()
    - SUFFIX.length());

  String left = entire.substring(0, entire.indexOf("=="));

  String right = entire.substring(entire.indexOf('\'') + 1,
    entire.lastIndexOf('\''));

  map.put("left", left);
  map.put("right", right);

  return map;
 }

 /*
  * Register Bean
  * 
  * @param element
  * 
  * @param parserContext
  * 
  * @return
  */
 private BeanDefinition registerBean(Element element,
   ParserContext parserContext) {
  BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
  BeanDefinitionHolder holder = delegate
    .parseBeanDefinitionElement(element);
  BeanDefinitionReaderUtils.registerBeanDefinition(holder,
    parserContext.getRegistry());

  return holder.getBeanDefinition();
 }

 /*
  * JAXP Parser
  * 
  * @param is
  * 
  * @return
  * 
  * @throws Exception
  */
 private Document parse(InputStream is) throws Exception {
  DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
  DocumentBuilder builder = dbfactory.newDocumentBuilder();
  Document doc = builder.parse(is);
  return doc;
 }
}
This new solution will load individual beans based on conditions defined in Bean Definition Files, and will now import entire Bean Definition Files based on those same declared conditions. Of course the same caveats apply. Developers must manage bean ID and NAME collisions when using multiple conditions and imports.

No comments:

Post a Comment