From 790ff57b8bc85681e5f3c5b0e8728c762a715ee9 Mon Sep 17 00:00:00 2001 From: Oswaldo Baptista Vicente Junior <45291656+oswaldobapvicjr@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:14:53 -0300 Subject: [PATCH] Infer the mapper based on the file extension (#176) --- .../obvj/confectory/ConfigurationBuilder.java | 64 +++++++++-- .../obvj/confectory/mapper/DynamicMapper.java | 102 ++++++++++++++++++ .../confectory/ConfigurationBuilderTest.java | 22 +++- .../confectory/mapper/DynamicMapperTest.java | 96 +++++++++++++++++ .../ConfectoryTestDriveGenericMapperIni.java | 39 +++++++ ...ctoryTestDriveGenericMapperProperties.java | 38 +++++++ .../ConfectoryTestDriveGenericMapperXml.java | 39 +++++++ .../src/test/resources/testfiles/note.xml | 6 ++ 8 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 confectory-core/src/main/java/net/obvj/confectory/mapper/DynamicMapper.java create mode 100644 confectory-core/src/test/java/net/obvj/confectory/mapper/DynamicMapperTest.java create mode 100644 confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperIni.java create mode 100644 confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperProperties.java create mode 100644 confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperXml.java create mode 100644 confectory-core/src/test/resources/testfiles/note.xml diff --git a/confectory-core/src/main/java/net/obvj/confectory/ConfigurationBuilder.java b/confectory-core/src/main/java/net/obvj/confectory/ConfigurationBuilder.java index 07803d37..fb591c3e 100644 --- a/confectory-core/src/main/java/net/obvj/confectory/ConfigurationBuilder.java +++ b/confectory-core/src/main/java/net/obvj/confectory/ConfigurationBuilder.java @@ -16,10 +16,9 @@ package net.obvj.confectory; -import java.util.Objects; - -import org.apache.commons.lang3.StringUtils; +import static org.apache.commons.lang3.StringUtils.*; +import net.obvj.confectory.mapper.DynamicMapper; import net.obvj.confectory.mapper.Mapper; import net.obvj.confectory.source.DynamicSource; import net.obvj.confectory.source.Source; @@ -31,7 +30,7 @@ * For example: * *
- * + * Example 1: Combining a source and a mapper (type-safe): *
  * {@code Configuration config = new ConfigurationBuilder()}
  * {@code         .source(new ClasspathFileSource<>("my.properties"))}
@@ -43,6 +42,18 @@
  * 
* *
+ *
+ * Example 2: Building a generic configuration + * (the actual mapper will be inferred based on the source file extension): + *
+ * {@code Configuration config = Configuration.builder()}
+ * {@code         .source("my.properties")}
+ * {@code         .lazy()}
+ * {@code         .build();}
+ * 
+ * + *
+ * * @param the target configuration type * @@ -55,6 +66,7 @@ public class ConfigurationBuilder implements ConfigurationMetadataRetriever source; private Mapper mapper; private boolean optional; @@ -145,12 +157,16 @@ public ConfigurationBuilder source(Source source) */ public ConfigurationBuilder source(String path) { + this.path = path; this.source = SourceFactory.dynamicSource(path); return this; } /** * Defines the {@link Mapper} of the new {@code Configuration}. + *

+ * Note: Since 2.6.0, if not specified, a default mapper will be defined + * dynamically, based on the source file extension, if present. * * @param mapper the {@link Mapper} to be set; not null * @return a reference to this same {@code ConfigurationBuilder} for chained calls @@ -237,21 +253,42 @@ public ConfigurationBuilder bean(T bean) * Builds the target {@code Configuration}. * * @return a new {@link Configuration} object - * @throws NullPointerException if either the {@code Source} or {@code Mapper} - * configuration parameters are missing + * @throws IllegalStateException if the {@code Source} parameter is missing; or the + * {@code Mapper} is missing; or the {@code Mapper} + * could not be inferred * @throws ConfigurationSourceException in the event of a failure loading the * configuration source */ public Configuration build() { - Objects.requireNonNull(source, "The configuration source must not be null"); - Objects.requireNonNull(mapper, "The configuration mapper must not be null"); + requireNonNullForBuild(source, "The configuration source must not be null"); + evaluateMapper(); - namespace = StringUtils.defaultString(namespace); + namespace = defaultString(namespace); return new Configuration<>(this); } + /** + * Secure the existence of a {@link Mapper}. Try to infer one if not specified. + * + * @throws IllegalStateException if the {@link Mapper} is null and could not be inferred + * @since 2.6.0 + */ + private void evaluateMapper() + { + if (mapper == null) + { + // Try to infer based on the file extension if present + String extension = substringAfterLast(path, "."); + if (isEmpty(extension)) + { + throw new IllegalStateException("The mapper could not be inferred. Please specify a concrete mapper."); + } + mapper = (Mapper) new DynamicMapper(extension); + } + } + @Override public String getNamespace() { @@ -293,4 +330,13 @@ public T getBean() return bean; } + private static T requireNonNullForBuild(T object, String message) + { + if (object == null) + { + throw new IllegalStateException(message); + } + return object; + } + } diff --git a/confectory-core/src/main/java/net/obvj/confectory/mapper/DynamicMapper.java b/confectory-core/src/main/java/net/obvj/confectory/mapper/DynamicMapper.java new file mode 100644 index 00000000..863aee02 --- /dev/null +++ b/confectory-core/src/main/java/net/obvj/confectory/mapper/DynamicMapper.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 obvj.net + * + * 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 net.obvj.confectory.mapper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.function.Supplier; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.obvj.confectory.internal.helper.ConfigurationHelper; +import net.obvj.confectory.util.Exceptions; + +/** + * A dynamic {@link Mapper} which automatically assigns a concrete mapping class + * (from the default core mappers) depending on a given file extension: + *

    + *
  • "ini" assigns the {@link INIToJSONObjectMapper} + *
  • "json" assigns the {@link JSONObjectMapper} + *
  • "properties" assigns the {@link PropertiesMapper} + *
  • "txt" assigns the {@link StringMapper} + *
  • "xml" assigns the {@link DocumentMapper} + *
+ * + * @author oswaldo.bapvic.jr (Oswaldo Junior) + * @since 2.6.0 + */ +public final class DynamicMapper extends AbstractBeanMapper implements Mapper +{ + private static final Map>> MAPPERS_BY_EXTENSION = Map.of( + "ini", INIToJSONObjectMapper::new, + "json", JSONObjectMapper::new, + "properties", PropertiesMapper::new, + "txt", StringMapper::new, + "xml", DocumentMapper::new + ); + + private static final Logger LOGGER = LoggerFactory.getLogger(DynamicMapper.class); + + private final Mapper actualMapper; + + /** + * Builds a new dynamic configuration mapper for the specified extension + * + * @param extension the file extension; not null + * @throws IllegalArgumentException if the extension is empty or unknown + */ + public DynamicMapper(String extension) + { + this.actualMapper = findMapper(extension); + } + + private static Mapper findMapper(String extension) + { + Supplier> supplier = MAPPERS_BY_EXTENSION.get(StringUtils.lowerCase(extension)); + if (supplier == null) + { + throw Exceptions.illegalArgument( + "No default mapper available for the extension: \"%s\"", extension); + } + return (Mapper) supplier.get(); + } + + @Override + public Object apply(InputStream inputStream) throws IOException + { + LOGGER.debug("Applying mapper {}", actualMapper.getClass()); + return actualMapper.apply(inputStream); + } + + @Override + public ConfigurationHelper configurationHelper(Object bean) + { + return actualMapper.configurationHelper(bean); + } + + /** + * @return The actual mapper + */ + public Mapper getActualMapper() + { + return actualMapper; + } + +} diff --git a/confectory-core/src/test/java/net/obvj/confectory/ConfigurationBuilderTest.java b/confectory-core/src/test/java/net/obvj/confectory/ConfigurationBuilderTest.java index 3ef20120..15540d8c 100644 --- a/confectory-core/src/test/java/net/obvj/confectory/ConfigurationBuilderTest.java +++ b/confectory-core/src/test/java/net/obvj/confectory/ConfigurationBuilderTest.java @@ -19,6 +19,7 @@ import static net.obvj.junit.utils.matchers.AdvancedMatchers.containsAll; import static net.obvj.junit.utils.matchers.AdvancedMatchers.throwsException; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.Mockito.mock; @@ -31,7 +32,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import net.obvj.confectory.internal.helper.ConfigurationHelper; +import net.obvj.confectory.mapper.DynamicMapper; import net.obvj.confectory.mapper.Mapper; +import net.obvj.confectory.mapper.PropertiesMapper; import net.obvj.confectory.source.AbstractSource; import net.obvj.confectory.source.DynamicSource; import net.obvj.confectory.source.Source; @@ -97,20 +100,31 @@ void constructor_exisitingConfiguration_presetBuilder() } @Test - void build_nullSource_nullPointerException() + void build_nullSource_illegalStateException() { ConfigurationBuilder builder = new ConfigurationBuilder<>(); assertThat(() -> builder.build(), - throwsException(NullPointerException.class).withMessage("The configuration source must not be null")); + throwsException(IllegalStateException.class).withMessage("The configuration source must not be null")); } @Test - void build_nullMapper_nullPointerException() + void build_nullMapperAndCouldNotBeInferred_illegalStateException() { ConfigurationBuilder builder = new ConfigurationBuilder<>() .source(source); assertThat(() -> builder.build(), - throwsException(NullPointerException.class).withMessage("The configuration mapper must not be null")); + throwsException(IllegalStateException.class).withMessage("The mapper could not be inferred. Please specify a concrete mapper.")); + } + + @Test + void build_nullMapperButCouldBeInferred_dynamicMapperAssignedBasedOnExtension() + { + ConfigurationBuilder builder = new ConfigurationBuilder<>() + .source("testfiles/my.properties").lazy(); + Configuration config = builder.build(); + + DynamicMapper assignedMapper = (DynamicMapper) config.getMapper(); + assertThat(assignedMapper.getActualMapper().getClass(), is(PropertiesMapper.class)); } @Test diff --git a/confectory-core/src/test/java/net/obvj/confectory/mapper/DynamicMapperTest.java b/confectory-core/src/test/java/net/obvj/confectory/mapper/DynamicMapperTest.java new file mode 100644 index 00000000..65552003 --- /dev/null +++ b/confectory-core/src/test/java/net/obvj/confectory/mapper/DynamicMapperTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 obvj.net + * + * 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 net.obvj.confectory.mapper; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import net.obvj.junit.utils.matchers.AdvancedMatchers; + +/** + * Unit tests for the {@link DynamicMapper}. + * + * @author oswaldo.bapvic.jr + * @since 2.6.0 + */ +class DynamicMapperTest +{ + static final String TEST_INI_CONTENT = "[web]\nhost=localhost\nport=1910"; + static final String TEST_PROPERTIES_CONTENT = "web.host=localhost\nweb.port=1910"; + static final String TEST_JSON_CONTENT = "{\"web\":{\"host\":\"localhost\",\"port\":1910}}"; + static final String TEST_TXT_CONTENT = "localhost:1910"; + static final String TEST_XML_CONTENT = "localhost1910"; + + private static ByteArrayInputStream asInputStream(String string) + { + return new ByteArrayInputStream(string.getBytes()); + } + + @Test + void apply_ini_loadedSuccessfully() throws IOException + { + Mapper mapper = new DynamicMapper("INI"); + Object bean = mapper.apply(asInputStream(TEST_INI_CONTENT)); + assertThat(mapper.configurationHelper(bean).getString("web.host"), equalTo("localhost")); + } + + @Test + void apply_json_loadedSuccessfully() throws IOException + { + Mapper mapper = new DynamicMapper("JSON"); + Object bean = mapper.apply(asInputStream(TEST_JSON_CONTENT)); + assertThat(mapper.configurationHelper(bean).getString("$.web.host"), equalTo("localhost")); + } + + @Test + void apply_properties_loadedSuccessfully() throws IOException + { + Mapper mapper = new DynamicMapper("PROPERTIES"); + Object bean = mapper.apply(asInputStream(TEST_PROPERTIES_CONTENT)); + assertThat(mapper.configurationHelper(bean).getInteger("web.port"), equalTo(1910)); + } + + @Test + void apply_txt_loadedSuccessfully() throws IOException + { + Mapper mapper = new DynamicMapper("TXT"); + Object bean = mapper.apply(asInputStream(TEST_TXT_CONTENT)); + assertThat(mapper.configurationHelper(bean).getAsString(), equalTo("localhost:1910")); + } + + @Test + void apply_xml_loadedSuccessfully() throws IOException + { + Mapper mapper = new DynamicMapper("XML"); + Object bean = mapper.apply(asInputStream(TEST_XML_CONTENT)); + assertThat(mapper.configurationHelper(bean).getInteger("/web/port"), equalTo(1910)); + } + + @Test + void constructor_unknown_illegalArgument() + { + assertThat(() -> new DynamicMapper("unknown"), + AdvancedMatchers.throwsException(IllegalArgumentException.class) + .withMessage("No default mapper available for the extension: \"unknown\"")); + } + +} diff --git a/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperIni.java b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperIni.java new file mode 100644 index 00000000..52db2d0e --- /dev/null +++ b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperIni.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 obvj.net + * + * 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 net.obvj.confectory.testdrive; + +import net.obvj.confectory.Configuration; + +public class ConfectoryTestDriveGenericMapperIni +{ + public static void main(String[] args) + { + Configuration config = Configuration.builder() + .source("testfiles/my-app.ini") + .lazy() + .build(); + + System.out.println(config.getBean()); + System.out.println(); + System.out.println(config.getAsString()); + System.out.println(); + System.out.println(config.getString("title")); + System.out.println(config.getString("owner.organization")); + System.out.println(config.getInteger("database.port")); + + } +} diff --git a/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperProperties.java b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperProperties.java new file mode 100644 index 00000000..ce7cb632 --- /dev/null +++ b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 obvj.net + * + * 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 net.obvj.confectory.testdrive; + +import net.obvj.confectory.Configuration; + +public class ConfectoryTestDriveGenericMapperProperties +{ + public static void main(String[] args) + { + Configuration config = Configuration.builder() + .source("testfiles/my-props.properties") + .required() + .build(); + + System.out.println(config.getBean()); + System.out.println(); + System.out.println(config.getAsString()); + System.out.println(); + System.out.println(config.getString("web.host")); + System.out.println(config.getInteger("web.port")); + + } +} diff --git a/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperXml.java b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperXml.java new file mode 100644 index 00000000..849ccd71 --- /dev/null +++ b/confectory-core/src/test/java/net/obvj/confectory/testdrive/ConfectoryTestDriveGenericMapperXml.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 obvj.net + * + * 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 net.obvj.confectory.testdrive; + +import net.obvj.confectory.Configuration; + +public class ConfectoryTestDriveGenericMapperXml +{ + public static void main(String[] args) + { + Configuration config = Configuration.builder() + .source("testfiles/note.xml") + .build(); + + System.out.println(config.getBean()); + System.out.println(); + System.out.println(config.getAsString()); + System.out.println(); + System.out.println(config.getString("note/to")); + System.out.println(config.getString("note/from")); + System.out.println(config.getString("note/heading")); + System.out.println(config.getString("note/body")); + + } +} diff --git a/confectory-core/src/test/resources/testfiles/note.xml b/confectory-core/src/test/resources/testfiles/note.xml new file mode 100644 index 00000000..9b2d252c --- /dev/null +++ b/confectory-core/src/test/resources/testfiles/note.xml @@ -0,0 +1,6 @@ + + Tove + Jani + Reminder + Go, Corinthians! +