I think this might be the case to most people as well. In this article I will show step by step how to create custom namespaces for their use with Spring. It won’t be a very involved example but neither will it be a trivial useless one.
As currently I’m working with recommendations engines and in particular looking at Mahout, I will define a Spring namespace for using Mahout recommenders in Spring.
The following is what I want to achieve with my namespace:
- <user-based-recommender id =”recommender”>
- <euclidean-distance-similarity/>
- <file-model path = “/tmp/model.csv”/>
- <nearest-neighborhood size = “2”/>
- </user-based-recommender>
The idea would be to allow any kind of similarity to be used and any kind of recommender and any kind of data model and any kind of neighbour definition. But for my example I will just admit the ones defined in the previous XML snippet.
When we create a namespace in Spring we are simply creating a better way to express standard Spring beans in a more Domain Specific Language. In our case we are using a language to express recommendations backed by Mahout. What this means is that our little XML snippet will be equivalent to the following standard beans definitions.
<bean id="recommenderRegularBean" class="org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender">
<constructor-arg ref="dataModel"/>
<constructor-arg ref="userNeighbourhood"/>
<constructor-arg ref="similarity"/>
</bean>
<bean id="dataModel"
class="org.apache.mahout.cf.taste.impl.model.file.FileDataModel">
<constructor-arg value="/tmp/model.csv" />
</bean>
<bean id="similarity" class="org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity">
<constructor-arg ref="dataModel"/>
</bean>
<bean id="userNeighbourhood" class="org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood">
<constructor-arg value="2"/>
<constructor-arg ref="similarity"/>
<constructor-arg ref="dataModel"/>
</bean>
<constructor-arg ref="dataModel"/>
<constructor-arg ref="userNeighbourhood"/>
<constructor-arg ref="similarity"/>
</bean>
<bean id="dataModel"
class="org.apache.mahout.cf.taste.impl.model.file.FileDataModel">
<constructor-arg value="/tmp/model.csv" />
</bean>
<bean id="similarity" class="org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity">
<constructor-arg ref="dataModel"/>
</bean>
<bean id="userNeighbourhood" class="org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood">
<constructor-arg value="2"/>
<constructor-arg ref="similarity"/>
<constructor-arg ref="dataModel"/>
</bean>
The first thing we need is to create a xsd schema for our XML. As I’m not particularly good at this, I will generate one from my xml with a tool and configure it if needed. I will use the tool “trang”
I generate the following xsd:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns="http://www.mycompany.com/schema/recommendations" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.mycompany.com/schema/recommendations">
<xs:element name="user-based-recommender">
<xs:complexType>
<xs:sequence>
<xs:element ref="euclidean-distance-similarity"/>
<xs:element ref="file-model"/>
<xs:element ref="nearest-neighborhood"/>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:ID"/>
</xs:complexType>
</xs:element>
<xs:element name="euclidean-distance-similarity">
<xs:complexType/>
</xs:element>
<xs:element name="file-model">
<xs:complexType>
<xs:attribute name="path" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="nearest-neighborhood">
<xs:complexType>
<xs:attribute name="size" use="required" type="xs:integer"/>
</xs:complexType>
</xs:element>
</xs:schema>
The next step is to define a NamespaceHandler which will take care of registering a particular BeanDefinitionParser for each of the top elements we have in our XML namespace. In our case we create this class:<xs:schema xmlns="http://www.mycompany.com/schema/recommendations" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.mycompany.com/schema/recommendations">
<xs:element name="user-based-recommender">
<xs:complexType>
<xs:sequence>
<xs:element ref="euclidean-distance-similarity"/>
<xs:element ref="file-model"/>
<xs:element ref="nearest-neighborhood"/>
</xs:sequence>
<xs:attribute name="id" use="required" type="xs:ID"/>
</xs:complexType>
</xs:element>
<xs:element name="euclidean-distance-similarity">
<xs:complexType/>
</xs:element>
<xs:element name="file-model">
<xs:complexType>
<xs:attribute name="path" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="nearest-neighborhood">
<xs:complexType>
<xs:attribute name="size" use="required" type="xs:integer"/>
</xs:complexType>
</xs:element>
</xs:schema>
package org.springframework.recommendations;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class RecommendationsNamespaceHandler extends NamespaceHandlerSupport{
public void init() {
registerBeanDefinitionParser("user-based-recommender", new UserBasedRecommenderBeanDefinitionParser());
}
}
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class RecommendationsNamespaceHandler extends NamespaceHandlerSupport{
public void init() {
registerBeanDefinitionParser("user-based-recommender", new UserBasedRecommenderBeanDefinitionParser());
}
}
Next we need to implement the BeanDefinitionParsers. This is the one class that will actually take care of the parsing of the XML elements.
package org.springframework.recommendations;
import java.util.List;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
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;
public class UserBasedRecommenderBeanDefinitionParser extends AbstractBeanDefinitionParser{
@Override
protected AbstractBeanDefinition parseInternal(Element element,
ParserContext parserContext) {
BeanDefinitionBuilder recommenderBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(GenericUserBasedRecommender.class);
BeanDefinitionBuilder similarityBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(EuclideanDistanceSimilarity.class);
BeanDefinitionBuilder neighborhoodBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(NearestNUserNeighborhood.class);
BeanDefinitionBuilder modelBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(FileDataModel.class);
Element similarity = DomUtils.getChildElementsByTagName(element, "euclidean-distance-similarity").get(0);
Element neighborhood = DomUtils.getChildElementsByTagName(element, "nearest-neighborhood").get(0);
Element model = DomUtils.getChildElementsByTagName(element, "file-model").get(0);
String filePath = model.getAttribute("path");
String neighborSize = neighborhood.getAttribute("size");
modelBeanBuilder.addConstructorArgValue(filePath);
similarityBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
neighborhoodBeanBuilder.addConstructorArgValue(neighborSize);
neighborhoodBeanBuilder.addConstructorArgValue(similarityBeanBuilder.getBeanDefinition());
neighborhoodBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(neighborhoodBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(similarityBeanBuilder.getBeanDefinition());
return recommenderBeanBuilder.getBeanDefinition();
}
}
import java.util.List;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
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;
public class UserBasedRecommenderBeanDefinitionParser extends AbstractBeanDefinitionParser{
@Override
protected AbstractBeanDefinition parseInternal(Element element,
ParserContext parserContext) {
BeanDefinitionBuilder recommenderBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(GenericUserBasedRecommender.class);
BeanDefinitionBuilder similarityBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(EuclideanDistanceSimilarity.class);
BeanDefinitionBuilder neighborhoodBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(NearestNUserNeighborhood.class);
BeanDefinitionBuilder modelBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(FileDataModel.class);
Element similarity = DomUtils.getChildElementsByTagName(element, "euclidean-distance-similarity").get(0);
Element neighborhood = DomUtils.getChildElementsByTagName(element, "nearest-neighborhood").get(0);
Element model = DomUtils.getChildElementsByTagName(element, "file-model").get(0);
String filePath = model.getAttribute("path");
String neighborSize = neighborhood.getAttribute("size");
modelBeanBuilder.addConstructorArgValue(filePath);
similarityBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
neighborhoodBeanBuilder.addConstructorArgValue(neighborSize);
neighborhoodBeanBuilder.addConstructorArgValue(similarityBeanBuilder.getBeanDefinition());
neighborhoodBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(modelBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(neighborhoodBeanBuilder.getBeanDefinition());
recommenderBeanBuilder.addConstructorArgValue(similarityBeanBuilder.getBeanDefinition());
return recommenderBeanBuilder.getBeanDefinition();
}
}
That is a simple code to understand. It is simply getting values from the XML and wiring components through constructors as is required by the Mahout implementations.
The next and final steps is to make Spring aware of this new configuration we have just created and give it a namespace value for use in our Spring application context.
Making Spring aware is done by creating two special files that Spring recognizes when starting up. the following two files must exist in the META-INF folder for them to be found:
spring.handlers
http\://www.mycompany.com/schema/recommendations=org.springframework.recommendations.RecommendationsNamespaceHandler
spring.schemas
http\://www.mycompany.com/schema/recommendations.xsd=org/springframework/recommendations/recommendations.xsd
Then in our Spring configuration file we simply add the schema namespace specification in the “beans” element as we do with any of the other namespaces. The final XML application context looks like this:
<?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:recommendations="http://www.mycompany.com/schema/recommendations"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.mycompany.com/schema/recommendations http://www.mycompany.com/schema/recommendations.xsd">
<recommendations:user-based-recommender id="recommender">
<recommendations:euclidean-distance-similarity />
<recommendations:file-model path="/tmp/model.csv" />
<recommendations:nearest-neighborhood size="4" />
</recommendations:user-based-recommender>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:recommendations="http://www.mycompany.com/schema/recommendations"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.mycompany.com/schema/recommendations http://www.mycompany.com/schema/recommendations.xsd">
<recommendations:user-based-recommender id="recommender">
<recommendations:euclidean-distance-similarity />
<recommendations:file-model path="/tmp/model.csv" />
<recommendations:nearest-neighborhood size="4" />
</recommendations:user-based-recommender>
</beans>
That’s it. We have created a functional custom xml extension for our Spring configuration.