From 811d12038bf8234e7f693ce571844de9385de144 Mon Sep 17 00:00:00 2001 From: ljacqu Date: Sun, 28 Sep 2025 21:36:06 +0200 Subject: [PATCH 1/3] #448 Change OptionalProperty to be based on a property type --- .../configme/properties/OptionalProperty.java | 86 +++++++------------ .../properties/PropertyInitializer.java | 30 ++++--- .../types/OptionalPropertyType.java | 39 +++++++++ .../configme/SettingsManagerImplTest.java | 3 +- .../properties/OptionalPropertyTest.java | 51 ++++------- .../configme/resource/UniqueCommentTest.java | 3 +- .../resource/YamlFileResourceTest.java | 10 ++- 7 files changed, 115 insertions(+), 107 deletions(-) create mode 100644 src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java diff --git a/src/main/java/ch/jalu/configme/properties/OptionalProperty.java b/src/main/java/ch/jalu/configme/properties/OptionalProperty.java index 55e6e32a..5b882c76 100644 --- a/src/main/java/ch/jalu/configme/properties/OptionalProperty.java +++ b/src/main/java/ch/jalu/configme/properties/OptionalProperty.java @@ -1,70 +1,50 @@ package ch.jalu.configme.properties; -import ch.jalu.configme.properties.convertresult.PropertyValue; -import ch.jalu.configme.resource.PropertyReader; +import ch.jalu.configme.properties.types.OptionalPropertyType; +import ch.jalu.configme.properties.types.PropertyType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Optional; /** - * Property which may be empty. - *

- * Wraps another property with an {@link Optional}: if a property is not present in the property resource, - * {@link Optional#empty} is returned. + * Optional property. Properties of this type may be absent from a property resource and it will still be considered + * valid. * * @param the type of value */ -public class OptionalProperty implements Property> { - - private final Property baseProperty; - private final Optional defaultValue; - - public OptionalProperty(@NotNull Property baseProperty) { - this.baseProperty = baseProperty; - this.defaultValue = Optional.empty(); - } - - public OptionalProperty(@NotNull Property baseProperty, @NotNull T defaultValue) { - this.baseProperty = baseProperty; - this.defaultValue = Optional.of(defaultValue); - } - - @Override - public @NotNull String getPath() { - return baseProperty.getPath(); - } - - @Override - public @NotNull PropertyValue> determineValue(@NotNull PropertyReader reader) { - PropertyValue basePropertyValue = baseProperty.determineValue(reader); - Optional value = basePropertyValue.isValidInResource() - ? Optional.ofNullable(basePropertyValue.getValue()) - : Optional.empty(); - - // Propagate the false "valid" property if the reader has a value at the base property's path - // and the base property says it's invalid -> triggers a rewrite to get rid of the invalid value. - boolean isWrongInResource = !basePropertyValue.isValidInResource() && reader.contains(baseProperty.getPath()); - return isWrongInResource - ? PropertyValue.withValueRequiringRewrite(value) - : PropertyValue.withValidValue(value); - } - - @Override - public @NotNull Optional getDefaultValue() { - return defaultValue; +public class OptionalProperty extends TypeBasedProperty> { + + /** + * Constructor. Creates a new property with an empty Optional as default value. + * + * @param path the path of the property + * @param valueType the property type of the value inside the optional + */ + public OptionalProperty(@NotNull String path, @NotNull PropertyType valueType) { + this(path, new OptionalPropertyType<>(valueType), Optional.empty()); } - @Override - public boolean isValidValue(@Nullable Optional value) { - if (value == null) { - return false; - } - return value.map(baseProperty::isValidValue).orElse(true); + /** + * Constructor. + * + * @param path the path of the property + * @param valueType the property type of the value inside the optional + * @param defaultValue the default value of the property (will be wrapped in an Optional) + */ + public OptionalProperty(@NotNull String path, @NotNull PropertyType valueType, @Nullable T defaultValue) { + this(path, new OptionalPropertyType<>(valueType), Optional.ofNullable(defaultValue)); } - @Override - public @Nullable Object toExportValue(@NotNull Optional value) { - return value.map(baseProperty::toExportValue).orElse(null); + /** + * Constructor. + * + * @param path the path of the property + * @param type the type of this property + * @param defaultValue the default value of this property + */ + public OptionalProperty(@NotNull String path, @NotNull PropertyType> type, + @NotNull Optional defaultValue) { + super(path, type, defaultValue); } } diff --git a/src/main/java/ch/jalu/configme/properties/PropertyInitializer.java b/src/main/java/ch/jalu/configme/properties/PropertyInitializer.java index 49e21957..cf7666d5 100644 --- a/src/main/java/ch/jalu/configme/properties/PropertyInitializer.java +++ b/src/main/java/ch/jalu/configme/properties/PropertyInitializer.java @@ -4,8 +4,15 @@ import ch.jalu.configme.properties.builder.CollectionPropertyBuilder; import ch.jalu.configme.properties.builder.MapPropertyBuilder; import ch.jalu.configme.properties.types.ArrayPropertyType; +import ch.jalu.configme.properties.types.BooleanType; +import ch.jalu.configme.properties.types.EnumPropertyType; import ch.jalu.configme.properties.types.InlineArrayPropertyType; +import ch.jalu.configme.properties.types.ListPropertyType; +import ch.jalu.configme.properties.types.NumberType; import ch.jalu.configme.properties.types.PropertyType; +import ch.jalu.configme.properties.types.RegexType; +import ch.jalu.configme.properties.types.SetPropertyType; +import ch.jalu.configme.properties.types.StringType; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -272,48 +279,47 @@ public static ArrayPropertyBuilder> inlineArrayPro // Optional flavors // -------------- public static @NotNull OptionalProperty optionalBooleanProperty(@NotNull String path) { - return new OptionalProperty<>(new BooleanProperty(path, false)); + return new OptionalProperty<>(path, BooleanType.BOOLEAN); } public static @NotNull OptionalProperty optionalShortProperty(@NotNull String path) { - return new OptionalProperty<>(new ShortProperty(path, (short) 0)); + return new OptionalProperty<>(path, NumberType.SHORT); } public static @NotNull OptionalProperty optionalIntegerProperty(@NotNull String path) { - return new OptionalProperty<>(new IntegerProperty(path, 0)); + return new OptionalProperty<>(path, NumberType.INTEGER); } public static @NotNull OptionalProperty optionalLongProperty(@NotNull String path) { - return new OptionalProperty<>(new LongProperty(path, 0L)); + return new OptionalProperty<>(path, NumberType.LONG); } public static @NotNull OptionalProperty optionalFloatProperty(@NotNull String path) { - return new OptionalProperty<>(new FloatProperty(path, 0f)); + return new OptionalProperty<>(path, NumberType.FLOAT); } public static @NotNull OptionalProperty optionalDoubleProperty(@NotNull String path) { - return new OptionalProperty<>(new DoubleProperty(path, 0.0)); + return new OptionalProperty<>(path, NumberType.DOUBLE); } public static @NotNull OptionalProperty optionalStringProperty(@NotNull String path) { - return new OptionalProperty<>(new StringProperty(path, "")); + return new OptionalProperty<>(path, StringType.STRING); } public static > @NotNull OptionalProperty optionalEnumProperty(@NotNull Class clazz, @NotNull String path) { - // default value may never be null, so get the first entry in the enum class - return new OptionalProperty<>(new EnumProperty<>(path, clazz, clazz.getEnumConstants()[0])); + return new OptionalProperty<>(path, new EnumPropertyType<>(clazz)); } public static @NotNull OptionalProperty optionalRegexProperty(@NotNull String path) { - return new OptionalProperty<>(new RegexProperty(path, "")); + return new OptionalProperty<>(path, RegexType.REGEX); } public static @NotNull OptionalProperty> optionalListProperty(@NotNull String path) { - return new OptionalProperty<>(new StringListProperty(path)); + return new OptionalProperty<>(path, new ListPropertyType<>(StringType.STRING)); } public static @NotNull OptionalProperty> optionalSetProperty(@NotNull String path) { - return new OptionalProperty<>(new StringSetProperty(path)); + return new OptionalProperty<>(path, new SetPropertyType<>(StringType.STRING)); } } diff --git a/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java b/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java new file mode 100644 index 00000000..f588a34d --- /dev/null +++ b/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java @@ -0,0 +1,39 @@ +package ch.jalu.configme.properties.types; + +import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +/** + * Property type for optionals. Wraps another property type. + * + * @param the value type of the optional + */ +public class OptionalPropertyType implements PropertyType> { + + private final PropertyType valueType; + + /** + * Constructor. + * + * @param valueType the property type to handle the value inside the optional + */ + public OptionalPropertyType(PropertyType valueType) { + this.valueType = valueType; + } + + @Override + public @Nullable Optional convert(@Nullable Object object, @NotNull ConvertErrorRecorder errorRecorder) { + if (object != null) { + return Optional.ofNullable(valueType.convert(object, errorRecorder)); + } + return Optional.empty(); + } + + @Override + public @Nullable Object toExportValue(@NotNull Optional value) { + return value.map(valueType::toExportValue).orElse(null); + } +} diff --git a/src/test/java/ch/jalu/configme/SettingsManagerImplTest.java b/src/test/java/ch/jalu/configme/SettingsManagerImplTest.java index 64b5f900..53967f06 100644 --- a/src/test/java/ch/jalu/configme/SettingsManagerImplTest.java +++ b/src/test/java/ch/jalu/configme/SettingsManagerImplTest.java @@ -9,6 +9,7 @@ import ch.jalu.configme.properties.BeanProperty; import ch.jalu.configme.properties.OptionalProperty; import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.types.NumberType; import ch.jalu.configme.resource.PropertyReader; import ch.jalu.configme.resource.PropertyResource; import ch.jalu.configme.resource.YamlFileResource; @@ -203,7 +204,7 @@ void shouldSetOptionalPropertyCorrectly() { PropertyResource resource = new YamlFileResource(file); SettingsManager settingsManager = new SettingsManagerImpl(resource, createConfiguration(TestConfiguration.class), null); - OptionalProperty intOptional = new OptionalProperty<>(newProperty("version", 65)); + OptionalProperty intOptional = new OptionalProperty<>("version", NumberType.INTEGER); // when settingsManager.setProperty(intOptional, Optional.empty()); diff --git a/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java b/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java index f7e19055..eb7a0c92 100644 --- a/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java +++ b/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java @@ -1,6 +1,10 @@ package ch.jalu.configme.properties; import ch.jalu.configme.properties.convertresult.PropertyValue; +import ch.jalu.configme.properties.types.BooleanType; +import ch.jalu.configme.properties.types.EnumPropertyType; +import ch.jalu.configme.properties.types.NumberType; +import ch.jalu.configme.properties.types.StringType; import ch.jalu.configme.resource.PropertyReader; import ch.jalu.configme.samples.TestEnum; import org.junit.jupiter.api.Test; @@ -16,10 +20,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Test for {@link OptionalProperty}. @@ -33,9 +33,9 @@ class OptionalPropertyTest { @Test void shouldReturnPresentValues() { // given - OptionalProperty booleanProp = new OptionalProperty<>(new BooleanProperty("bool.path.test", false)); - OptionalProperty intProp = new OptionalProperty<>(new IntegerProperty("int.path.test", 0)); - OptionalProperty enumProp = new OptionalProperty<>(new EnumProperty<>("enum.path.test", TestEnum.class, TestEnum.SECOND)); + OptionalProperty booleanProp = new OptionalProperty<>("bool.path.test", BooleanType.BOOLEAN); + OptionalProperty intProp = new OptionalProperty<>("int.path.test", NumberType.INTEGER); + OptionalProperty enumProp = new OptionalProperty<>("enum.path.test", new EnumPropertyType<>(TestEnum.class)); given(reader.getObject("bool.path.test")).willReturn(true); given(reader.getObject("int.path.test")).willReturn(27); @@ -55,9 +55,9 @@ void shouldReturnPresentValues() { @Test void shouldReturnEmptyOptional() { // given - OptionalProperty booleanProp = new OptionalProperty<>(new BooleanProperty("bool.path.wrong", false)); - OptionalProperty intProp = new OptionalProperty<>(new IntegerProperty("int.path.wrong", 0)); - OptionalProperty enumProp = new OptionalProperty<>(new EnumProperty<>("enum.path.wrong", TestEnum.class, TestEnum.SECOND)); + OptionalProperty booleanProp = new OptionalProperty<>("bool.path.wrong", BooleanType.BOOLEAN); + OptionalProperty intProp = new OptionalProperty<>("int.path.wrong", NumberType.INTEGER); + OptionalProperty enumProp = new OptionalProperty<>("enum.path.wrong", new EnumPropertyType<>(TestEnum.class)); // when PropertyValue> boolResult = booleanProp.determineValue(reader); @@ -73,7 +73,7 @@ void shouldReturnEmptyOptional() { @Test void shouldAllowToDefineDefaultValue() { // given - OptionalProperty integerProp = new OptionalProperty<>(new IntegerProperty("path", 0), 42); + OptionalProperty integerProp = new OptionalProperty<>("int.path.wrong", NumberType.INTEGER, 42); // when Optional defaultValue = integerProp.getDefaultValue(); @@ -85,38 +85,20 @@ void shouldAllowToDefineDefaultValue() { @Test void shouldReturnValueWithInvalidFlagIfReturnedFromReader() { // given - StringProperty baseProperty = spy(new StringProperty("the.path", "DEFAULT")); - doReturn(PropertyValue.withValueRequiringRewrite("this should be discarded")).when(baseProperty).determineValue(reader); - given(reader.contains("the.path")).willReturn(true); - OptionalProperty optionalProperty = new OptionalProperty<>(baseProperty); + given(reader.getObject("the.path")).willReturn(400); + OptionalProperty optionalProperty = new OptionalProperty<>("the.path", NumberType.BYTE); // when - PropertyValue> value = optionalProperty.determineValue(reader); + PropertyValue> value = optionalProperty.determineValue(reader); // then - assertThat(value, isErrorValueOf(Optional.empty())); - } - - @Test - void shouldDelegateToBasePropertyAndHaveEmptyOptionalAsDefault() { - // given - StringProperty baseProperty = new StringProperty("some.path", "Def"); - OptionalProperty property = new OptionalProperty<>(baseProperty); - - // when - Optional defaultValue = property.getDefaultValue(); - String path = property.getPath(); - - // then - assertThat(defaultValue, equalTo(Optional.empty())); - assertThat(path, equalTo("some.path")); + assertThat(value, isErrorValueOf(Optional.of(Byte.MAX_VALUE))); } @Test void shouldValidateWithBasePropertyNullSafe() { // given - StringProperty baseProperty = spy(new StringProperty("some.path", "Def")); - OptionalProperty property = new OptionalProperty<>(baseProperty); + OptionalProperty property = new OptionalProperty<>("path", StringType.STRING); // when boolean isEmptyValid = property.isValidValue(Optional.empty()); @@ -127,6 +109,5 @@ void shouldValidateWithBasePropertyNullSafe() { assertThat(isEmptyValid, equalTo(true)); assertThat(isValueValid, equalTo(true)); assertThat(isNullValid, equalTo(false)); - verify(baseProperty, only()).isValidValue("foo"); } } diff --git a/src/test/java/ch/jalu/configme/resource/UniqueCommentTest.java b/src/test/java/ch/jalu/configme/resource/UniqueCommentTest.java index 743673ed..fb55a6ca 100644 --- a/src/test/java/ch/jalu/configme/resource/UniqueCommentTest.java +++ b/src/test/java/ch/jalu/configme/resource/UniqueCommentTest.java @@ -5,7 +5,6 @@ import ch.jalu.configme.TestUtils; import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.configurationdata.ConfigurationDataBuilder; -import ch.jalu.configme.properties.BeanProperty; import ch.jalu.configme.properties.ListProperty; import ch.jalu.configme.properties.MapProperty; import ch.jalu.configme.properties.OptionalProperty; @@ -141,7 +140,7 @@ public static final class ServerSettingHolder implements SettingsHolder { new ServerCollection(true, "reception"), new ServerCollection(false, "lobby")); public static final Property> ALT = - new OptionalProperty<>(new BeanProperty<>("alternative", ServerCollection.class, new ServerCollection())); + new OptionalProperty<>("alternative", BeanPropertyType.of(ServerCollection.class)); private ServerSettingHolder() { } diff --git a/src/test/java/ch/jalu/configme/resource/YamlFileResourceTest.java b/src/test/java/ch/jalu/configme/resource/YamlFileResourceTest.java index c37c7fb5..d825c16c 100644 --- a/src/test/java/ch/jalu/configme/resource/YamlFileResourceTest.java +++ b/src/test/java/ch/jalu/configme/resource/YamlFileResourceTest.java @@ -8,6 +8,8 @@ import ch.jalu.configme.properties.BeanProperty; import ch.jalu.configme.properties.OptionalProperty; import ch.jalu.configme.properties.Property; +import ch.jalu.configme.properties.types.EnumPropertyType; +import ch.jalu.configme.properties.types.NumberType; import ch.jalu.configme.samples.TestConfiguration; import ch.jalu.configme.samples.TestEnum; import org.junit.jupiter.api.Test; @@ -195,8 +197,8 @@ void shouldExportConfigurationWithExpectedComments() throws IOException { void shouldSkipAbsentOptionalProperty() throws IOException { // given ConfigurationData configurationData = createConfiguration(asList( - new OptionalProperty<>(TestConfiguration.DURATION_IN_SECONDS), - new OptionalProperty<>(TestConfiguration.RATIO_ORDER))); + new OptionalProperty<>("test.duration", NumberType.INTEGER), + new OptionalProperty<>("sample.ratio.order", new EnumPropertyType<>(TestEnum.class)))); Path file = copyFileFromResources(INCOMPLETE_FILE); PropertyResource resource = new YamlFileResource(file); configurationData.initializeValues(resource.createReader()); @@ -216,8 +218,8 @@ void shouldSkipAbsentOptionalProperty() throws IOException { void shouldExportAllPresentOptionalProperties() throws IOException { // given ConfigurationData configurationData = createConfiguration(asList( - new OptionalProperty<>(TestConfiguration.DURATION_IN_SECONDS), - new OptionalProperty<>(TestConfiguration.RATIO_ORDER))); + new OptionalProperty<>("test.duration", NumberType.INTEGER), + new OptionalProperty<>("sample.ratio.order", new EnumPropertyType<>(TestEnum.class)))); Path file = copyFileFromResources(COMPLETE_FILE); PropertyResource resource = new YamlFileResource(file); configurationData.initializeValues(resource.createReader()); From d78751d0d53565e9a861c63fa269d53e5a9fbc5e Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 31 Dec 2025 16:42:15 +0100 Subject: [PATCH 2/3] #448 Specify @NotNull on OptionalPropertyType#convert --- .../ch/jalu/configme/properties/types/OptionalPropertyType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java b/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java index f588a34d..a8e78720 100644 --- a/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java +++ b/src/main/java/ch/jalu/configme/properties/types/OptionalPropertyType.java @@ -25,7 +25,7 @@ public OptionalPropertyType(PropertyType valueType) { } @Override - public @Nullable Optional convert(@Nullable Object object, @NotNull ConvertErrorRecorder errorRecorder) { + public @NotNull Optional convert(@Nullable Object object, @NotNull ConvertErrorRecorder errorRecorder) { if (object != null) { return Optional.ofNullable(valueType.convert(object, errorRecorder)); } From 84f8967a52d5ad0a7e6500c0dc7bd472d91079ea Mon Sep 17 00:00:00 2001 From: ljacqu Date: Wed, 31 Dec 2025 16:55:23 +0100 Subject: [PATCH 3/3] #448 Add tests for the export value of Optional properties --- .../properties/OptionalPropertyTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java b/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java index eb7a0c92..175bf3ba 100644 --- a/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java +++ b/src/test/java/ch/jalu/configme/properties/OptionalPropertyTest.java @@ -4,6 +4,7 @@ import ch.jalu.configme.properties.types.BooleanType; import ch.jalu.configme.properties.types.EnumPropertyType; import ch.jalu.configme.properties.types.NumberType; +import ch.jalu.configme.properties.types.PropertyType; import ch.jalu.configme.properties.types.StringType; import ch.jalu.configme.resource.PropertyReader; import ch.jalu.configme.samples.TestEnum; @@ -13,13 +14,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; +import java.util.concurrent.TimeUnit; import static ch.jalu.configme.TestUtils.isErrorValueOf; import static ch.jalu.configme.TestUtils.isValidValueOf; import static java.util.Optional.of; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Test for {@link OptionalProperty}. @@ -110,4 +114,42 @@ void shouldValidateWithBasePropertyNullSafe() { assertThat(isValueValid, equalTo(true)); assertThat(isNullValid, equalTo(false)); } + + @Test + void shouldReturnNullAsExportValue() { + // given + OptionalProperty property = new OptionalProperty<>("int.path", NumberType.INTEGER); + + // when + Object exportValue = property.toExportValue(Optional.empty()); + + // then + assertThat(exportValue, nullValue()); + } + + @Test + void shouldReturnNullIfValuePropertyTypeReturnsNull() { + // given + PropertyType valuePropertyType = mock(PropertyType.class); + given(valuePropertyType.toExportValue("demo")).willReturn(null); + OptionalProperty optionalProperty = new OptionalProperty<>("int.path", valuePropertyType); + + // when + Object exportValue = optionalProperty.toExportValue(Optional.of("demo")); + + // then + assertThat(exportValue, nullValue()); + } + + @Test + void shouldConstructExportValue() { + // given + OptionalProperty optionalProperty = new OptionalProperty<>("duration.unit", EnumPropertyType.of(TimeUnit.class)); + + // when + Object exportValue = optionalProperty.toExportValue(Optional.of(TimeUnit.HOURS)); + + // then + assertThat(exportValue, equalTo("HOURS")); + } }