Spring Custom Tags (Extensible XML) – Part 2

The DefinitionParser

Ok, we’ve got it all set up, now we need to code the thing. Let me state, if you didn’t grasp this, that this is more of an advanced example. If you want a basic example take a look at the Spring documentation linked to at the top of the document.

You still here? Ok, I warned you. Follow me into the depths of code 🙂

Well, what does custom tag code do? In a nutshell it is in charge of registering a org.springframework.beans.factory.FactoryBean wherever your tag exists in your XML so that when it comes time to use the value that your tag represents it can easily get it following the Spring FactoryBean methodology.

Any class that wishes to handle the parsing of custom tags must implement org.springframework.beans.factory.xml.BeanDefinitionParser. It’s just an interface that passed in the W3C Dom Element that is the tag itself and the Context in which the parsing is occurring. The Element object is easy enough, but what is the Context?

Spring passes you an instance of org.springframework.beans.factory.xml.ParserContext that represents where the tag is in the context of things (example: is it a nested tag?) as well as references to the BeanDefinitionRegistry (where Spring keeps info on all the beans you have every defined) and a few other things. This is helpful later as you may want to lookup another bean or perhaps treat the bean created from your tag differently if it is nested.

Lets approach this by following the tag structure. First we will write the code that will parse the fileList followed by the fileFilter. Lastly we will code a util to handle the Spring bean/ref/idref/value tags. I’m making this a util since it might be handy for other tags in the future.

FileListDefinitionParser

The FileListDefinitionParser is in charge of handling the parsing of the fileList tag. To make this easier to follow I’m going to put the comments about the code directly in the code (my college professors would be so proud of me) so that I can explain things better.

package com.example.core.commons.tag;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Returns a list of files that are in a directory.  The list may be limited
 * by nesting a core-commons:fileFilter tag that will do the intersection
 * of all the fileFilters vs what is in the directory.
 * <p/>
 *
 * Also supports the nesting of beans, idref, ref, and value tags that return
 * File objects (the value tag's value will be converted to a new File)
 *
 * <p/>
 * Examples:
 * <br/>
 *
 * Get all the files in the current directory
 * <core-commons:fileList directory="."/>
 *
 * Get all the files in the current directory that end in XML
 * <core-commons:fileList directory=".">
 * 		<core-commons:fileFilter>
 *      	<bean class="org.apache.commons.io.filefilter.RegexFileFilter">
 *          	<constructor-arg value=".*.xml"/>
 *           </bean>
 *      </core-commons:fileFilter>
 * </core-commons:fileList>
 *
 * Get all the files in the current directory that end in XML (specify the fileList separately)
 * <core-commons:fileList>
 *  	<core-commons:fileFilter>
 *      	<bean class="org.apache.commons.io.filefilter.RegexFileFilter">
 *          	<constructor-arg value=".*.xml"/>
 *          </bean>
 *      </core-commons:fileFilter>
 *      <core-commons:fileList directory="."/>
 * </core-commons:fileList>
 *
 * Get all files in the /tmp and /something directory that end in .xml
 * <core-commons:fileList directory="/tmp">
 * 		<core-commons:fileFilter>
 * 			<bean class="org.apache.commons.io.filefilter.RegexFileFilter">
 * 				<constructor-arg value=".*.xml"/>
 * 			</bean>
 * 		</core-commons:fileFilter>
 * 		<core-commons:fileList directory="/something"/>
 * </core-commons:fileList>
 *
 * Get all files in the /tmp and /something directory that end in .xml and can be written to
 * <core-commons:fileList directory="/tmp">
 * 		<core-commons:fileFilter>
 * 			<bean class="org.apache.commons.io.filefilter.CanWriteFileFilter"/>
 * 			<bean class="org.apache.commons.io.filefilter.RegexFileFilter">
 * 				<constructor-arg value=".*.xml"/>
 * 			</bean>
 * 		</core-commons:fileFilter>
 * 		<core-commons:fileList directory="/something"/>
 * </core-commons:fileList>
 *
 * Get all files in the /tmp directory and the pom.xml file in another directory
 * <core-commons:fileList>
 * 		<core-commons:fileList directory="/tmp"/>
 * 		<value>pom.xml</value>
 * </core-commons:fileList>
 *
 * @author seamans
 *
 */
public class FileListDefinitionParser
	extends AbstractSingleBeanDefinitionParser
{

	/**
	 * The bean that is created for this tag element
	 * 
	 * @param element The tag element
	 * @return A FileListFactoryBean
	 */
	@Override
	protected Class<?> getBeanClass(Element element) {
		return FileListFactoryBean.class;
	}

	/**
	 * Called when the fileList tag is to be parsed
	 * 
	 * @param element The tag element
	 * @param ctx The context in which the parsing is occuring
	 * @param builder The bean definitions build to use
	 */
	@Override
	protected void doParse(Element element, ParserContext ctx, BeanDefinitionBuilder builder) {
		// Set the directory property
		builder.addPropertyValue("directory", element.getAttribute("directory"));
		
		// Set the scope
		builder.setScope(element.getAttribute("scope"));

		// We want any parsing to occur as a child of this tag so we need to make
		// a new one that has this as it's owner/parent
		ParserContext nestedCtx = new ParserContext(ctx.getReaderContext(), ctx.getDelegate(), builder.getBeanDefinition());

		// Support for filters
		Element exclusionElem = DomUtils.getChildElementByTagName(element, "fileFilter");
		if (exclusionElem != null) {
			// Just make a new Parser for each one and let the parser do the work
			FileFilterDefinitionParser ff = new FileFilterDefinitionParser();
			builder.addPropertyValue("filters", ff.parse(exclusionElem, nestedCtx));
		}

		// Support for nested fileList
		List<Element> fileLists = DomUtils.getChildElementsByTagName(element, "fileList");
		// Any objects that created will be placed in a ManagedList
		// so Spring does the bulk of the resolution work for us
		ManagedList<Object> nestedFiles = new ManagedList<Object>();
		if (fileLists.size() > 0) {
			// Just make a new Parser for each one and let them do the work
			FileListDefinitionParser fldp = new FileListDefinitionParser();
			for (Element fileListElem : fileLists) {
				nestedFiles.add(fldp.parse(fileListElem, nestedCtx));
			}
		}

		// Support for other tags that return File (value will be converted to file)
		try {
			// Go through any other tags we may find.  This does not mean we support
			// any tag, we support only what parseLimitedList will process
			NodeList nl = element.getChildNodes();
			for (int i=0; i<nl.getLength(); i++) {
				// Parse each child tag we find in the correct scope but we 
				// won't support custom tags at this point as it coudl destablize things
				DefinitionParserUtil.parseLimitedList(nestedFiles, nl.item(i), ctx,
					builder.getBeanDefinition(), element.getAttribute("scope"), false);
			}
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}

		// Set the nestedFiles in the properties so it is set on the FactoryBean
		builder.addPropertyValue("nestedFiles", nestedFiles);

	}

	public static class FileListFactoryBean
		implements FactoryBean<Collection<File>>
	{

		String directory;
		private Collection<FileFilter> filters;
		private Collection<File> nestedFiles;

		@Override
		public Collection<File> getObject() throws Exception {
			// These can be an array list because the directory will have unique's and the nested is already only unique's
			Collection<File> files = new ArrayList<File>();
			Collection<File> results = new ArrayList<File>(0);

			if (directory != null) {
				// get all the files in the directory
				File dir = new File(directory);
				File[] dirFiles = dir.listFiles();
				if (dirFiles != null) {
					files = Arrays.asList(dirFiles);
				}
			}

			// If there are any files that were created from the nested tags,
			// add those to the list of files
			if (nestedFiles != null) {
				files.addAll(nestedFiles);
			}

			// If there are filters we need to go through each filter
			// and see if the files in the list pass the filters.
			// If the files does not pass any one of the filters then it
			// will not be included in the list
			if (filters != null) {
				boolean add;
				for (File f : files) {
					add = true;
					for (FileFilter ff : filters) {
						if (!ff.accept(f)) {
							add = false;
							break;
						}
					}
					if (add) results.add(f);
				}
				return results;
			}

			return files;
		}

		@Override
		public Class<?> getObjectType() {
			return Collection.class;
		}

		@Override
		public boolean isSingleton() {
			return false;
		}

		public void setDirectory(String dir) {
			this.directory = dir;
		}

		public void setFilters(Collection<FileFilter> filters) {
			this.filters = filters;
		}

		/**
		 * What we actually get from the processing of the nested tags
		 * is a collection of files within a collection so we flatten it and
		 * only keep the uniques
		 */
		public void setNestedFiles(Collection<Collection<File>> nestedFiles) {
			this.nestedFiles = new HashSet<File>(); // keep the list unique
			for (Collection<File> nested : nestedFiles) {
				this.nestedFiles.addAll(nested);
			}
		}

	}
}


You can see in the code above that I directly reference FileFilterDefinitionParser which is the part of the code that lets us filter any files that are in the list of files in the directory. Lets go into that code:

FileFilterDefinitionParser

package com.example.core.commons.tag;

import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.io.filefilter.NameFileFilter;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Creates a list of FileFilters based on the configured value, ref, idRef, or bean.
 * <p/>
 *
 * Note: value defaults to a NamedFileFilter
 *
 * Example:
 *
 * Create a NamedFileFilter that will filter for the specific name
 *   <core-commons:fileFilter id="a">
 *       <value>someFile.txt</value>
 *   </core-commons:fileFilter>
 *
 * Create a filter that will filter for any file ending in .xml and that is writable
 *   <core-commons:fileFilter id="a">
 *       <bean class="org.apache.commons.io.filefilter.CanWriteFileFilter"/>
 *       <bean class="org.apache.commons.io.filefilter.RegexFileFilter">
 *           <constructor-arg value=".*.xml"/>
 *       </bean>
 *   </core-commons:fileFilter>
 *
 * @author seamans
 *
 */
public class FileFilterDefinitionParser
	extends AbstractSingleBeanDefinitionParser
{

	/**
	 * The bean that is created for this tag element
	 * 
	 * @param element The tag element
	 * @return A FileFilterFactoryBean
	 */
	@Override
	protected Class<?> getBeanClass(Element element) {
		return FileFilterFactoryBean.class;
	}

	/**
	 * Called when the fileFilter tag is to be parsed
	 * 
	 * @param element The tag element
	 * @param ctx The context in which the parsing is occuring
	 * @param builder The bean definitions build to use
	 */
	@Override
	protected void doParse(Element element, ParserContext ctx, BeanDefinitionBuilder builder) {
		
		// Set the scope
		builder.setScope(element.getAttribute("scope"));
		
		try {
			// All of the filters will eventually end up in this list
			// We use a 'ManagedList' and not a regular list because anything
			// placed in a ManagedList object will support all of Springs
			// functionalities and scopes for us, we dont' have to code anything
			// in terms of reference lookups, EL, etc
			ManagedList<Object> filters = new ManagedList<Object>();

			// For each child node of the fileFilter tag, parse it and place it
			// in the filtes list
			NodeList nl = element.getChildNodes();
			for (int i=0; i<nl.getLength(); i++) {
				DefinitionParserUtil.parseLimitedList(filters, nl.item(i), ctx, builder.getBeanDefinition(), element.getAttribute("scope"));
			}

			// Add the filtes to the list of properties (this is applied
			// to the factory beans setFilters below)
			builder.addPropertyValue("filters", filters);
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static class FileFilterFactoryBean
		implements FactoryBean<Collection<FileFilter>>
	{

		private final List<FileFilter> filters = new ArrayList<FileFilter>();

		@Override
		public Collection<FileFilter> getObject() throws Exception {
			return filters;
		}

		@Override
		public Class<?> getObjectType() {
			return Collection.class;
		}

		@Override
		public boolean isSingleton() {
			return false;
		}

		/**
		 * Go through the list of filters and convert the String ones
		 * (the ones that were set with <value> and make them NameFileFilters
		 */
		public void setFilters(Collection<Object> filterList) {
			for (Object o : filterList) {
				if (o instanceof String) {
					filters.add(new NameFileFilter(o.toString()));
				}
				else if (o instanceof FileFilter) {
					filters.add((FileFilter)o);
				}
			}
		}

	}
}

As you saw in the XSD definition (previous post) for the fileFilter tag it supports child tags that are standard Spring tags (bean/ref/idref/value). We need to code support for this since unfortunately Spring does NOT provide a helper class for parsing its own tags. I was a bit disappointed in this and I have to admit that most of my time went into the coding of this util class and how I could support the Spring bean tag properly.

This piece of code may be of interest to others as it shows how to create a Spring bean programmatically and still get all its functionality

DefinitionParserUtil

package com.example.core.commons.tag;
package com.broadridge.adc.core.commons.tag;

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.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class DefinitionParserUtil {

	/**
	 * Parses the children of the passed in ParentNode for the following tags:
	 * <br/>
	 * value
	 * ref
	 * idref
	 * bean
	 * property
	 * *custom*
	 * <p/>
	 *
	 * The value tag works with Spring EL even in a Spring Batch scope="step"
	 *
	 * @param objects The list of resultings objects from the parsing (passed in for recursion purposes)
	 * @param parentNode The node who's children should be parsed
	 * @param ctx The ParserContext to use
	 * @param parentBean The BeanDefinition of the bean who is the parent of the parsed bean
	 * 		(i.e. the Bean that is the parentNode)
	 * @param scope The scope to execute in.  Checked if 'step' to provide Spring EL
	 * 		support in a Spring Batch env
	 * @throws Exception
	 */
	public static void parseLimitedList(ManagedList<Object> objects, Node node,
		ParserContext ctx, BeanDefinition parentBean, String scope)
		throws Exception
	{
		parseLimitedList(objects, node, ctx, parentBean, scope, true);
	}

	/**
	 * Parses the children of the passed in ParentNode for the following tags:
	 * <br/>
	 * value
	 * ref
	 * idref
	 * bean
	 * property
	 * *custom*
	 * <p/>
	 *
	 * The value tag works with Spring EL even in a Spring Batch scope="step"
	 *
	 * @param objects The list of resultings objects from the parsing (passed in for recursion purposes)
	 * @param parentNode The node who's children should be parsed
	 * @param ctx The ParserContext to use
	 * @param parentBean The BeanDefinition of the bean who is the parent of the parsed bean
	 * 		(i.e. the Bean that is the parentNode)
	 * @param scope The scope to execute in.  Checked if 'step' to provide Spring EL
	 * 		support in a Spring Batch env
	 * @param supportCustomTags Should we support custom tags within our tags?
	 * @throws Exception
	 */
	public static void parseLimitedList(ManagedList<Object> objects, Node node,
		ParserContext ctx, BeanDefinition parentBean, String scope, boolean supportCustomTags)
		throws Exception
	{
		// Only worry about element nodes
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			Element elem = (Element)node;
			String tagName = node.getLocalName();

			if (tagName.equals("value")) {
				String val = node.getTextContent();
				// to get around an issue with Spring Batch not parsing Spring EL
				// we will do it for them
				if (scope.equals("step")
					&& (val.startsWith("#{") && val.endsWith("}"))
					&& (!val.startsWith("#{jobParameters"))
					)
				{
					// Set up a new EL parser
					ExpressionParser parser = new SpelExpressionParser();
					// Parse the value
					Expression exp = parser.parseExpression(val.substring(2, val.length()-1));
					// Place the results in the list of created objects
					objects.add(exp.getValue());
				}
				else {
					// Otherwise, just treat it as a normal value tag
					objects.add(val);
				}
			}
			// Either of these is a just a lookup of an existing bean 
			else if (tagName.equals("ref") || tagName.equals("idref")) {
				objects.add(ctx.getRegistry().getBeanDefinition(node.getTextContent()));
			}
			// We need to create the bean
			else if (tagName.equals("bean")) {
				// There is no quick little util I could find to create a bean
				// on the fly programmatically in Spring and still support all
				// Spring functionality so basically I mimic what Spring actually
				// does but on a smaller scale.  Everything Spring allows is
				// still supported

				// Create a factory to make the bean
				DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
				// Set up a parser for the bean
				BeanDefinitionParserDelegate pd = new BeanDefinitionParserDelegate(ctx.getReaderContext());
				// Parse the bean get its information, now in a DefintionHolder
				BeanDefinitionHolder bh = pd.parseBeanDefinitionElement(elem, parentBean);
				// Register the bean will all the other beans Spring is aware of
				BeanDefinitionReaderUtils.registerBeanDefinition(bh, beanFactory);
				// Get the bean from the factory.  This will allows Spring
				// to do all its work (EL processing, scope, etc) and give us 
				// the actual bean itself
				Object bean = beanFactory.getBean(bh.getBeanName());
				objects.add(bean);
			}
			/*
			 * This is handled a bit differently in that it actually sets the property
			 * on the parent bean for us based on the property
			 */
			else if (tagName.equals("property")) {
				BeanDefinitionParserDelegate pd = new BeanDefinitionParserDelegate(ctx.getReaderContext());
				// This method actually set eh property on the parentBean for us so
				// we don't have to add anything to the objects object
				pd.parsePropertyElement(elem, parentBean);
			}
			else if (supportCustomTags) {
				// handle custom tag
				BeanDefinitionParserDelegate pd = new BeanDefinitionParserDelegate(ctx.getReaderContext());
				BeanDefinition bd = pd.parseCustomElement(elem, parentBean);
				objects.add(bd);
			}
		}
	}
}

And that’s it! Ok, so you may be saying, “Whoa, that was a lot of code”, well, I did say it was a more advanced example.

If you have any questions please post a comment. I know it can be a bit confusing so I tried to document in-line in the code as much as possible to make it easy to follow.

Really though, this is a very powerful, under-utilized feature of Spring. Knowing how to do this can really make your XML smaller while harnessing the power of converting things on the fly and keeping your beans as generic as possible.

About sseaman

Connect with me on Google+
This entry was posted in Java, Programming and tagged , . Bookmark the permalink.

4 Responses to Spring Custom Tags (Extensible XML) – Part 2

Leave a Reply

Your email address will not be published.