diff --git a/config/src/main/java/com/typesafe/config/Optional.java b/config/src/main/java/com/typesafe/config/Optional.java new file mode 100644 index 00000000..4645ed5f --- /dev/null +++ b/config/src/main/java/com/typesafe/config/Optional.java @@ -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 { + +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java b/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java index 6f425a95..e794132e 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigBeanImpl.java @@ -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); + } } diff --git a/config/src/test/java/beanconfig/ObjectsConfig.java b/config/src/test/java/beanconfig/ObjectsConfig.java new file mode 100644 index 00000000..f3009c96 --- /dev/null +++ b/config/src/test/java/beanconfig/ObjectsConfig.java @@ -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(); + } +} diff --git a/config/src/test/resources/beanconfig/beanconfig01.conf b/config/src/test/resources/beanconfig/beanconfig01.conf index 11adc078..fae411af 100644 --- a/config/src/test/resources/beanconfig/beanconfig01.conf +++ b/config/src/test/resources/beanconfig/beanconfig01.conf @@ -92,5 +92,10 @@ "configValue" : "hello world", "list" : [1,2,3], "unwrappedMap" : ${validation} + }, + "objects" : { + "valueObject": { + "mandatoryValue": "notNull" + } } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala index 64b3f0c4..755ab2ae 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigBeanFactoryTest.scala @@ -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] {