Merge pull request from karthicks/issue-399

Add Support for Optional Bean Properties.
This commit is contained in:
Havoc Pennington 2016-06-02 10:02:35 -04:00
commit 23c5306038
5 changed files with 142 additions and 2 deletions
config/src
main/java/com/typesafe/config
test
java/beanconfig
resources/beanconfig
scala/com/typesafe/config/impl

View File

@ -0,0 +1,14 @@
package com.typesafe.config;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Allows an config property to be {@code null}.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Optional {
}

View File

@ -4,6 +4,7 @@ import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@ -21,6 +22,7 @@ import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMemorySize;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
import com.typesafe.config.Optional;
/**
* Internal implementation detail, not ABI stable, do not touch.
@ -90,7 +92,9 @@ public class ConfigBeanImpl {
if (configValue != null) {
SimpleConfig.checkValid(path, expectedType, configValue, problems);
} else {
SimpleConfig.addMissing(problems, expectedType, path, config.origin());
if (!isOptionalProperty(clazz, beanProp)) {
SimpleConfig.addMissing(problems, expectedType, path, config.origin());
}
}
}
}
@ -105,7 +109,17 @@ public class ConfigBeanImpl {
Method setter = beanProp.getWriteMethod();
Type parameterType = setter.getGenericParameterTypes()[0];
Class<?> parameterClass = setter.getParameterTypes()[0];
Object unwrapped = getValue(clazz, parameterType, parameterClass, config, originalNames.get(beanProp.getName()));
String configPropName = originalNames.get(beanProp.getName());
// Is the property key missing in the config?
if (configPropName == null) {
// If so, continue if the field is marked as @{link Optional}
if (isOptionalProperty(clazz, beanProp)) {
continue;
}
// Otherwise, raise a {@link Missing} exception right here
throw new ConfigException.Missing(beanProp.getName());
}
Object unwrapped = getValue(clazz, parameterType, parameterClass, config, configPropName);
setter.invoke(bean, unwrapped);
}
return bean;
@ -252,4 +266,24 @@ public class ConfigBeanImpl {
return false;
}
private static boolean isOptionalProperty(Class beanClass, PropertyDescriptor beanProp) {
Field field = getField(beanClass, beanProp.getName());
return (field.getAnnotationsByType(Optional.class).length > 0);
}
private static Field getField(Class beanClass, String fieldName) {
try {
Field field = beanClass.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
// Don't give up yet. Try to look for field in super class, if any.
}
beanClass = beanClass.getSuperclass();
if (beanClass == null) {
return null;
}
return getField(beanClass, fieldName);
}
}

View File

@ -0,0 +1,68 @@
package beanconfig;
import com.typesafe.config.Optional;
public class ObjectsConfig {
public static class ValueObject {
@Optional
private String optionalValue;
private String mandatoryValue;
public String getMandatoryValue() {
return mandatoryValue;
}
public void setMandatoryValue(String mandatoryValue) {
this.mandatoryValue = mandatoryValue;
}
public String getOptionalValue() {
return optionalValue;
}
public void setOptionalValue(String optionalValue) {
this.optionalValue = optionalValue;
}
}
private ValueObject valueObject;
public ValueObject getValueObject() {
return valueObject;
}
public void setValueObject(ValueObject valueObject) {
this.valueObject = valueObject;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ObjectsConfig)) {
return false;
}
ObjectsConfig that = (ObjectsConfig) o;
return !(getValueObject() != null ? !getValueObject().equals(that.getValueObject()) : that.getValueObject() != null);
}
@Override
public int hashCode() {
return getValueObject() != null ? getValueObject().hashCode() : 0;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("ObjectsConfig{");
sb.append("innerType=").append(valueObject);
sb.append('}');
return sb.toString();
}
}

View File

@ -92,5 +92,10 @@
"configValue" : "hello world",
"list" : [1,2,3],
"unwrappedMap" : ${validation}
},
"objects" : {
"valueObject": {
"mandatoryValue": "notNull"
}
}
}

View File

@ -162,6 +162,25 @@ class ConfigBeanFactoryTest extends TestUtils {
assertEquals(42, beanConfig.getUnwrappedMap.get("should-be-boolean"))
}
@Test
def testOptionalProperties() {
val beanConfig: ObjectsConfig = ConfigBeanFactory.create(loadConfig().getConfig("objects"), classOf[ObjectsConfig])
assertNotNull(beanConfig)
assertNotNull(beanConfig.getValueObject)
assertNull(beanConfig.getValueObject.getOptionalValue)
assertEquals("notNull", beanConfig.getValueObject.getMandatoryValue)
}
@Test
def testNotAnOptionalProperty(): Unit = {
val e = intercept[ConfigException.ValidationFailed] {
ConfigBeanFactory.create(parseConfig("{valueObject: {}}"), classOf[ObjectsConfig])
}
assertTrue("missing value error", e.getMessage.contains("No setting"))
assertTrue("error about the right property", e.getMessage.contains("mandatoryValue"))
}
@Test
def testNotABeanField() {
val e = intercept[ConfigException.BadBean] {