Archive

Posts Tagged ‘w3c dom’

DomELResolver

September 6, 2012 2 comments

Starting with JSP 2.1 there is a very nifty feature: ELResolvers. Even there’s a little less popular, it can make your code nice and clean.
It is well known, that scriptlets are a no-go for JSP development. As an alternative, you can use custom tags and expression language. The nice thing about EL is that it is easily extensible. You can define custom functions, using tag library descriptors, and through resolver, you can plugin bean property resolution.

Now I’ll walk you through a full blown example of working with ELResolvers.

I don’t know if you ever had to work with XML documents inside your JSP, but the options aren’t so good and in the end your code will look ugly, either you use scriptlets (totally not recommended), complex custom tags or custom functions. I acknowledged that XML/DOM processing should happen mostly on the controller side (servlet, Spring controller or other), but there are times when you need it on JSP, like some kind of transcoder functionality.
JSTL offers a set of tags for XML processings. Still feels a little bit hard to work with. Just imagine you would have to use the value of an XPath into an attribute.

<x:set var="title" select="$doc//title"/>
${title}

It would be nice if you could do it directly: ${doc["//title"]}. And now we get to the EL Resolver. It practically instructs EL engine how to evaluate a property for a bean.

Implement an EL Resolver

As the code will tell better the story, here it is.

import java.beans.FeatureDescriptor;
import java.util.ArrayList;
import java.util.Iterator;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;

import org.jaxen.JaxenException;
import org.jaxen.XPath;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class DomELResolver extends ELResolver {

	@Override
	public Class<?> getCommonPropertyType(ELContext context, Object base) {
		if (base instanceof NodeList)
			return Integer.class;
		return String.class;
	}

	@Override
	public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
		return null;
	}

	@Override
	public Class<?> getType(ELContext context, Object base, Object property) {
		return getValue(context, base, property).getClass();
	}

	@Override
	public Object getValue(ELContext context, Object base, Object property) {
		if (property == null) {
			return null;
		}
		// get the property as a string
		String propertyAsString = property.toString();
		
		if (base instanceof ListOfNodes) {
			// if the base object is a list of nodes, the property must be an index
			int index = getPropertyAsIndex(property);
			if (index >= 0) {
				if (context != null) {
					context.setPropertyResolved(true);
				}
				return ((ListOfNodes)base).get(index);
			} else {
				base = ((ListOfNodes)base).get(0);
			}
		}
		
		if (base instanceof NodeList && !(base instanceof Node)) {
			// if the base object is a DOM NodeList, the property must be an index
			int index = getPropertyAsIndex(property);
			if (index >= 0) {
				if (context != null) {
					context.setPropertyResolved(true);
				}
				return ((NodeList)base).item(index);
			} else {
				base = ((NodeList)base).item(0);
			}
		}
		
		if (base instanceof Node) {
			Node baseNode = (Node)base;
			
			// if the property contains special characters, it is most probably an XPath expression
			if (!containsOnlyAlphaNumeric(propertyAsString)) {
				try {
					// creates the XPath expression and evaluates it
					XPath xpath = new org.jaxen.dom.DOMXPath(propertyAsString);
				    ListOfNodes l = new ListOfNodes();
				    for (Object result : xpath.selectNodes(base)) {
				    	if (result instanceof Node) {
				    		l.add((Node)result);
				    	}
				    }
				    // if we found a node then we consider the expression resolved
				    if (!l.isEmpty()) {
						if (context != null) {
							context.setPropertyResolved(true);
						}
						return l;
				    }
				    return null;
				} catch (JaxenException exc) {
					throw new ELException("Cannot compile XPath.", exc);
				}
				
			}
			
			// if the base bean is a node and has children, then get the children of which tag name is
			// the same as the given property
			if (hasChildElements(baseNode)) { 				
				ListOfNodes c = getChildrenByTagName(baseNode, propertyAsString);
				if (c != null) {
					if (context != null) {
						context.setPropertyResolved(true);
					}
					return c;
				}
			}
		}
		
		// evaluates the expression to an attribute of the base element
		if (base instanceof Element) {
			Element el = (Element) base;
			if (el.hasAttribute(propertyAsString)) {
				if (context != null) {
					context.setPropertyResolved(true);
				}
				return el.getAttribute(propertyAsString);
			}
		}
		
		return null;
	}

	@Override
	public boolean isReadOnly(ELContext context, Object base, Object property) {
		// we cannot modify the DOM
		return true;
	}

	@Override
	public void setValue(ELContext context, Object base, Object property, Object value) {
		// we don't modify the DOM
		return;
	}
	
	/**
	 * @param property the EL property 
	 * @return the property as an integer, -1 if the property is not a number 
	 */
	private int getPropertyAsIndex(Object property) {
		int index = -1;
		if (property instanceof Number) {
			index = ((Number)property).intValue();
		} else if (property instanceof String) {
			try {
				index = Integer.parseInt(property.toString());
			} catch (NumberFormatException exc) {
			}
		}
		return index;
	}
	
	/**
	 * @param s the string to be tested
	 * @return true if the given string contains only alphanumeric characters, false otherwise
	 */
	private boolean containsOnlyAlphaNumeric(String s) {
		for (int i = 0, n = s.length(); i < n; i++) {
			if (!Character.isLetterOrDigit(s.codePointAt(i))) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * @param el the element to search for children with a given tag name
	 * @param tagChildName the tag name 
	 * @return the first element with the given tag name
	 */
	private ListOfNodes getChildrenByTagName(Node el, String tagChildName) {
		ListOfNodes l = new ListOfNodes();
		NodeList children = el.getChildNodes();
		for (int i = 0, n = children.getLength(); i < n; i++) {
			Node c = children.item(i);
			if (c instanceof Element) {
				Element ce = (Element)c;
				if (tagChildName.equals(ce.getTagName())) {
					l.add(ce);
				}
			}
		}
		return l.isEmpty() ? null : l;
	}

	/**
	 * @param el the DOM element
	 * @return true if the given DOM element has at least one children element, false otherwise
	 */
	private boolean hasChildElements(Node el) {
		NodeList children = el.getChildNodes();
		for (int i = 0, n = children.getLength(); i < n; i++) {
			if (children.item(i) instanceof Element) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Encapsulates a list of nodes to give EL the opportunity to work with as with a normal collection.
	 * Also it evaluates to a string, as the first node text content.
	 */
	@SuppressWarnings("serial")
	private static class ListOfNodes extends ArrayList<Node> {
		@Override
		public String toString() {
			return get(0).getTextContent();
		}
	}
	
}

This entire code practically instructs EL engine how to evaluate on a DOM element a property, either to a child tag name or to an XPath.

Register an EL Resolver

Before using it, you need to register your resolver. The below code does this and you can put it into a ServletContextListener, a ServletContextAware Spring bean or other kind of method that registers it at the application statup.

Using a ServletContextAware bean
import javax.el.ELResolver;
import javax.servlet.ServletContext;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.jsp.JspFactory;

import org.springframework.web.context.ServletContextAware;

/** Registers the specified ELResolver's in the JSP EL context. */
public class ELResolversRegistrar implements ServletContextAware {

	/**
	 * The EL resolvers to be registered.
	 */
	private ELResolver[] resolvers;
	
	/** 
	 * Creates a registrar with the given resolvers.
	 * @param resolvers the EL resolvers to be registered 
	 */
	public ELResolversRegistrar(ELResolver... resolvers) {
		this.resolvers = resolvers;
	}

	/** 
	 * Creates a registrar with the given resolver.
	 * @param resolver the EL resolver to be registered 
	 */
	public ELResolversRegistrar(ELResolver resolver) {
		this(new ELResolver[]{resolver});
	}

	public void setServletContext(ServletContext servletContext) {
        JspApplicationContext jspContext = JspFactory.getDefaultFactory().getJspApplicationContext(servletContext);
        for (ELResolver resolver : resolvers) {
        	jspContext.addELResolver(resolver);
        }
	}
}

and the code in the Spring context XML

	<bean class="ELResolversRegistrar">
		<constructor-arg>
			<bean class="DomELResolver"/>
		</constructor-arg>
	</bean>
Using a ServletContextListener
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.jsp.JspFactory;

public class DomELResolverRegistrarListener implements ServletContextListener {

	public void contextInitialized(ServletContextEvent sce) {
        JspApplicationContext jspContext = JspFactory.getDefaultFactory()
        		.getJspApplicationContext(sce.getServletContext());
       	jspContext.addELResolver(new DomELResolver());
	}

	public void contextDestroyed(ServletContextEvent sce) {
		// do nothing
	}

}

and in web.xml

	<listener>
		<listener-class>DomELResolverRegistrarListener</listener-class>
	</listener>

It is obvious why I prefer the first version: easy extensible and you can have only one registrar class for multiple resolvers.

And in the end some examples how you can use the resolver in a JSP

Use an EL Resolver

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %>
<x:parse varDom="doc">
<html>
<head>
	<title>Hello</title>
</head>
<body>
	<ul>
		<li>Item 1</li>
		<li>Item 2</li>
	</ul>
</body>
</html>
</x:parse>
 
Title: ${doc.html.head.title} = ${doc["//title"]}

<br/>
Items: 
<c:forEach var="i" items="${doc.html.body.ul.li}">
 	${i.textContent}
</c:forEach>
 
<br/>
First item: ${doc.html.body.ul.li}

You can modify the resolver to even evaluate CSS selectors. But that’s your homework. Or a future story.

Categories: Software Tags: ,