From 2b02e8f845f917848ff3bd07f4b13ea465fe11b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:41:56 +0000 Subject: [PATCH 1/3] Update dependency org.postgresql:postgresql to v42.7.4 --- sadu-examples/build.gradle.kts | 2 +- sadu-postgresql/build.gradle.kts | 2 +- sadu-queries/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sadu-examples/build.gradle.kts b/sadu-examples/build.gradle.kts index 989e91e4..6a2bae28 100644 --- a/sadu-examples/build.gradle.kts +++ b/sadu-examples/build.gradle.kts @@ -6,7 +6,7 @@ dependencies { // database driver compileOnly("org.xerial", "sqlite-jdbc", "3.46.1.0") - compileOnly("org.postgresql", "postgresql", "42.7.3") + compileOnly("org.postgresql", "postgresql", "42.7.4") compileOnly("org.mariadb.jdbc", "mariadb-java-client", "3.4.1") compileOnly("mysql", "mysql-connector-java", "8.0.33") } diff --git a/sadu-postgresql/build.gradle.kts b/sadu-postgresql/build.gradle.kts index 4827ec33..e797fb49 100644 --- a/sadu-postgresql/build.gradle.kts +++ b/sadu-postgresql/build.gradle.kts @@ -4,7 +4,7 @@ dependencies { api(project(":sadu-updater")) api(project(":sadu-mapper")) - testImplementation("org.postgresql", "postgresql", "42.7.3") + testImplementation("org.postgresql", "postgresql", "42.7.4") testImplementation(testlibs.bundles.junit) testImplementation(project(":sadu-queries")) testImplementation(project(":sadu-datasource")) diff --git a/sadu-queries/build.gradle.kts b/sadu-queries/build.gradle.kts index 234e9026..083ce320 100644 --- a/sadu-queries/build.gradle.kts +++ b/sadu-queries/build.gradle.kts @@ -1,7 +1,7 @@ dependencies { api(project(":sadu-core")) api(project(":sadu-mapper")) - testImplementation("org.postgresql", "postgresql", "42.7.3") + testImplementation("org.postgresql", "postgresql", "42.7.4") testImplementation(testlibs.bundles.junit) testImplementation(project(":sadu-datasource")) testImplementation(project(":sadu-postgresql")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 684ea888..f84fd210 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,7 +61,7 @@ dependencyResolutionManagement { version("slf4j", "2.0.16") library("slf4j-noop", "org.slf4j", "slf4j-nop").versionRef("slf4j") - library("driver-postgres", "org.postgresql:postgresql:42.7.3") + library("driver-postgres", "org.postgresql:postgresql:42.7.4") library("driver-mariadb", "org.mariadb.jdbc:mariadb-java-client:3.4.1") library("driver-sqlite", "org.xerial:sqlite-jdbc:3.46.1.0") library("driver-mysql", "com.mysql:mysql-connector-j:9.0.0") From 52c7f91ae62f13b9b4a8065e730437a8f005621a Mon Sep 17 00:00:00 2001 From: Lilly Tempest <46890129+rainbowdashlabs@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:34:14 +0200 Subject: [PATCH 2/3] Add row mapping annotation and auto discovery (#213) --- build.gradle.kts | 2 +- sadu-mapper/build.gradle.kts | 9 ++ .../chojo/sadu/mapper/RowMapperRegistry.java | 85 ++++++++++++ .../mapper/annotation/MappingProvider.java | 28 ++++ .../exceptions/InvalidMappingException.java | 18 +++ .../chojo/sadu/mapper/PostgresDatabase.java | 23 ++++ .../annotation/MappingProviderTest.java | 121 ++++++++++++++++++ settings.gradle.kts | 3 +- 8 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 sadu-mapper/src/main/java/de/chojo/sadu/mapper/annotation/MappingProvider.java create mode 100644 sadu-mapper/src/main/java/de/chojo/sadu/mapper/exceptions/InvalidMappingException.java create mode 100644 sadu-mapper/src/test/java/de/chojo/sadu/mapper/PostgresDatabase.java create mode 100644 sadu-mapper/src/test/java/de/chojo/sadu/mapper/annotation/MappingProviderTest.java diff --git a/build.gradle.kts b/build.gradle.kts index ee3c36e2..aa93bc2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { publishData { useEldoNexusRepos(false) - publishingVersion = "2.2.5" + publishingVersion = "2.3.0" } group = "de.chojo.sadu" diff --git a/sadu-mapper/build.gradle.kts b/sadu-mapper/build.gradle.kts index e5c3a763..d6e0dd27 100644 --- a/sadu-mapper/build.gradle.kts +++ b/sadu-mapper/build.gradle.kts @@ -2,4 +2,13 @@ description = "SADU module to map values received from a database to java object dependencies { api(project(":sadu-core")) + + testImplementation("org.postgresql", "postgresql", "42.7.3") + testImplementation(testlibs.bundles.junit) + testImplementation(project(":sadu-queries")) + testImplementation(project(":sadu-datasource")) + testImplementation(project(":sadu-postgresql")) + + testImplementation(testlibs.bundles.database.postgres) + testImplementation(testlibs.slf4j.noop) } diff --git a/sadu-mapper/src/main/java/de/chojo/sadu/mapper/RowMapperRegistry.java b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/RowMapperRegistry.java index 23518c13..a63992f4 100644 --- a/sadu-mapper/src/main/java/de/chojo/sadu/mapper/RowMapperRegistry.java +++ b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/RowMapperRegistry.java @@ -6,14 +6,24 @@ package de.chojo.sadu.mapper; +import de.chojo.sadu.mapper.annotation.MappingProvider; +import de.chojo.sadu.mapper.exceptions.InvalidMappingException; import de.chojo.sadu.mapper.exceptions.MappingAlreadyRegisteredException; import de.chojo.sadu.mapper.exceptions.MappingException; import de.chojo.sadu.mapper.rowmapper.RowMapper; +import de.chojo.sadu.mapper.rowmapper.RowMapping; +import de.chojo.sadu.mapper.wrapper.Row; +import org.slf4j.Logger; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -21,11 +31,14 @@ import java.util.Map; import java.util.Optional; +import static org.slf4j.LoggerFactory.getLogger; + /** * Class to register {@link RowMapper} to map rows to objects. */ public class RowMapperRegistry { private final Map, List>> mapper = new HashMap<>(); + private static final Logger log = getLogger(RowMapperRegistry.class); public RowMapperRegistry() { } @@ -176,6 +189,78 @@ public RowMapper findOrWildcard(Class clazz, ResultSetMetaData meta, M if (mapper.isPresent()) { return mapper.get(); } + + // Autodetect mapper if available + if (registerInternal(clazz)) { + return findOrWildcard(clazz, meta, config); + } + throw MappingException.create(clazz, meta); } + + /** + * Registers row mapper of a class if they contain a constructor or static method annotated with {@link MappingProvider} + * + * @param clazz clazz to be registered + * @param type of class to be registered + * @throws InvalidMappingException when no mapping was found. + */ + public void register(Class clazz) { + if (registerInternal(clazz)) return; + throw new InvalidMappingException("No mapping was detected for class %s".formatted(clazz.getName())); + } + + private boolean registerInternal(Class clazz) { + boolean method = discoverMethodMapping(clazz); + boolean constructor = discoverConstructorMapping(clazz); + return method || constructor; + } + + @SuppressWarnings("unchecked") + private boolean discoverMethodMapping(Class clazz) { + List methods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(MappingProvider.class) && Modifier.isStatic(method.getModifiers())) + .toList(); + + for (Method method : methods) { + MappingProvider provider = method.getAnnotation(MappingProvider.class); + RowMapping mapper; + try { + mapper = (RowMapping) method.invoke(null); + } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) { + throw new InvalidMappingException("Could not retrieve mapping. Method has to return %s<%s> and take no arguments".formatted(RowMapping.class.getName(), clazz.getName()), e); + } + register(RowMapper.forClass(clazz).mapper(mapper).addColumns(provider.value()).build()); + log.info("Registered method auto mapping for {} with rows {}", clazz.getName(), provider.value()); + + } + return !methods.isEmpty(); + } + + @SuppressWarnings("unchecked") + private boolean discoverConstructorMapping(Class clazz) { + List> constructors = Arrays.stream(clazz.getDeclaredConstructors()) + .filter(constr -> constr.isAnnotationPresent(MappingProvider.class)) + .toList(); + + for (Constructor constructor : constructors) { + MappingProvider provider = constructor.getAnnotation(MappingProvider.class); + if (constructor.getParameterCount() != 1) { + throw new InvalidMappingException("Signature of a constructor with MappingProvider should be Constructor(Row)"); + } + if (constructor.getParameterTypes()[0] != Row.class) { + throw new InvalidMappingException("Signature of a constructor with MappingProvider should be Constructor(Row), but was Constructor(%s)".formatted(constructor.getParameterTypes()[0].getName())); + } + + register(RowMapper.forClass(clazz).mapper(row -> { + try { + return (V) constructor.newInstance(row); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new InvalidMappingException("Failed to create instance from RowMapping via constructor"); + } + }).addColumns(provider.value()).build()); + log.info("Registered constructor auto mapping for {} with rows {}", clazz.getName(), provider.value()); + } + return !constructors.isEmpty(); + } } diff --git a/sadu-mapper/src/main/java/de/chojo/sadu/mapper/annotation/MappingProvider.java b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/annotation/MappingProvider.java new file mode 100644 index 00000000..c42bbe9a --- /dev/null +++ b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/annotation/MappingProvider.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * Copyright (C) RainbowDashLabs and Contributor + */ + +package de.chojo.sadu.mapper.annotation; + +import de.chojo.sadu.mapper.rowmapper.RowMapping; +import de.chojo.sadu.mapper.wrapper.Row; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark a constructor or method as a {@link RowMapping} provider. + *

+ * A Method needs to return a {@link RowMapping} which maps to the annotated class. + *

+ * A Constructor needs to accept only a {@link Row}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface MappingProvider { + String[] value(); +} diff --git a/sadu-mapper/src/main/java/de/chojo/sadu/mapper/exceptions/InvalidMappingException.java b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/exceptions/InvalidMappingException.java new file mode 100644 index 00000000..386d194c --- /dev/null +++ b/sadu-mapper/src/main/java/de/chojo/sadu/mapper/exceptions/InvalidMappingException.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * Copyright (C) RainbowDashLabs and Contributor + */ + +package de.chojo.sadu.mapper.exceptions; + +public class InvalidMappingException extends RuntimeException { + + public InvalidMappingException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidMappingException(String message) { + super(message); + } +} diff --git a/sadu-mapper/src/test/java/de/chojo/sadu/mapper/PostgresDatabase.java b/sadu-mapper/src/test/java/de/chojo/sadu/mapper/PostgresDatabase.java new file mode 100644 index 00000000..06c19043 --- /dev/null +++ b/sadu-mapper/src/test/java/de/chojo/sadu/mapper/PostgresDatabase.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * Copyright (C) RainbowDashLabs and Contributor + */ + +package de.chojo.sadu.mapper; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +public class PostgresDatabase { + public static GenericContainer createContainer(String user, String pw) { + GenericContainer self = new GenericContainer<>(DockerImageName.parse("postgres:latest")) + .withExposedPorts(5432) + .withEnv("POSTGRES_USER", user) + .withEnv("POSTGRES_PASSWORD", pw) + .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*", 2)); + self.start(); + return self; + } +} diff --git a/sadu-mapper/src/test/java/de/chojo/sadu/mapper/annotation/MappingProviderTest.java b/sadu-mapper/src/test/java/de/chojo/sadu/mapper/annotation/MappingProviderTest.java new file mode 100644 index 00000000..da40f58f --- /dev/null +++ b/sadu-mapper/src/test/java/de/chojo/sadu/mapper/annotation/MappingProviderTest.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * Copyright (C) RainbowDashLabs and Contributor + */ + +package de.chojo.sadu.mapper.annotation; + +import de.chojo.sadu.datasource.DataSourceCreator; +import de.chojo.sadu.mapper.RowMapperRegistry; +import de.chojo.sadu.mapper.exceptions.InvalidMappingException; +import de.chojo.sadu.mapper.rowmapper.RowMapping; +import de.chojo.sadu.mapper.wrapper.Row; +import de.chojo.sadu.postgresql.databases.PostgreSql; +import de.chojo.sadu.postgresql.mapper.PostgresqlMapper; +import de.chojo.sadu.queries.api.configuration.QueryConfiguration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; + +import javax.sql.DataSource; +import java.io.IOException; +import java.sql.SQLException; + +import static de.chojo.sadu.mapper.PostgresDatabase.createContainer; +import static de.chojo.sadu.queries.api.query.Query.query; + +public class MappingProviderTest { + + + private static GenericContainer pg; + + @BeforeAll + static void beforeAll() throws IOException { + pg = createContainer("postgres", "postgres"); + DataSource dc = DataSourceCreator.create(PostgreSql.get()) + .configure(c -> c.host(pg.getHost()).port(pg.getFirstMappedPort())).create() + .usingPassword("postgres") + .usingUsername("postgres") + .build(); + QueryConfiguration.setDefault(QueryConfiguration.builder(dc) + .setRowMapperRegistry(new RowMapperRegistry().register(PostgresqlMapper.getDefaultMapper())) + .build()); + } + + @Test + public void testMethodMapperRegistration() { + MethodMappedClass methodMappedClass = query("Select 'test' as test") + .single() + .mapAs(MethodMappedClass.class) + .first() + .get(); + Assertions.assertEquals(methodMappedClass.test, "test"); + } + + @Test + public void testConstructorMapperRegistration() { + ConstructorMappedClass methodMappedClass = query("Select 'test' as test") + .single() + .mapAs(ConstructorMappedClass.class) + .first() + .get(); + Assertions.assertEquals(methodMappedClass.test, "test"); + } + + @Test + public void testInvalidRegistration() { + Assertions.assertThrows(InvalidMappingException.class, () -> { + query("Select 'test' as test") + .single() + .mapAs(InvalidClass.class) + .first(); + }); + } + + @AfterAll + static void afterAll() throws IOException { + pg.close(); + } + + public static class MethodMappedClass { + + String test; + + public MethodMappedClass(String test) { + this.test = test; + } + + @MappingProvider({"test"}) + public static RowMapping map() { + return row -> new MethodMappedClass(row.getString(1)); + } + } + + public static class ConstructorMappedClass { + + String test; + + public ConstructorMappedClass(String test) { + this.test = test; + } + + @MappingProvider({"test"}) + public ConstructorMappedClass(Row row) throws SQLException { + this.test = row.getString("test"); + } + + } + + public static class InvalidClass { + + String test; + + @MappingProvider({"test"}) + public InvalidClass(String test) { + this.test = test; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f84fd210..d20dc954 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,7 +49,7 @@ dependencyResolutionManagement { version("junit", "5.11.0") library("junit-jupiter", "org.junit.jupiter", "junit-jupiter").versionRef("junit") library("junit-params", "org.junit.jupiter", "junit-jupiter-params").versionRef("junit") - bundle("junit", listOf("junit-jupiter", "junit-params")) + bundle("junit", listOf("junit-jupiter", "junit-params", "slf4j-simple")) version("testcontainers", "1.20.1") library("testcontainers-postgres", "org.testcontainers", "postgresql").versionRef("testcontainers") @@ -60,6 +60,7 @@ dependencyResolutionManagement { version("slf4j", "2.0.16") library("slf4j-noop", "org.slf4j", "slf4j-nop").versionRef("slf4j") + library("slf4j-simple", "org.slf4j", "slf4j-simple").versionRef("slf4j") library("driver-postgres", "org.postgresql:postgresql:42.7.4") library("driver-mariadb", "org.mariadb.jdbc:mariadb-java-client:3.4.1") From 8e3f37ca9844721bff54783d71561e78741a12f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:11:44 +0200 Subject: [PATCH 3/3] Update dependency org.postgresql:postgresql to v42.7.4 (#214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- sadu-mapper/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sadu-mapper/build.gradle.kts b/sadu-mapper/build.gradle.kts index d6e0dd27..8ab3e0a3 100644 --- a/sadu-mapper/build.gradle.kts +++ b/sadu-mapper/build.gradle.kts @@ -3,7 +3,7 @@ description = "SADU module to map values received from a database to java object dependencies { api(project(":sadu-core")) - testImplementation("org.postgresql", "postgresql", "42.7.3") + testImplementation("org.postgresql", "postgresql", "42.7.4") testImplementation(testlibs.bundles.junit) testImplementation(project(":sadu-queries")) testImplementation(project(":sadu-datasource"))