Author: oheger
Date: Sun Nov 11 18:09:24 2012
New Revision: 1408062
URL: http://svn.apache.org/viewvc?rev=1408062&view=rev
Log:
[CONFIGURATION-514] Added a method to BeanHelper to determine the constructor
to be invoked.
Modified:
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/ConstructorArg.java
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
Modified:
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
URL:
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java?rev=1408062&r1=1408061&r2=1408062&view=diff
==============================================================================
---
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
(original)
+++
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/BeanHelper.java
Sun Nov 11 18:09:24 2012
@@ -17,10 +17,12 @@
package org.apache.commons.configuration.beanutils;
import java.beans.PropertyDescriptor;
+import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -63,6 +65,10 @@ public final class BeanHelper
private static final Map<String, BeanFactory> BEAN_FACTORIES = Collections
.synchronizedMap(new HashMap<String, BeanFactory>());
+ /** A format string for generating error messages for constructor
matching. */
+ private static final String FMT_CTOR_ERROR =
+ "%s! Bean class = %s, constructor arguments = %s";
+
/**
* Stores the default bean factory, which will be used if no other factory
* is provided.
@@ -373,6 +379,29 @@ public final class BeanHelper
}
/**
+ * Evaluates constructor arguments in the specified {@code BeanDeclaration}
+ * and tries to find a unique matching constructor. If this is not
possible,
+ * an exception is thrown. Note: This method is intended to be used by
+ * concrete {@link BeanFactory} implementations and not by client code.
+ *
+ * @param beanClass the class of the bean to be created
+ * @param data the current {@code BeanDeclaration}
+ * @return the single matching constructor
+ * @throws ConfigurationRuntimeException if no single matching constructor
+ * can be found
+ * @throws NullPointerException if the bean class or bean declaration are
+ * <b>null</b>
+ */
+ public static <T> Constructor<T> findMatchingConstructor(
+ Class<T> beanClass, BeanDeclaration data)
+ {
+ List<Constructor<T>> matchingConstructors =
+ findMatchingConstructors(beanClass, data);
+ checkSingleMatchingConstructor(beanClass, data, matchingConstructors);
+ return matchingConstructors.get(0);
+ }
+
+ /**
* Returns a {@code java.lang.Class} object for the specified name.
* Because class loading can be tricky in some environments the code for
* retrieving a class by its name was extracted into this helper method. So
@@ -464,4 +493,120 @@ public final class BeanHelper
return getDefaultBeanFactory();
}
}
+
+ /**
+ * Returns a list with all constructors which are compatible with the
+ * constructor arguments specified by the given {@code BeanDeclaration}.
+ *
+ * @param beanClass the bean class to be instantiated
+ * @param data the current {@code BeanDeclaration}
+ * @return a list with all matching constructors
+ */
+ private static <T> List<Constructor<T>> findMatchingConstructors(
+ Class<T> beanClass, BeanDeclaration data)
+ {
+ List<Constructor<T>> result = new LinkedList<Constructor<T>>();
+ Collection<ConstructorArg> args = getConstructorArgs(data);
+ for (Constructor<?> ctor : beanClass.getConstructors())
+ {
+ if (matchesConstructor(ctor, args))
+ {
+ // cast should be okay according to the JavaDocs of
+ // getConstructors()
+ @SuppressWarnings("unchecked")
+ Constructor<T> match = (Constructor<T>) ctor;
+ result.add(match);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Checks whether the given constructor is compatible with the given list
of
+ * arguments.
+ *
+ * @param ctor the constructor to be checked
+ * @param args the collection of constructor arguments
+ * @return a flag whether this constructor is compatible with the given
+ * arguments
+ */
+ private static boolean matchesConstructor(Constructor<?> ctor,
+ Collection<ConstructorArg> args)
+ {
+ Class<?>[] types = ctor.getParameterTypes();
+ if (types.length != args.size())
+ {
+ return false;
+ }
+
+ int idx = 0;
+ for (ConstructorArg arg : args)
+ {
+ if (!arg.matches(types[idx++]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper method for extracting constructor arguments from a bean
+ * declaration. Deals with <b>null</b> values.
+ *
+ * @param data the bean declaration
+ * @return the collection with constructor arguments (never <b>null</b>)
+ */
+ private static Collection<ConstructorArg> getConstructorArgs(
+ BeanDeclaration data)
+ {
+ Collection<ConstructorArg> args = data.getConstructorArgs();
+ if (args == null)
+ {
+ args = Collections.emptySet();
+ }
+ return args;
+ }
+
+ /**
+ * Helper method for testing whether exactly one matching constructor was
+ * found. Throws a meaningful exception if there is not a single matching
+ * constructor.
+ *
+ * @param beanClass the bean class
+ * @param data the bean declaration
+ * @param matchingConstructors the list with matching constructors
+ * @throws ConfigurationRuntimeException if there is not exactly one match
+ */
+ private static <T> void checkSingleMatchingConstructor(Class<T> beanClass,
+ BeanDeclaration data, List<Constructor<T>> matchingConstructors)
+ {
+ if (matchingConstructors.isEmpty())
+ {
+ throw constructorMatchingException(beanClass, data,
+ "No matching constructor found");
+ }
+ if (matchingConstructors.size() > 1)
+ {
+ throw constructorMatchingException(beanClass, data,
+ "Multiple matching constructors found");
+ }
+ }
+
+ /**
+ * Creates an exception if no single matching constructor was found with a
+ * meaningful error message.
+ *
+ * @param beanClass the affected bean class
+ * @param data the bean declaration
+ * @param msg an error message
+ * @return the exception with the error message
+ */
+ private static ConfigurationRuntimeException constructorMatchingException(
+ Class<?> beanClass, BeanDeclaration data, String msg)
+ {
+ return new ConfigurationRuntimeException(String.format(FMT_CTOR_ERROR,
+ msg, beanClass.getName(),
getConstructorArgs(data).toString()));
+ }
}
Modified:
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/ConstructorArg.java
URL:
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/ConstructorArg.java?rev=1408062&r1=1408061&r2=1408062&view=diff
==============================================================================
---
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/ConstructorArg.java
(original)
+++
commons/proper/configuration/trunk/src/main/java/org/apache/commons/configuration/beanutils/ConstructorArg.java
Sun Nov 11 18:09:24 2012
@@ -205,4 +205,26 @@ public final class ConstructorArg
return getTypeName() == null || getTypeName().equals(argCls.getName());
}
+
+ /**
+ * Returns a string representation of this object. This string contains the
+ * value of this constructor argument and the explicit type if provided.
+ *
+ * @return a string for this object
+ */
+ @Override
+ public String toString()
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append(getClass().getSimpleName());
+ buf.append(" [ value = ");
+ buf.append(isNestedBeanDeclaration() ? getBeanDeclaration()
+ : getValue());
+ if (getTypeName() != null)
+ {
+ buf.append(" (").append(getTypeName()).append(')');
+ }
+ buf.append(" ]");
+ return buf.toString();
+ }
}
Modified:
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
URL:
http://svn.apache.org/viewvc/commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java?rev=1408062&r1=1408061&r2=1408062&view=diff
==============================================================================
---
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
(original)
+++
commons/proper/configuration/trunk/src/test/java/org/apache/commons/configuration/beanutils/TestBeanHelper.java
Sun Nov 11 18:09:24 2012
@@ -21,7 +21,10 @@ import static org.junit.Assert.assertNot
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -42,6 +45,12 @@ import org.junit.Test;
*/
public class TestBeanHelper
{
+ /** Constant for the test value of the string property. */
+ private static final String TEST_STRING = "testString";
+
+ /** Constant for the test value of the numeric property. */
+ private static final int TEST_INT = 42;
+
/** Constant for the name of the test bean factory. */
private static final String TEST_FACTORY = "testFactory";
@@ -327,6 +336,98 @@ public class TestBeanHelper
}
/**
+ * Tests whether the standard constructor can be found.
+ */
+ @Test
+ public void testFindMatchingConstructorNoArgs()
+ {
+ TestBeanDeclaration decl = new TestBeanDeclaration();
+ Constructor<TestBean> ctor =
+ BeanHelper.findMatchingConstructor(TestBean.class, decl);
+ assertEquals("Not the standard constructor", 0,
+ ctor.getParameterTypes().length);
+ }
+
+ /**
+ * Tests whether a matching constructor is found if the number of arguments
+ * is unique.
+ */
+ @Test
+ public void testFindMatchingConstructorArgCount()
+ {
+ TestBeanDeclaration decl = new TestBeanDeclaration();
+ Collection<ConstructorArg> args = new ArrayList<ConstructorArg>();
+ args.add(ConstructorArg.forValue(TEST_STRING));
+ args.add(ConstructorArg.forValue(String.valueOf(TEST_INT)));
+ decl.setConstructorArgs(args);
+ Constructor<TestCtorBean> ctor =
+ BeanHelper.findMatchingConstructor(TestCtorBean.class, decl);
+ Class<?>[] paramTypes = ctor.getParameterTypes();
+ assertEquals("Wrong number of parameters", 2, paramTypes.length);
+ assertEquals("Wrong parameter type 1", String.class, paramTypes[0]);
+ assertEquals("Wrong parameter type 2", Integer.TYPE, paramTypes[1]);
+ }
+
+ /**
+ * Tests whether ambiguous constructor arguments are detected.
+ */
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testFindMatchingConstructorAmbiguous()
+ {
+ TestBeanDeclaration decl = new TestBeanDeclaration();
+ Collection<ConstructorArg> args = new ArrayList<ConstructorArg>();
+ args.add(ConstructorArg.forValue(TEST_STRING));
+ decl.setConstructorArgs(args);
+ BeanHelper.findMatchingConstructor(TestCtorBean.class, decl);
+ }
+
+ /**
+ * Tests whether explicit type declarations are used to resolve ambiguous
+ * parameter types.
+ */
+ @Test
+ public void testFindMatchingConstructorExplicitType()
+ {
+ TestBeanDeclaration decl = new TestBeanDeclaration();
+ Collection<ConstructorArg> args = new ArrayList<ConstructorArg>();
+ args.add(ConstructorArg.forBeanDeclaration(setUpBeanDeclaration(),
+ TestBean.class.getName()));
+ decl.setConstructorArgs(args);
+ Constructor<TestCtorBean> ctor =
+ BeanHelper.findMatchingConstructor(TestCtorBean.class, decl);
+ Class<?>[] paramTypes = ctor.getParameterTypes();
+ assertEquals("Wrong number of parameters", 1, paramTypes.length);
+ assertEquals("Wrong parameter type", TestBean.class, paramTypes[0]);
+ }
+
+ /**
+ * Tests the case that no matching constructor is found.
+ */
+ @Test
+ public void testFindMatchingConstructorNoMatch()
+ {
+ TestBeanDeclaration decl = new TestBeanDeclaration();
+ Collection<ConstructorArg> args = new ArrayList<ConstructorArg>();
+ args.add(ConstructorArg.forValue(TEST_STRING, getClass().getName()));
+ decl.setConstructorArgs(args);
+ try
+ {
+ BeanHelper.findMatchingConstructor(TestCtorBean.class, decl);
+ fail("No exception thrown!");
+ }
+ catch (ConfigurationRuntimeException crex)
+ {
+ String msg = crex.getMessage();
+ assertTrue("Bean class not found:" + msg,
+ msg.indexOf(TestCtorBean.class.getName()) > 0);
+ assertTrue("Parameter value not found: " + msg,
+ msg.indexOf(TEST_STRING) > 0);
+ assertTrue("Parameter type not found: " + msg,
+ msg.indexOf("(" + getClass().getName() + ')') > 0);
+ }
+ }
+
+ /**
* Returns an initialized bean declaration.
*
* @return the bean declaration
@@ -335,8 +436,8 @@ public class TestBeanHelper
{
TestBeanDeclaration data = new TestBeanDeclaration();
Map<String, Object> properties = new HashMap<String, Object>();
- properties.put("stringValue", "testString");
- properties.put("intValue", "42");
+ properties.put("stringValue", TEST_STRING);
+ properties.put("intValue", String.valueOf(TEST_INT));
data.setBeanProperties(properties);
TestBeanDeclaration buddyData = new TestBeanDeclaration();
Map<String, Object> properties2 = new HashMap<String, Object>();
@@ -362,9 +463,9 @@ public class TestBeanHelper
*/
private void checkBean(TestBean bean)
{
- assertEquals("Wrong string property", "testString", bean
+ assertEquals("Wrong string property", TEST_STRING, bean
.getStringValue());
- assertEquals("Wrong int property", 42, bean.getIntValue());
+ assertEquals("Wrong int property", TEST_INT, bean.getIntValue());
TestBean buddy = bean.getBuddy();
assertNotNull("Buddy was not set", buddy);
assertEquals("Wrong string property in buddy", "Another test string",
@@ -415,6 +516,32 @@ public class TestBeanHelper
}
/**
+ * Another test bean class which defines some constructors.
+ */
+ public static class TestCtorBean extends TestBean
+ {
+ public TestCtorBean()
+ {
+ }
+
+ public TestCtorBean(TestBean buddy)
+ {
+ setBuddy(buddy);
+ }
+
+ public TestCtorBean(String s)
+ {
+ setStringValue(s);
+ }
+
+ public TestCtorBean(String s, int i)
+ {
+ this(s);
+ setIntValue(i);
+ }
+ }
+
+ /**
* An implementation of the BeanFactory interface used for testing. This
* implementation is really simple: If the TestBean class is provided, a
new
* instance will be created. Otherwise an exception is thrown.
@@ -454,7 +581,7 @@ public class TestBeanHelper
/**
* A test implementation of the BeanDeclaration interface. This
- * implementation allows to set the values directly, which should be
+ * implementation allows setting the values directly, which should be
* returned by the methods required by the BeanDeclaration interface.
*/
static class TestBeanDeclaration implements BeanDeclaration
@@ -469,6 +596,8 @@ public class TestBeanHelper
private Map<String, Object> nestedBeanDeclarations;
+ private Collection<ConstructorArg> constructorArgs;
+
public String getBeanClassName()
{
return beanClassName;
@@ -521,8 +650,12 @@ public class TestBeanHelper
public Collection<ConstructorArg> getConstructorArgs()
{
- //TODO implementation
- return null;
+ return constructorArgs;
+ }
+
+ public void setConstructorArgs(Collection<ConstructorArg> args)
+ {
+ constructorArgs = args;
}
}
}