/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.arquillian.impl;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.jboss.arquillian.spi.Configuration;
import org.jboss.arquillian.spi.ConfigurationException;
import org.jboss.arquillian.spi.ContainerConfiguration;
import org.jboss.arquillian.spi.ServiceLoader;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* An implementation of {@link ConfigurationBuilder} that loads the configuration
* from the arquillian.xml file located in the root of the classpath. If not found,
* it just returns an empty {@link org.jboss.arquillian.spi.Configuration} object.
*
* @author <a href="mailto:german.escobarc@gmail.com">German Escobar</a>
* @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
* @author Dan Allen
* @version $Revision: $
*/
public class XmlConfigurationBuilder implements ConfigurationBuilder
{
private static final Logger log = Logger.getLogger(XmlConfigurationBuilder.class.getName());
/**
* The default XML resource path.
*/
private static final String DEFAULT_RESOURCE_PATH = "arquillian.xml";
/**
* The actual resourcePath
*/
private String resourcePath;
private ServiceLoader serviceLoader;
/**
* Constructor. Initializes with the default resource path and service loader.
*/
public XmlConfigurationBuilder()
{
this(DEFAULT_RESOURCE_PATH);
}
/**
* Constructor. Initializes with the provided resource path and the default
* service loader.
* @param resourcePath the path to the XML configuration file.
*/
public XmlConfigurationBuilder(String resourcePath)
{
this(resourcePath, new DynamicServiceLoader());
}
/**
* Constructor. Initializes with the provided resource path and service loader.
* @param resourcePath the path to the XML configuration file.
* @param serviceLoader the ServiceLoader implementation to use.
*/
public XmlConfigurationBuilder(String resourcePath, ServiceLoader serviceLoader)
{
this.resourcePath = resourcePath;
this.serviceLoader = serviceLoader;
}
/* (non-Javadoc)
* @see org.jboss.arquillian.impl.ConfigurationBuilder#build()
*/
public Configuration build() throws ConfigurationException
{
// the configuration object we are going to return
Configuration configuration = new Configuration();
Collection<ContainerConfiguration> containersConfigurations = serviceLoader.all(ContainerConfiguration.class);
log.fine("Container Configurations: " + containersConfigurations.size());
for(ContainerConfiguration containerConfiguration : containersConfigurations)
{
configuration.addContainerConfig(containerConfiguration);
}
try
{
Document arquillianConfiguration = loadArquillianConfiguration(resourcePath);
if(arquillianConfiguration != null)
{
populateConfiguration(arquillianConfiguration, containersConfigurations);
populateConfiguration(arquillianConfiguration, configuration);
}
}
catch (Exception e)
{
throw new ConfigurationException("Could not create configuration", e);
}
return configuration;
}
private Document loadArquillianConfiguration(String resourcePath) throws Exception
{
InputStream inputStream = null;
try
{
// load the xml configuration file
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
inputStream = classLoader.getResourceAsStream(resourcePath);
if (inputStream != null)
{
log.info("building configuration from XML file: " + resourcePath);
return getDocument(inputStream);
}
else
{
log.fine("No " + resourcePath + " file found");
}
}
finally
{
if(inputStream != null)
{
try { inputStream.close(); } catch (Exception e) { /* NO-OP */ }
}
}
return null;
}
private void populateConfiguration(Document xmlDocument, Collection<ContainerConfiguration> containersConfigurations) throws Exception
{
// load all the container nodes
NodeList nodeList = xmlDocument.getDocumentElement().getElementsByTagNameNS("*", "container");
for (int i=0; i < nodeList.getLength(); i++)
{
Node containerNode = nodeList.item(i);
// retrieve the package
String pkg = containerNode.getNamespaceURI().replaceFirst("urn:arq:", "");
// try to find a ContainerConfiguration that matches the package
ContainerConfiguration containerConfig = matchContainerConfiguration(containersConfigurations, pkg);
if (containerConfig != null)
{
// map the nodes
mapNodesToProperties(containerConfig, containerNode);
}
}
}
private void populateConfiguration(Document xmlDocument, Configuration configuration) throws Exception
{
// try to map all child nodes
NodeList nodeList = xmlDocument.getDocumentElement().getElementsByTagNameNS("*", "engine");
for (int i=0; i < nodeList.getLength(); i++)
{
Node node = nodeList.item(i);
mapNodesToProperties(configuration, node);
}
}
/**
* Fills the properties of the Configuration implementation object with the
* information from the XML fragment.
* @param configurationObject the object to be filled from the XML fragment
* @param xmlNode the XML node that represents the configuration.
* @throws Exception if there is a problem filling the object.
*/
private void mapNodesToProperties(Object configurationObject, Node xmlNode) throws Exception
{
// validation
Validate.notNull(configurationObject, "No ConfigurationObject specified");
Validate.notNull(xmlNode, "No XML Node specified");
log.fine("filling container configuration for class: " + configurationObject.getClass().getName());
// here we will store the properties taken from the child elements of the node
Map<String,String> properties = new HashMap<String,String>();
NodeList childNodes = xmlNode.getChildNodes();
for (int i=0; i < childNodes.getLength(); i++)
{
Node child = childNodes.item(i);
// only process element nodes
if (child.getNodeType() == Node.ELEMENT_NODE)
{
properties.putAll(getPropertiesFromNode(child));
}
}
Map<String, Method> setters = new HashMap<String, Method>();
for (Method candidate : configurationObject.getClass().getMethods())
{
String methodName = candidate.getName();
if (methodName.matches("^set[A-Z].*") &&
candidate.getReturnType().equals(Void.TYPE) &&
candidate.getParameterTypes().length == 1)
{
candidate.setAccessible(true);
setters.put(methodName.substring(3, 4).toLowerCase() + methodName.substring(4), candidate);
}
}
// set the properties found in the container XML fragment to the Configuration Object
for (Map.Entry<String, String> property : properties.entrySet())
{
if (setters.containsKey(property.getKey()))
{
Method method = setters.get(property.getKey());
Object value = convert(method.getParameterTypes()[0], property.getValue());
method.invoke(configurationObject, value);
}
}
}
/**
* Creates all the properties from a single Node element. The element must be a child of the
* 'section' root element.
* @param element the XML Node from which we are going to create the properties.
* @return a Map of properties names and values mapped from the XML Node element.
*/
private Map<String,String> getPropertiesFromNode(Node element) {
Map<String,String> properties = new HashMap<String,String>();
// retrieve the attributes of the element
NamedNodeMap attributes = element.getAttributes();
// choose the strategy
if (attributes.getLength() > 0)
{
new TagNameAttributeMapper().map(element, properties);
}
else
{
new TagNameMapper().map(element, properties);
}
return properties;
}
/**
* Matches a ContainerConfiguration implementation object with the pkg parameter.
* @param pkg the package prefix used to match the ContainerConfiguration.
* @return the ContainerConfiguration implementation object that matches the package,
* null otherwise.
*/
private ContainerConfiguration matchContainerConfiguration(Collection<ContainerConfiguration> containerConfigurations, String pkg)
{
log.fine("trying to match a container configuration for package: " + pkg);
// load all the containers configurations
ContainerConfiguration containerConfig = null;
// select the container configuration that matches the package
for (ContainerConfiguration cc : containerConfigurations)
{
if (cc.getClass().getName().startsWith(pkg))
{
containerConfig = cc;
}
}
// warn: we didn't find the class
if (containerConfig == null)
{
log.warning("No container configuration found for URI: java:urn:" + pkg);
}
return containerConfig;
}
/**
* Retrieves the DOM document object from the inputStream.
* @param inputStream the inputStream of the XML file.
* @return a loaded Document object for DOM manipulation.
* @throws Exception if the Document object couldn't be created.
*/
private Document getDocument(InputStream inputStream) throws Exception
{
Validate.notNull(inputStream, "No input stream specified");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(inputStream);
document.getDocumentElement().normalize();
return document;
}
/**
* Converts a String value to the specified class.
* @param clazz
* @param value
* @return
*/
private Object convert(Class<?> clazz, String value)
{
/* TODO create a new Converter class and move this method there for reuse */
if (Integer.class.equals(clazz) || int.class.equals(clazz))
{
return Integer.valueOf(value);
}
else if (Double.class.equals(clazz) || double.class.equals(clazz))
{
return Double.valueOf(value);
}
else if (Long.class.equals(clazz) || long.class.equals(clazz))
{
return Long.valueOf(value);
}
else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz))
{
return Boolean.valueOf(value);
}
return value;
}
/**
*
* @author <a href="mailto:german.escobarc@gmail.com">German Escobar</a>
*/
private interface PropertiesMapper
{
void map(Node element, Map<String,String> properties);
}
/**
*
* @author <a href="mailto:german.escobarc@gmail.com">German Escobar</a>
*/
private class TagNameAttributeMapper implements PropertiesMapper
{
public void map(Node element, Map<String, String> properties)
{
// retrieve the attributes of the element
NamedNodeMap attributes = element.getAttributes();
for (int k=0; k < attributes.getLength(); k++)
{
Node attribute = attributes.item(k);
// build the property name
String attributeName = attribute.getNodeName();
String fullPropertyName = element.getLocalName() + Character.toUpperCase(attributeName.charAt(0))
+ attributeName.substring(1);
// add the property name and its value
properties.put(fullPropertyName, attribute.getNodeValue());
}
}
}
/**
*
* @author <a href="mailto:german.escobarc@gmail.com">German Escobar</a>
*/
private class TagNameMapper implements PropertiesMapper
{
public void map(Node element, Map<String, String> properties)
{
String value = "";
if (!element.hasChildNodes())
{
throw new ConfigurationException("Node " + element.getNodeName() + " has no value");
}
value = element.getChildNodes().item(0).getNodeValue();
properties.put(element.getLocalName(), value);
}
}
}