From 265844a1c5fd263b76d7eb3448f7ffc1aa138ca9 Mon Sep 17 00:00:00 2001 From: TheRealRyGuy Date: Sat, 13 May 2023 13:23:19 -0400 Subject: [PATCH 1/4] impl annotation based collector registry --- .../sqlobject/config/RegisterCollector.java | 27 +++++++++++++++++++ .../internal/RegisterCollectorImpl.java | 24 +++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java create mode 100644 sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java new file mode 100644 index 0000000000..4ce4bef9af --- /dev/null +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java @@ -0,0 +1,27 @@ +package org.jdbi.v3.sqlobject.config; + +import org.jdbi.v3.core.extension.annotation.UseExtensionConfigurer; +import org.jdbi.v3.sqlobject.config.internal.RegisterCollectorImpl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.stream.Collector; + +/** + * Registers specifically one collector for a sql object type + */ +@UseExtensionConfigurer(RegisterCollectorImpl.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface RegisterCollector { + + /** + * The collector instance to register + * + * @return the collector instance + */ + Class> value(); + +} diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java new file mode 100644 index 0000000000..29582af159 --- /dev/null +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java @@ -0,0 +1,24 @@ +package org.jdbi.v3.sqlobject.config.internal; + +import org.jdbi.v3.core.collector.JdbiCollectors; +import org.jdbi.v3.core.config.ConfigRegistry; +import org.jdbi.v3.core.extension.SimpleExtensionConfigurer; +import org.jdbi.v3.sqlobject.config.RegisterCollector; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; + +public class RegisterCollectorImpl extends SimpleExtensionConfigurer { + @Override + public void configure(ConfigRegistry config, Annotation annotation, Class extensionType) { + RegisterCollector registerCollector = (RegisterCollector) annotation; + JdbiCollectors mappers = config.get(JdbiCollectors.class); + try { + Class type = (Class) ((ParameterizedType) getClass() + .getGenericSuperclass()).getActualTypeArguments()[2]; //gets resultant type of collector + mappers.registerCollector(type.getGenericSuperclass(), registerCollector.value().getConstructor().newInstance()); + } catch (ReflectiveOperationException | SecurityException e) { + throw new IllegalStateException("Unable to instantiate collector class " + registerCollector.value(), e); + } + } +} From c30b9902b34ce134e1a76e375f0ac3832638fac8 Mon Sep 17 00:00:00 2001 From: TheRealRyGuy Date: Mon, 15 May 2023 12:40:26 -0400 Subject: [PATCH 2/4] fix resultant type --- .../config/internal/RegisterCollectorImpl.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java index 29582af159..e366ff01f3 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java @@ -7,16 +7,26 @@ import java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; public class RegisterCollectorImpl extends SimpleExtensionConfigurer { @Override public void configure(ConfigRegistry config, Annotation annotation, Class extensionType) { RegisterCollector registerCollector = (RegisterCollector) annotation; - JdbiCollectors mappers = config.get(JdbiCollectors.class); + JdbiCollectors collectors = config.get(JdbiCollectors.class); + try { - Class type = (Class) ((ParameterizedType) getClass() - .getGenericSuperclass()).getActualTypeArguments()[2]; //gets resultant type of collector - mappers.registerCollector(type.getGenericSuperclass(), registerCollector.value().getConstructor().newInstance()); + Type type = null; //resultant type + for(Type t : registerCollector.value().getGenericInterfaces()) { + if(t instanceof ParameterizedType pt) { + if(pt.getRawType().toString().equals("interface Collector")) { + type = pt.getActualTypeArguments()[2]; + break; + } + } + } + if(type == null) throw new IllegalArgumentException("Tried to pass non-collector object to @RegisterCollector"); + collectors.registerCollector(type, registerCollector.value().getConstructor().newInstance()); } catch (ReflectiveOperationException | SecurityException e) { throw new IllegalStateException("Unable to instantiate collector class " + registerCollector.value(), e); } From 860704dcaca4bdf82526054820783bb44dd567c6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 16 May 2023 12:50:42 -0400 Subject: [PATCH 3/4] don't use pattern matching to fix j8 compile --- .../v3/sqlobject/config/internal/RegisterCollectorImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java index e366ff01f3..a85ebcb1bc 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java @@ -18,7 +18,8 @@ public void configure(ConfigRegistry config, Annotation annotation, Class ext try { Type type = null; //resultant type for(Type t : registerCollector.value().getGenericInterfaces()) { - if(t instanceof ParameterizedType pt) { + if(t instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) t; if(pt.getRawType().toString().equals("interface Collector")) { type = pt.getActualTypeArguments()[2]; break; From c54e62ea4ecd455faa03c6da1732e1e3d797ca85 Mon Sep 17 00:00:00 2001 From: Steven Schlansker Date: Thu, 8 Jun 2023 19:57:02 -0700 Subject: [PATCH 4/4] RegisterCollector: add test --- RELEASE_NOTES.md | 1 + .../collector/SimpleCollectorFactory.java | 14 ++- docs/src/adoc/index.adoc | 7 ++ .../sqlobject/config/RegisterCollector.java | 19 +++- .../internal/RegisterCollectorImpl.java | 46 ++++++---- .../config/RegisterCollectorTest.java | 91 +++++++++++++++++++ 6 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 sqlobject/src/test/java/org/jdbi/v3/sqlobject/config/RegisterCollectorTest.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f8ce2a67e3..1f085e522e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,7 @@ - document vavr incompatibility between 0.10.x and 1.0.0-alpha (#2350) - Handle.inTransaction: improve exception thrown when restoring transaction isolation #2343 - add support for Guice 6.x (using javax.inject annotations) and guice 7.x (using jakarta.inject annotations) + - add new @RegisterCollector customizing annotation (#2377) # 3.38.2 - spring5 JdbiUtil: fix thread safety #2341 diff --git a/core/src/main/java/org/jdbi/v3/core/collector/SimpleCollectorFactory.java b/core/src/main/java/org/jdbi/v3/core/collector/SimpleCollectorFactory.java index d848c6aed5..34d85ab520 100644 --- a/core/src/main/java/org/jdbi/v3/core/collector/SimpleCollectorFactory.java +++ b/core/src/main/java/org/jdbi/v3/core/collector/SimpleCollectorFactory.java @@ -20,21 +20,25 @@ import org.jdbi.v3.core.generic.GenericTypes; class SimpleCollectorFactory implements CollectorFactory { - private final Type containerType; + private final Type resultType; private final Collector collector; - SimpleCollectorFactory(Type containerType, Collector collector) { - this.containerType = containerType; + SimpleCollectorFactory(Type resultType, Collector collector) { + this.resultType = resultType; this.collector = collector; } @Override public boolean accepts(Type containerType) { - return GenericTypes.isSuperType(containerType, this.containerType); + return GenericTypes.isSuperType(containerType, resultType); } @Override public Optional elementType(Type containerType) { + Optional collectedType = GenericTypes.findGenericParameter(collector.getClass(), Collector.class); + if (collectedType.isPresent()) { + return collectedType; + } if (GenericTypes.isSuperType(Iterable.class, containerType)) { return GenericTypes.findGenericParameter(containerType, Iterable.class); } @@ -48,6 +52,6 @@ public Optional elementType(Type containerType) { @Override public String toString() { - return "CollectorFactory handling " + containerType + " with " + collector; + return "CollectorFactory handling " + resultType + " with " + collector; } } diff --git a/docs/src/adoc/index.adoc b/docs/src/adoc/index.adoc index c5d3709a4c..6de03aab6a 100644 --- a/docs/src/adoc/index.adoc +++ b/docs/src/adoc/index.adoc @@ -4636,7 +4636,14 @@ the `User.id` and `User.name` fields. Likewise for `r_id` and `r_name` into the The link:{jdbidocs}/sqlobject/config/RegisterConstructorMapper.html[@RegisterConstructorMapper^] annotation may be repeated multiple times on the same type or method to register multiple constructor mappers. +==== @RegisterCollectorFactory +Convenience annotation to register a `CollectorFactory` for this SqlObject method. See <> for more details. + +==== @RegisterCollectorFactory + +Convenience annotation to register a `Collector` for this SqlObject method. The element and result types are inferred from +the concrete `Collector` implementation's type parameters. See <> for more details. === Other SQL Object annotations diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java index 4ce4bef9af..0c15f22b01 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/RegisterCollector.java @@ -1,14 +1,27 @@ +/* + * 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 org.jdbi.v3.sqlobject.config; -import org.jdbi.v3.core.extension.annotation.UseExtensionConfigurer; -import org.jdbi.v3.sqlobject.config.internal.RegisterCollectorImpl; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.stream.Collector; +import org.jdbi.v3.core.extension.annotation.UseExtensionConfigurer; +import org.jdbi.v3.sqlobject.config.internal.RegisterCollectorImpl; + /** * Registers specifically one collector for a sql object type */ diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java index a85ebcb1bc..f186636dd4 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterCollectorImpl.java @@ -1,35 +1,41 @@ +/* + * 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 org.jdbi.v3.sqlobject.config.internal; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.stream.Collector; + import org.jdbi.v3.core.collector.JdbiCollectors; import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.core.extension.SimpleExtensionConfigurer; +import org.jdbi.v3.core.generic.GenericTypes; +import org.jdbi.v3.core.statement.UnableToCreateStatementException; import org.jdbi.v3.sqlobject.config.RegisterCollector; -import java.lang.annotation.Annotation; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - public class RegisterCollectorImpl extends SimpleExtensionConfigurer { @Override - public void configure(ConfigRegistry config, Annotation annotation, Class extensionType) { - RegisterCollector registerCollector = (RegisterCollector) annotation; - JdbiCollectors collectors = config.get(JdbiCollectors.class); + public void configure(final ConfigRegistry config, final Annotation annotation, final Class extensionType) { + final RegisterCollector registerCollector = (RegisterCollector) annotation; + final JdbiCollectors collectors = config.get(JdbiCollectors.class); try { - Type type = null; //resultant type - for(Type t : registerCollector.value().getGenericInterfaces()) { - if(t instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) t; - if(pt.getRawType().toString().equals("interface Collector")) { - type = pt.getActualTypeArguments()[2]; - break; - } - } - } - if(type == null) throw new IllegalArgumentException("Tried to pass non-collector object to @RegisterCollector"); - collectors.registerCollector(type, registerCollector.value().getConstructor().newInstance()); + final Type resultType = GenericTypes.findGenericParameter(registerCollector.value(), Collector.class, 2) + .orElseThrow(() -> new IllegalArgumentException("Tried to pass non-collector object to @RegisterCollector")); + collectors.registerCollector(resultType, registerCollector.value().getConstructor().newInstance()); } catch (ReflectiveOperationException | SecurityException e) { - throw new IllegalStateException("Unable to instantiate collector class " + registerCollector.value(), e); + throw new UnableToCreateStatementException("Unable to instantiate collector class " + registerCollector.value(), e); } } } diff --git a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/config/RegisterCollectorTest.java b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/config/RegisterCollectorTest.java new file mode 100644 index 0000000000..c18ae543ef --- /dev/null +++ b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/config/RegisterCollectorTest.java @@ -0,0 +1,91 @@ +/* + * 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 org.jdbi.v3.sqlobject.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.junit5.H2DatabaseExtension; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RegisterCollectorTest { + + @RegisterExtension + H2DatabaseExtension h2 = H2DatabaseExtension.withPlugins(); + Handle h; + + @BeforeEach + void setup() { + h = h2.getSharedHandle(); + h.execute("create table i (i int)"); + h.execute("insert into i values(1)"); + h.execute("insert into i values(2)"); + } + + @Test + void registerCollector() { + assertThat(h.attach(RegisterCollectorDao.class).select()) + .isEqualTo("1 2"); + } + + public interface RegisterCollectorDao { + @RegisterCollector(StringConcatCollector.class) + @SqlQuery("select i from i order by i asc") + String select(); + } + + public static class StringConcatCollector implements Collector, String> { + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public BiConsumer, Integer> accumulator() { + return (l, i) -> l.add(i); + } + + @Override + public BinaryOperator> combiner() { + return (a, b) -> { + a.addAll(b); + return a; + }; + } + + @Override + public Function, String> finisher() { + return i -> i.stream().map(Object::toString).collect(Collectors.joining(" ")); + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } + } +}