From 2b8737d3007298f7bbcb016e5fb1d205e5d69a86 Mon Sep 17 00:00:00 2001 From: Steven Schlansker <stevenschlansker@gmail.com> Date: Fri, 4 Jan 2019 14:12:00 -0800 Subject: [PATCH] Further code review --- 4.0_TODO | 1 + .../core/argument/BeanPropertyArguments.java | 12 ++ .../internal/PojoPropertyArguments.java | 45 +----- .../mapper/immutables/ImmutablesPlugin.java | 144 ++++++++++++++++++ .../internal}/ImmutablesMapperFactory.java | 27 ++-- .../v3/core/mapper/reflect/BeanMapper.java | 20 +-- .../core/mapper/reflect/PropertiesMapper.java | 49 ------ .../mapper/reflect/internal/PojoMapper.java | 25 +++ .../internal/PojoPropertiesFactories.java | 50 ++++++ .../jdbi/v3/core/qualifier/Qualifiers.java | 4 +- .../jdbi/v3/core/result/ResultBearing.java | 1 + .../jdbi/v3/core/statement/SqlStatement.java | 8 +- .../jdbi/v3/core/mapper/ImmutablesTest.java | 24 +-- docs/src/adoc/index.adoc | 6 +- pom.xml | 10 +- postgres/pom.xml | 1 + .../v3/postgres/TestImmutablesHStore.java | 12 +- .../internal/RegisterBeanMapperImpl.java | 1 + .../{BindProperties.java => BindPojo.java} | 6 +- ...rtiesFactory.java => BindPojoFactory.java} | 10 +- .../jdbi/v3/sqlobject/TestBindProperties.java | 11 +- 21 files changed, 319 insertions(+), 148 deletions(-) create mode 100644 core/src/main/java/org/jdbi/v3/core/mapper/immutables/ImmutablesPlugin.java rename core/src/main/java/org/jdbi/v3/core/mapper/{reflect => immutables/internal}/ImmutablesMapperFactory.java (59%) delete mode 100644 core/src/main/java/org/jdbi/v3/core/mapper/reflect/PropertiesMapper.java create mode 100644 core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoMapper.java create mode 100644 core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoPropertiesFactories.java rename sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/{BindProperties.java => BindPojo.java} (86%) rename sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/{BindPropertiesFactory.java => BindPojoFactory.java} (84%) diff --git a/4.0_TODO b/4.0_TODO index 0dd080d902..3d1da39217 100644 --- a/4.0_TODO +++ b/4.0_TODO @@ -6,3 +6,4 @@ * Require @HStore annotation for HSTORE mapping / binding (remove legacy support for unqualified maps in Postgres plugin) * Throwables throwOnlyException only exists because of the above <X extends Exception> issue, so clean that up too +* BeanMapper should throw if it can't find a nested type rather than default to `rs.getObject` (strictColumnMapping) diff --git a/core/src/main/java/org/jdbi/v3/core/argument/BeanPropertyArguments.java b/core/src/main/java/org/jdbi/v3/core/argument/BeanPropertyArguments.java index 9167f12a96..ba6b70fece 100644 --- a/core/src/main/java/org/jdbi/v3/core/argument/BeanPropertyArguments.java +++ b/core/src/main/java/org/jdbi/v3/core/argument/BeanPropertyArguments.java @@ -24,7 +24,10 @@ /** * Inspect a {@link java.beans} style object and bind parameters * based on each of its discovered properties. + * + * @deprecated this should never have been public API */ +@Deprecated public class BeanPropertyArguments extends MethodReturnValueNamedArgumentFinder { private final PojoProperties<?> properties; @@ -37,6 +40,15 @@ public BeanPropertyArguments(String prefix, Object bean) { properties = BeanPropertiesFactory.propertiesFor(obj.getClass()); } + /** + * @param prefix an optional prefix (we insert a '.' as a separator) + * @param bean the bean to inspect and bind + */ + protected BeanPropertyArguments(String prefix, Object bean, PojoProperties<?> properties) { + super(prefix, bean); + this.properties = properties; + } + @Override protected Optional<TypedValue> getValue(String name, StatementContext ctx) { @SuppressWarnings("unchecked") diff --git a/core/src/main/java/org/jdbi/v3/core/argument/internal/PojoPropertyArguments.java b/core/src/main/java/org/jdbi/v3/core/argument/internal/PojoPropertyArguments.java index dabe817d87..24cfc826b2 100644 --- a/core/src/main/java/org/jdbi/v3/core/argument/internal/PojoPropertyArguments.java +++ b/core/src/main/java/org/jdbi/v3/core/argument/internal/PojoPropertyArguments.java @@ -13,57 +13,26 @@ */ package org.jdbi.v3.core.argument.internal; -import java.util.Map; -import java.util.Optional; - +import org.jdbi.v3.core.argument.BeanPropertyArguments; import org.jdbi.v3.core.argument.NamedArgumentFinder; -import org.jdbi.v3.core.mapper.RowMapper; -import org.jdbi.v3.core.mapper.reflect.BeanMapper; -import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties.PojoProperty; +import org.jdbi.v3.core.mapper.reflect.internal.PojoPropertiesFactories; import org.jdbi.v3.core.statement.StatementContext; -import org.jdbi.v3.core.statement.UnableToCreateStatementException; /** - * Inspect a object and bind parameters via {@link BeanMapper}'s properties. + * This class only exists to use the protected BeanPropertyArguments constructor. + * When we can remove that class from public API, this class will easily merge into it. */ -public class PojoPropertyArguments extends MethodReturnValueNamedArgumentFinder { - private final Map<String, ? extends PojoProperty<?>> properties; +@SuppressWarnings("deprecation") +public class PojoPropertyArguments extends BeanPropertyArguments { private final StatementContext ctx; - /** - * @param prefix an optional prefix (we insert a '.' as a separator) - * @param bean the bean to inspect and bind - * @param ctx the statement context - */ public PojoPropertyArguments(String prefix, Object bean, StatementContext ctx) { - super(prefix, bean); + super(prefix, bean, ctx.getConfig(PojoPropertiesFactories.class).propertiesOf(bean.getClass())); this.ctx = ctx; - final RowMapper<? extends Object> mapper = ctx.findRowMapperFor(bean.getClass()) - .orElseThrow(() -> new UnableToCreateStatementException("Couldn't find registered property mapper for " + bean.getClass())); - if (!(mapper instanceof BeanMapper<?>)) { - throw new UnableToCreateStatementException("Registered mapper for " + bean.getClass() + " is not a property based mapper"); - } - properties = ((BeanMapper<?>) mapper).getBeanInfo().getProperties(); - } - - @Override - protected Optional<TypedValue> getValue(String name, StatementContext ctx2) { - @SuppressWarnings("unchecked") - PojoProperty<Object> property = (PojoProperty<Object>) properties.get(name); - if (property == null) { - return Optional.empty(); - } - - return Optional.of(new TypedValue(property.getQualifiedType(), property.get(obj))); } @Override protected NamedArgumentFinder getNestedArgumentFinder(Object o) { return new PojoPropertyArguments(null, o, ctx); } - - @Override - public String toString() { - return "{lazy bean property arguments \"" + obj + "\"}"; - } } diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/immutables/ImmutablesPlugin.java b/core/src/main/java/org/jdbi/v3/core/mapper/immutables/ImmutablesPlugin.java new file mode 100644 index 0000000000..fd1784e553 --- /dev/null +++ b/core/src/main/java/org/jdbi/v3/core/mapper/immutables/ImmutablesPlugin.java @@ -0,0 +1,144 @@ +/* + * 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.core.mapper.immutables; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Type; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.internal.JdbiOptionals; +import org.jdbi.v3.core.internal.exceptions.Unchecked; +import org.jdbi.v3.core.mapper.immutables.internal.ImmutablesMapperFactory; +import org.jdbi.v3.core.mapper.reflect.internal.ImmutablesPropertiesFactory; +import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties; +import org.jdbi.v3.core.mapper.reflect.internal.PojoPropertiesFactories; +import org.jdbi.v3.core.spi.JdbiPlugin; +import org.jdbi.v3.meta.Beta; + +/** + * Install support for an Immutables generated {@code Immutable} or {@code Modifiable} value type. + * Note that unlike most plugins, this plugin is expected to be installed multiple times, once for each value type you wish to handle. + */ +@Beta +public class ImmutablesPlugin<S> implements JdbiPlugin { + private final Class<S> spec; + private final Class<? extends S> impl; + private final Function<Type, ? extends PojoProperties<S>> properties; + + private ImmutablesPlugin(Class<S> spec, Class<? extends S> impl, Function<Type, ? extends PojoProperties<S>> properties) { + this.spec = spec; + this.impl = impl; + this.properties = properties; + } + + /** + * Register bean arguments and row mapping for an {@code Immutable*} value class, expecting the default generated class and builder names. + * @param spec the specification interface or abstract class + * @param <S> the specification class + * @return a plugin that configures type mapping for the given class + */ + public static <S> ImmutablesPlugin<S> forImmutable(Class<S> spec) { + final Class<? extends S> impl = classByPrefix("Immutable", spec); + return forImmutable(spec, impl, JdbiOptionals.findFirstPresent( + () -> nullaryMethodOf(spec, "builder"), + () -> nullaryMethodOf(impl, "builder")) + .orElseThrow(() -> new IllegalArgumentException("Neither " + spec + " nor " + impl + " have a 'builder' method"))); + } + + /** + * Register bean arguments and row mapping for an {@code Immutable*} value class, using a supplied implementation and builder. + * @param spec the specification interface or abstract class + * @param impl the generated implementation class + * @param builder a supplier of new Builder instances + * @param <S> the specification class + * @param <I> the implementation class + * @return a plugin that configures type mapping for the given class + */ + public static <S, I extends S> ImmutablesPlugin<S> forImmutable(Class<S> spec, Class<I> impl, Supplier<?> builder) { + return new ImmutablesPlugin<S>(spec, impl, ImmutablesPropertiesFactory.immutable(spec, builder)); + } + + /** + * Register bean arguments and row mapping for an {@code Modifiable*} value class, expecting the default generated class and public nullary constructor. + * @param spec the specification interface or abstract class + * @param <S> the specification class + * @return a plugin that configures type mapping for the given class + */ + public static <S> ImmutablesPlugin<S> forModifiable(Class<S> spec) { + final Class<? extends S> impl = classByPrefix("Modifiable", spec); + return forModifiable(spec, impl, + nullaryMethodOf(impl, "create") + .orElseGet(() -> constructorOf(impl))); + } + + /** + * Register bean arguments and row mapping for an {@code Modifiable*} value class, using a supplied implementation and constructor. + * @param spec the specification interface or abstract class + * @param impl the modifiable class + * @param constructor a supplier of new Modifiable instances + * @param <S> the specification class + * @param <M> the modifiable class + * @return a plugin that configures type mapping for the given class + */ + public static <S, M extends S> ImmutablesPlugin<S> forModifiable(Class<S> spec, Class<M> impl, Supplier<?> constructor) { + return new ImmutablesPlugin<S>(spec, impl, ImmutablesPropertiesFactory.modifiable(spec, () -> impl.cast(constructor.get()))); + } + + private static Optional<Supplier<?>> nullaryMethodOf(Class<?> impl, String methodName) { + try { + return Optional.of(Unchecked.supplier(MethodHandles.lookup() + .unreflect(impl.getMethod(methodName))::invoke)); + } catch (ReflectiveOperationException e) { + return Optional.empty(); + } + } + + @SuppressWarnings("unchecked") + private static <S> Supplier<S> constructorOf(Class<S> impl) { + try { + return (Supplier<S>) Unchecked.supplier(MethodHandles.lookup().findConstructor(impl, MethodType.methodType(void.class))::invoke); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Couldn't find public constructor of " + impl, e); + } + } + + private static <S, Sub extends S> Class<? extends S> classByPrefix(String prefix, Class<S> spec) { + final String implName = spec.getPackage().getName() + '.' + prefix + spec.getSimpleName(); + try { + return Class.forName(implName).asSubclass(spec); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Couldn't locate default implementation class " + implName, e); + } + } + + @Override + public void customizeJdbi(Jdbi jdbi) { + jdbi.getConfig(PojoPropertiesFactories.class).register(new Factory()); + jdbi.registerRowMapper(new ImmutablesMapperFactory<S>(spec, impl, properties)); + } + + class Factory implements Function<Type, Optional<PojoProperties<?>>> { + @Override + public Optional<PojoProperties<?>> apply(Type t) { + if (t == spec || t == impl) { + return Optional.of(properties.apply(t)); + } + return Optional.empty(); + } + } +} diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/ImmutablesMapperFactory.java b/core/src/main/java/org/jdbi/v3/core/mapper/immutables/internal/ImmutablesMapperFactory.java similarity index 59% rename from core/src/main/java/org/jdbi/v3/core/mapper/reflect/ImmutablesMapperFactory.java rename to core/src/main/java/org/jdbi/v3/core/mapper/immutables/internal/ImmutablesMapperFactory.java index a1c9d6a3a5..4f663eb413 100644 --- a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/ImmutablesMapperFactory.java +++ b/core/src/main/java/org/jdbi/v3/core/mapper/immutables/internal/ImmutablesMapperFactory.java @@ -11,48 +11,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jdbi.v3.core.mapper.reflect; +package org.jdbi.v3.core.mapper.immutables.internal; import java.lang.reflect.Type; import java.util.Optional; import java.util.function.Function; -import java.util.function.Supplier; - import org.jdbi.v3.core.config.ConfigRegistry; import org.jdbi.v3.core.generic.GenericTypes; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.mapper.RowMapperFactory; -import org.jdbi.v3.core.mapper.reflect.internal.ImmutablesPropertiesFactory; +import org.jdbi.v3.core.mapper.reflect.internal.PojoMapper; import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties; -import org.jdbi.v3.meta.Beta; -@Beta +/** + * Row mapper that inspects an {@code immutables}-style Immutable or Modifiable value class for properties + * and binds them in the style of {@link org.jdbi.v3.core.mapper.reflect.BeanMapper}. + * @param <T> the mapped value type + */ public class ImmutablesMapperFactory<T> implements RowMapperFactory { private final Class<T> defn; private final Class<? extends T> impl; - private final Function<Type, PojoProperties<T>> properties; + private final Function<Type, ? extends PojoProperties<T>> properties; - private ImmutablesMapperFactory(Class<T> defn, Class<? extends T> impl, Function<Type, PojoProperties<T>> properties) { + public ImmutablesMapperFactory(Class<T> defn, Class<? extends T> impl, Function<Type, ? extends PojoProperties<T>> properties) { this.defn = defn; this.impl = impl; this.properties = properties; } - public static <T, I extends T, B> RowMapperFactory mapImmutable(Class<T> defn, Class<I> immutable, Supplier<B> builder) { - return new ImmutablesMapperFactory<>(defn, immutable, ImmutablesPropertiesFactory.immutable(defn, builder)); - } - - public static <T, M extends T> RowMapperFactory mapModifiable(Class<T> defn, Class<M> modifiable, Supplier<M> constructor) { - return new ImmutablesMapperFactory<>(defn, modifiable, ImmutablesPropertiesFactory.modifiable(defn, constructor)); - } - @SuppressWarnings("unchecked") @Override public Optional<RowMapper<?>> build(Type type, ConfigRegistry config) { Class<?> erasedType = GenericTypes.getErasedType(type); if (defn.equals(erasedType) || impl.equals(erasedType)) { - return Optional.of(PropertiesMapper.of((Class<T>) erasedType, properties.apply(type))); + return Optional.of(new PojoMapper<>((Class<T>) erasedType, properties.apply(type), "")); } return Optional.empty(); } diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/BeanMapper.java b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/BeanMapper.java index df0f295a4d..81c368cf07 100644 --- a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/BeanMapper.java +++ b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/BeanMapper.java @@ -24,6 +24,7 @@ import org.jdbi.v3.core.generic.GenericTypes; import org.jdbi.v3.core.mapper.ColumnMapper; import org.jdbi.v3.core.mapper.Nested; +import org.jdbi.v3.core.mapper.NoSuchMapperException; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.mapper.RowMapperFactory; import org.jdbi.v3.core.mapper.SingleColumnMapper; @@ -32,8 +33,6 @@ import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties.PojoBuilder; import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties.PojoProperty; import org.jdbi.v3.core.statement.StatementContext; -import org.jdbi.v3.meta.Beta; - import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.anyColumnsStartWithPrefix; import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.findColumnIndex; import static org.jdbi.v3.core.mapper.reflect.ReflectionMapperUtil.getColumnNames; @@ -46,7 +45,10 @@ * properties. * * The mapped class must have a default constructor. + * + * @deprecated this class should not be public API, use {@link org.jdbi.v3.core.statement.SqlStatement#bindBean(Object)} instead. */ +@Deprecated public class BeanMapper<T> implements RowMapper<T> { static final String DEFAULT_PREFIX = ""; @@ -100,6 +102,7 @@ public static <T> RowMapper<T> of(Class<T> type, String prefix) { return new BeanMapper<>(type, prefix); } + protected boolean strictColumnMapping; // this should be default (only?) behavior but that's a breaking change protected final Class<T> type; protected final String prefix; private final PojoProperties<T> properties; @@ -110,7 +113,7 @@ public static <T> RowMapper<T> of(Class<T> type, String prefix) { this(type, (PojoProperties<T>) BeanPropertiesFactory.propertiesFor(type), prefix); } - BeanMapper(Class<T> type, PojoProperties<T> properties, String prefix) { + protected BeanMapper(Class<T> type, PojoProperties<T> properties, String prefix) { this.type = type; this.properties = properties; this.prefix = prefix.toLowerCase(); @@ -198,15 +201,14 @@ private Optional<RowMapper<T>> specialize0(StatementContext ctx, }); } - ColumnMapper<?> defaultColumnMapper(PojoProperty<T> property) { + private ColumnMapper<?> defaultColumnMapper(PojoProperty<T> property) { + if (strictColumnMapping) { + throw new NoSuchMapperException(String.format( + "Couldn't find mapper for property '%s' of type '%s' from %s", property.getName(), property.getQualifiedType(), type)); + } return (r, n, c) -> r.getObject(n); } - @Beta - public PojoProperties<T> getBeanInfo() { - return properties; - } - private String getName(PojoProperty<T> property) { return property.getAnnotation(ColumnName.class) .map(ColumnName::value) diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/PropertiesMapper.java b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/PropertiesMapper.java deleted file mode 100644 index 222b0b7274..0000000000 --- a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/PropertiesMapper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.core.mapper.reflect; - -import org.jdbi.v3.core.mapper.ColumnMapper; -import org.jdbi.v3.core.mapper.NoSuchMapperException; -import org.jdbi.v3.core.mapper.RowMapper; -import org.jdbi.v3.core.mapper.RowMapperFactory; -import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties; -import org.jdbi.v3.core.mapper.reflect.internal.PojoProperties.PojoProperty; - -class PropertiesMapper<T> extends BeanMapper<T> { - private PropertiesMapper(Class<T> type, PojoProperties<T> properties, String prefix) { - super(type, properties, prefix); - } - - public static <T> RowMapperFactory factory(Class<T> type, PojoProperties<T> properties) { - return RowMapperFactory.of(type, PropertiesMapper.of(type, properties)); - } - - public static <T> RowMapperFactory factory(Class<T> type, PojoProperties<T> properties, String prefix) { - return RowMapperFactory.of(type, PropertiesMapper.of(type, properties, prefix)); - } - - public static <T> RowMapper<T> of(Class<T> type, PojoProperties<T> properties) { - return PropertiesMapper.of(type, properties, DEFAULT_PREFIX); - } - - public static <T> RowMapper<T> of(Class<T> type, PojoProperties<T> properties, String prefix) { - return new PropertiesMapper<>(type, properties, prefix); - } - - @Override - protected ColumnMapper<?> defaultColumnMapper(PojoProperty<T> property) { - throw new NoSuchMapperException(String.format( - "Couldn't find mapper for property '%s' of type '%s' from %s", property.getName(), property.getQualifiedType(), type)); - } -} diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoMapper.java b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoMapper.java new file mode 100644 index 0000000000..2a0c9a5590 --- /dev/null +++ b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoMapper.java @@ -0,0 +1,25 @@ +/* + * 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.core.mapper.reflect.internal; + +import org.jdbi.v3.core.mapper.reflect.BeanMapper; + +/** This class only exists to extend BeanMapper with API that would otherwise be unavoidably public. */ +@SuppressWarnings("deprecation") +public class PojoMapper<T> extends BeanMapper<T> { + public PojoMapper(Class<T> type, PojoProperties<T> properties, String prefix) { + super(type, properties, prefix); + strictColumnMapping = true; + } +} diff --git a/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoPropertiesFactories.java b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoPropertiesFactories.java new file mode 100644 index 0000000000..f5ff037400 --- /dev/null +++ b/core/src/main/java/org/jdbi/v3/core/mapper/reflect/internal/PojoPropertiesFactories.java @@ -0,0 +1,50 @@ +/* + * 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.core.mapper.reflect.internal; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.jdbi.v3.core.config.JdbiConfig; +import org.jdbi.v3.core.internal.JdbiOptionals; + +public class PojoPropertiesFactories implements JdbiConfig<PojoPropertiesFactories> { + private final List<Function<Type, Optional<PojoProperties<?>>>> factories = new ArrayList<>(); + + public PojoPropertiesFactories() {} + + private PojoPropertiesFactories(PojoPropertiesFactories other) { + factories.addAll(other.factories); + } + + public PojoPropertiesFactories register(Function<Type, Optional<PojoProperties<?>>> factory) { + factories.add(factory); + return this; + } + + public PojoProperties<?> propertiesOf(Type type) { + return factories.stream() + .flatMap(ppf -> JdbiOptionals.stream(ppf.apply(type))) + .findFirst() + .orElseGet(() -> BeanPropertiesFactory.propertiesFor(type)); + } + + @Override + public PojoPropertiesFactories createCopy() { + return new PojoPropertiesFactories(this); + } +} diff --git a/core/src/main/java/org/jdbi/v3/core/qualifier/Qualifiers.java b/core/src/main/java/org/jdbi/v3/core/qualifier/Qualifiers.java index 6de5f9b521..7b57f9b8e3 100644 --- a/core/src/main/java/org/jdbi/v3/core/qualifier/Qualifiers.java +++ b/core/src/main/java/org/jdbi/v3/core/qualifier/Qualifiers.java @@ -13,6 +13,8 @@ */ package org.jdbi.v3.core.qualifier; +import static java.util.stream.Collectors.toSet; + import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.util.Arrays; @@ -21,8 +23,6 @@ import org.jdbi.v3.meta.Beta; -import static java.util.stream.Collectors.toSet; - /** * Utility class for type qualifiers supported by Jdbi core. */ diff --git a/core/src/main/java/org/jdbi/v3/core/result/ResultBearing.java b/core/src/main/java/org/jdbi/v3/core/result/ResultBearing.java index 69ff493a86..29ba654e72 100644 --- a/core/src/main/java/org/jdbi/v3/core/result/ResultBearing.java +++ b/core/src/main/java/org/jdbi/v3/core/result/ResultBearing.java @@ -179,6 +179,7 @@ default ResultIterable<?> mapTo(QualifiedType type) { * @param <T> the bean type to map the result set rows to * @return a {@link ResultIterable} of the given type. */ + @SuppressWarnings("deprecation") default <T> ResultIterable<T> mapToBean(Class<T> type) { return map(BeanMapper.of(type)); } diff --git a/core/src/main/java/org/jdbi/v3/core/statement/SqlStatement.java b/core/src/main/java/org/jdbi/v3/core/statement/SqlStatement.java index c5cecb2def..48d1863bdb 100644 --- a/core/src/main/java/org/jdbi/v3/core/statement/SqlStatement.java +++ b/core/src/main/java/org/jdbi/v3/core/statement/SqlStatement.java @@ -166,6 +166,7 @@ public This bind(String name, Argument argument) { * * @return modified statement */ + @SuppressWarnings("deprecation") public This bindBean(Object bean) { return bindNamedArgumentFinder(new BeanPropertyArguments(null, bean)); } @@ -180,6 +181,7 @@ public This bindBean(Object bean) { * * @return modified statement */ + @SuppressWarnings("deprecation") public This bindBean(String prefix, Object bean) { return bindNamedArgumentFinder(new BeanPropertyArguments(prefix, bean)); } @@ -192,8 +194,8 @@ public This bindBean(String prefix, Object bean) { * @return modified statement */ @Beta - public This bindProperties(Object pojo) { - return bindNamedArgumentFinder(new PojoPropertyArguments(null, pojo, getContext())); + public This bindPojo(Object pojo) { + return bindPojo(null, pojo); } /** @@ -207,7 +209,7 @@ public This bindProperties(Object pojo) { * @return modified statement */ @Beta - public This bindProperties(String prefix, Object pojo) { + public This bindPojo(String prefix, Object pojo) { return bindNamedArgumentFinder(new PojoPropertyArguments(prefix, pojo, getContext())); } diff --git a/core/src/test/java/org/jdbi/v3/core/mapper/ImmutablesTest.java b/core/src/test/java/org/jdbi/v3/core/mapper/ImmutablesTest.java index ac48ed78e8..73a923d488 100644 --- a/core/src/test/java/org/jdbi/v3/core/mapper/ImmutablesTest.java +++ b/core/src/test/java/org/jdbi/v3/core/mapper/ImmutablesTest.java @@ -19,8 +19,9 @@ import org.immutables.value.Value; import org.jdbi.v3.core.Handle; +import org.jdbi.v3.core.Jdbi; import org.jdbi.v3.core.generic.GenericType; -import org.jdbi.v3.core.mapper.reflect.ImmutablesMapperFactory; +import org.jdbi.v3.core.mapper.immutables.ImmutablesPlugin; import org.jdbi.v3.core.rule.H2DatabaseRule; import org.junit.Before; import org.junit.Rule; @@ -30,19 +31,24 @@ public class ImmutablesTest { @Rule - public H2DatabaseRule dbRule = new H2DatabaseRule(); + public H2DatabaseRule dbRule = new H2DatabaseRule() + .withPlugin(ImmutablesPlugin.forImmutable(SubValue.class)) + .withPlugin(ImmutablesPlugin.forImmutable(FooBarBaz.class)) + .withPlugin(ImmutablesPlugin.forModifiable(FooBarBaz.class)); + private Jdbi jdbi; private Handle h; @Before public void setup() { + jdbi = dbRule.getJdbi(); h = dbRule.getSharedHandle(); h.execute("create table immutables (t int, x varchar)"); - - h.registerRowMapper(ImmutablesMapperFactory.mapImmutable(SubValue.class, ImmutableSubValue.class, ImmutableSubValue::builder)); } // tag::example[] + // First, install the plugin: ; + @Value.Immutable public interface Train { String name(); @@ -52,12 +58,12 @@ public interface Train { @Test public void simpleTest() { + jdbi.installPlugin(ImmutablesPlugin.forImmutable(Train.class)); h.execute("create table train (name varchar, carriages int, observation_car boolean)"); - h.registerRowMapper(ImmutablesMapperFactory.mapImmutable(Train.class, ImmutableTrain.class, ImmutableTrain::builder)); assertThat( h.createUpdate("insert into train(name, carriages, observation_car) values (:name, :carriages, :observationCar)") - .bindProperties(ImmutableTrain.builder().name("Zephyr").carriages(8).observationCar(true).build()) + .bindPojo(ImmutableTrain.builder().name("Zephyr").carriages(8).observationCar(true).build()) .execute()) .isEqualTo(1); @@ -74,7 +80,7 @@ public void simpleTest() { public void parameterizedTest() { assertThat( h.createUpdate("insert into immutables(t, x) values (:t, :x)") - .bindProperties(ImmutableSubValue.<String, Integer>builder().t(42).x("foo").build()) + .bindPojo(ImmutableSubValue.<String, Integer>builder().t(42).x("foo").build()) .execute()) .isEqualTo(1); @@ -106,12 +112,10 @@ public interface FooBarBaz { @Test public void testModifiable() { - h.registerRowMapper(ImmutablesMapperFactory.mapImmutable(FooBarBaz.class, ImmutableFooBarBaz.class, ImmutableFooBarBaz::builder)); - h.registerRowMapper(ImmutablesMapperFactory.mapModifiable(FooBarBaz.class, ModifiableFooBarBaz.class, ModifiableFooBarBaz::create)); h.execute("create table fbb (id serial, foo varchar, bar int, baz real)"); assertThat(h.createUpdate("insert into fbb (id, foo, bar, baz) values (:id, :foo, :bar, :baz)") - .bindProperties(ModifiableFooBarBaz.create().setFoo("foo").setBar(42).setBaz(1.0)) + .bindPojo(ModifiableFooBarBaz.create().setFoo("foo").setBar(42).setBaz(1.0)) .execute()) .isEqualTo(1); diff --git a/docs/src/adoc/index.adoc b/docs/src/adoc/index.adoc index a5fce44cc7..74d177c0f3 100644 --- a/docs/src/adoc/index.adoc +++ b/docs/src/adoc/index.adoc @@ -3800,7 +3800,11 @@ to `Jdbi` properties binding and row mapping. [WARNING] Immutables support is still experimental and does not yet support custom naming schemes. -Just tell us about your types, and we do the rest: +Just tell us about your types by installing the plugin (possibly multiple times): + +`jdbi.installPlugin(ImmutablesPlugin.forImmutable(MyValueType.class))` + +and we do the rest: [source,java,indent=0] ---- diff --git a/pom.xml b/pom.xml index 153385ed69..f9db44e7d1 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,7 @@ <dep.antlr.version>3.4</dep.antlr.version> <dep.checkstyle.version>8.10</dep.checkstyle.version> <dep.dokka.version>0.9.17</dep.dokka.version> + <dep.immutables.version>2.7.1</dep.immutables.version> <dep.jackson2.version>2.9.8</dep.jackson2.version> <dep.jetbrainsAnnotations.version>13.0</dep.jetbrainsAnnotations.version> <dep.kotlin.version>1.2.31</dep.kotlin.version> @@ -242,7 +243,7 @@ <dependency> <groupId>org.immutables</groupId> <artifactId>value</artifactId> - <version>2.7.1</version> + <version>${dep.immutables.version}</version> </dependency> <dependency> @@ -506,6 +507,13 @@ <configuration> <source>${project.build.targetJdk}</source> <target>${project.build.targetJdk}</target> + <annotationProcessorPaths> + <annotationProcessorPath> + <groupId>org.immutables</groupId> + <artifactId>value</artifactId> + <version>${dep.immutables.version}</version> + </annotationProcessorPath> + </annotationProcessorPaths> </configuration> </plugin> <plugin> diff --git a/postgres/pom.xml b/postgres/pom.xml index 63d40d17cb..78f9f31bbc 100644 --- a/postgres/pom.xml +++ b/postgres/pom.xml @@ -60,6 +60,7 @@ <artifactId>value</artifactId> <scope>test</scope> </dependency> + <dependency> <groupId>org.jdbi</groupId> <artifactId>jdbi3-core</artifactId> diff --git a/postgres/src/test/java/org/jdbi/v3/postgres/TestImmutablesHStore.java b/postgres/src/test/java/org/jdbi/v3/postgres/TestImmutablesHStore.java index a11a443205..cbace59abd 100644 --- a/postgres/src/test/java/org/jdbi/v3/postgres/TestImmutablesHStore.java +++ b/postgres/src/test/java/org/jdbi/v3/postgres/TestImmutablesHStore.java @@ -18,10 +18,10 @@ import org.immutables.value.Value; import org.jdbi.v3.core.Handle; -import org.jdbi.v3.core.mapper.reflect.ImmutablesMapperFactory; +import org.jdbi.v3.core.mapper.immutables.ImmutablesPlugin; import org.jdbi.v3.core.rule.PgDatabaseRule; import org.jdbi.v3.sqlobject.SqlObjectPlugin; -import org.jdbi.v3.sqlobject.customizer.BindProperties; +import org.jdbi.v3.sqlobject.customizer.BindPojo; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.junit.After; @@ -34,14 +34,16 @@ public class TestImmutablesHStore { @Rule - public PgDatabaseRule dbRule = new PgDatabaseRule().withPlugin(new PostgresPlugin()).withPlugin(new SqlObjectPlugin()); + public PgDatabaseRule dbRule = new PgDatabaseRule() + .withPlugin(new PostgresPlugin()) + .withPlugin(new SqlObjectPlugin()) + .withPlugin(ImmutablesPlugin.forImmutable(Mappy.class)); private Handle h; @Before public void setup() { h = dbRule.openHandle(); - h.registerRowMapper(ImmutablesMapperFactory.mapImmutable(Mappy.class, ImmutableMappy.class, Mappy::builder)); h.execute("create extension if not exists \"hstore\""); h.execute("create table mappy (numbers hstore not null)"); } @@ -87,7 +89,7 @@ public void testMap() { public interface MappyDao { @SqlUpdate("insert into mappy (numbers) values (:numbers)") - int insert(@BindProperties Mappy mappy); + int insert(@BindPojo Mappy mappy); @SqlQuery("select numbers from mappy") List<Mappy> select(); diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterBeanMapperImpl.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterBeanMapperImpl.java index 78507bc75b..fe662f42b3 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterBeanMapperImpl.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/config/internal/RegisterBeanMapperImpl.java @@ -24,6 +24,7 @@ public class RegisterBeanMapperImpl implements Configurer { @Override + @SuppressWarnings("deprecation") public void configureForType(ConfigRegistry registry, Annotation annotation, Class<?> sqlObjectType) { RegisterBeanMapper registerBeanMapper = (RegisterBeanMapper) annotation; Class<?> beanClass = registerBeanMapper.value(); diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindProperties.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindPojo.java similarity index 86% rename from sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindProperties.java rename to sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindPojo.java index 02b222cf81..e3913d99b5 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindProperties.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/BindPojo.java @@ -18,15 +18,15 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.jdbi.v3.sqlobject.customizer.internal.BindPropertiesFactory; +import org.jdbi.v3.sqlobject.customizer.internal.BindPojoFactory; /** * Binds the properties of an object to a SQL statement. */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) -@SqlStatementCustomizingAnnotation(BindPropertiesFactory.class) -public @interface BindProperties { +@SqlStatementCustomizingAnnotation(BindPojoFactory.class) +public @interface BindPojo { /** * Prefix to apply to each property. If specified, properties will be bound as * {@code prefix.propertyName}. diff --git a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPropertiesFactory.java b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPojoFactory.java similarity index 84% rename from sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPropertiesFactory.java rename to sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPojoFactory.java index 3b85f73708..af837ce305 100644 --- a/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPropertiesFactory.java +++ b/sqlobject/src/main/java/org/jdbi/v3/sqlobject/customizer/internal/BindPojoFactory.java @@ -18,11 +18,11 @@ import java.lang.reflect.Parameter; import java.lang.reflect.Type; -import org.jdbi.v3.sqlobject.customizer.BindProperties; +import org.jdbi.v3.sqlobject.customizer.BindPojo; import org.jdbi.v3.sqlobject.customizer.SqlStatementCustomizerFactory; import org.jdbi.v3.sqlobject.customizer.SqlStatementParameterCustomizer; -public class BindPropertiesFactory implements SqlStatementCustomizerFactory { +public class BindPojoFactory implements SqlStatementCustomizerFactory { @Override public SqlStatementParameterCustomizer createForParameter(Annotation annotation, Class<?> sqlObjectType, @@ -30,13 +30,13 @@ public SqlStatementParameterCustomizer createForParameter(Annotation annotation, Parameter param, int index, Type type) { - BindProperties bind = (BindProperties) annotation; + BindPojo bind = (BindPojo) annotation; return (stmt, bean) -> { String prefix = bind.value(); if (prefix.isEmpty()) { - stmt.bindProperties(bean); + stmt.bindPojo(bean); } else { - stmt.bindProperties(prefix, bean); + stmt.bindPojo(prefix, bean); } }; } diff --git a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestBindProperties.java b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestBindProperties.java index 3e92b558a9..7c830edd0a 100644 --- a/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestBindProperties.java +++ b/sqlobject/src/test/java/org/jdbi/v3/sqlobject/TestBindProperties.java @@ -18,9 +18,9 @@ import org.jdbi.v3.core.Handle; import org.jdbi.v3.core.mapper.ImmutableTrain; import org.jdbi.v3.core.mapper.ImmutablesTest.Train; -import org.jdbi.v3.core.mapper.reflect.ImmutablesMapperFactory; +import org.jdbi.v3.core.mapper.immutables.ImmutablesPlugin; import org.jdbi.v3.core.rule.H2DatabaseRule; -import org.jdbi.v3.sqlobject.customizer.BindProperties; +import org.jdbi.v3.sqlobject.customizer.BindPojo; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.junit.Before; @@ -31,7 +31,9 @@ public class TestBindProperties { @Rule - public H2DatabaseRule dbRule = new H2DatabaseRule().withPlugin(new SqlObjectPlugin()); + public H2DatabaseRule dbRule = new H2DatabaseRule() + .withPlugin(new SqlObjectPlugin()) + .withPlugin(ImmutablesPlugin.forImmutable(Train.class)); private Handle h; @@ -40,7 +42,6 @@ public class TestBindProperties { @Before public void setUp() { h = dbRule.getSharedHandle(); - h.registerRowMapper(ImmutablesMapperFactory.mapImmutable(Train.class, ImmutableTrain.class, ImmutableTrain::builder)); h.execute("create table train (name varchar, carriages int, observation_car boolean)"); dao = h.attach(Dao.class); @@ -63,7 +64,7 @@ public void testBindBean() { public interface Dao { @SqlUpdate("insert into train(name, carriages, observation_car) values (:name, :carriages, :observationCar)") - int insert(@BindProperties Train train); + int insert(@BindPojo Train train); @SqlQuery("select * from train") List<Train> getTrains();