diff --git a/CHANGELOG.md b/CHANGELOG.md index d52d1e0d..14d392fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ([#19](https://github.com/EvidentSolutions/dalesbred/issues/19)) - Support binding arrays on Oracle, which does not support standard JDBC API. - Add dialect for H2. ([#20](https://github.com/EvidentSolutions/dalesbred/issues/20)) + - Support property paths when binding (instead of just simple names). ([#7](https://github.com/EvidentSolutions/dalesbred/issues/7)) ## 1.0.0 (2015-05-29) diff --git a/dalesbred/src/main/java/org/dalesbred/internal/instantiation/PropertyAccessor.java b/dalesbred/src/main/java/org/dalesbred/internal/instantiation/PropertyAccessor.java index 59abb906..cb9060b7 100644 --- a/dalesbred/src/main/java/org/dalesbred/internal/instantiation/PropertyAccessor.java +++ b/dalesbred/src/main/java/org/dalesbred/internal/instantiation/PropertyAccessor.java @@ -25,11 +25,14 @@ import org.dalesbred.annotation.DalesbredIgnore; import org.dalesbred.internal.utils.Throwables; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Optional; +import java.util.regex.Pattern; import static java.lang.reflect.Modifier.isPublic; import static org.dalesbred.internal.utils.StringUtils.isEqualIgnoringCaseAndUnderscores; @@ -41,13 +44,48 @@ abstract class PropertyAccessor { abstract Type getType(); @NotNull - static Optional findAccessor(@NotNull Class cl, @NotNull String name) { - Optional setter = findSetter(cl, name).map(SetterPropertyAccessor::new); + private static final Pattern PERIOD = Pattern.compile("\\."); + + @NotNull + static Optional findAccessor(@NotNull Class cl, @NotNull String path) { + String[] segments = PERIOD.split(path, -1); + + assert segments.length != 0; // split will always return non-empty array + + if (segments.length == 1) + return findFinalAccessor(cl, segments[0]); + + Class currentClass = cl; + + PropertyReader[] readers = new PropertyReader[segments.length - 1]; + for (int i = 0; i < segments.length - 1; i++) { + Field field = findField(currentClass, segments[i]).orElse(null); + if (field != null) { + readers[i] = field::get; + currentClass = field.getType(); + } else { + Method getter = findGetter(currentClass, segments[i]).orElse(null); + if (getter != null) { + readers[i] = getter::invoke; + currentClass = getter.getReturnType(); + } else { + return Optional.empty(); + } + } + } + + Optional accessor = findFinalAccessor(currentClass, segments[segments.length - 1]); + return accessor.map(a -> new NestedPathAccessor(readers, path, a)); + } + + @NotNull + private static Optional findFinalAccessor(@NotNull Class currentClass, @NotNull String name) { + Optional setter = findSetter(currentClass, name).map(SetterPropertyAccessor::new); if (setter.isPresent()) { return setter; } else { - return findField(cl, name).map(FieldPropertyAccessor::new); + return findField(currentClass, name).map(FieldPropertyAccessor::new); } } @@ -67,14 +105,27 @@ private static Optional findField(@NotNull Class cl, @NotNull String n @NotNull private static Optional findSetter(@NotNull Class cl, @NotNull String name) { + return findGetterOrSetter(cl, name, false); + } + + @NotNull + private static Optional findGetter(@NotNull Class cl, @NotNull String name) { + return findGetterOrSetter(cl, name, true); + } + + @NotNull + private static Optional findGetterOrSetter(@NotNull Class cl, @NotNull String propertyName, boolean getter) { + String methodName = (getter ? "get" : "set") + propertyName; + int parameterCount = getter ? 0 : 1; Method result = null; - String methodName = "set" + name; for (Method method : cl.getMethods()) { - - if (isPublic(method.getModifiers()) && isEqualIgnoringCaseAndUnderscores(methodName, method.getName()) && method.getParameterTypes().length == 1 && !method.isAnnotationPresent(DalesbredIgnore.class)) { + if (isPublic(method.getModifiers()) + && isEqualIgnoringCaseAndUnderscores(methodName, method.getName()) + && method.getParameterCount() == parameterCount + && !method.isAnnotationPresent(DalesbredIgnore.class)) { if (result != null) - throw new InstantiationFailureException("Conflicting setters for property: " + result + " - " + name); + throw new InstantiationFailureException("Conflicting accessors for property: " + result + " - " + propertyName); result = method; } } @@ -134,5 +185,59 @@ void set(Object object, Object value) { } } } + + private static final class NestedPathAccessor extends PropertyAccessor { + + @NotNull + private final PropertyReader[] readers; + + @NotNull + private final String path; + + @NotNull + private final PropertyAccessor accessor; + + public NestedPathAccessor(@NotNull PropertyReader[] readers, @NotNull String path, @NotNull PropertyAccessor accessor) { + this.readers = readers; + this.path = path; + this.accessor = accessor; + } + + @Override + Type getType() { + return accessor.getType(); + } + + @Override + void set(Object object, Object value) { + accessor.set(resolveFinalObject(object), value); + } + + @NotNull + private Object resolveFinalObject(@NotNull Object object) { + try { + Object obj = object; + + for (PropertyReader reader : readers) { + Object value = reader.propertyValue(obj); + if (value != null) + obj = value; + else + throw new InstantiationFailureException( + "Failed to set property for '" + path + "', because one of the intermediate objects was null."); + } + + return obj; + + } catch (InvocationTargetException | IllegalAccessException e) { + throw new InstantiationFailureException("Failed to set property for '" + path + "'.", e); + } + } + } + + private interface PropertyReader { + @Nullable + Object propertyValue(@NotNull Object o) throws IllegalAccessException, InvocationTargetException; + } } diff --git a/dalesbred/src/test/java/org/dalesbred/DatabasePropertyPathBindingTest.java b/dalesbred/src/test/java/org/dalesbred/DatabasePropertyPathBindingTest.java new file mode 100644 index 00000000..86a82781 --- /dev/null +++ b/dalesbred/src/test/java/org/dalesbred/DatabasePropertyPathBindingTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 Evident Solutions Oy + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.dalesbred; + +import org.junit.Rule; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class DatabasePropertyPathBindingTest { + + private final Database db = TestDatabaseProvider.createInMemoryHSQLDatabase(); + + @Rule + public final TransactionalTestsRule rule = new TransactionalTestsRule(db); + + @Test + public void bindingToNestedPaths() { + ResultClass result = db.findUnique(ResultClass.class, + "select 'AAA' as \"nestedField.foo\"," + + " 'BBB' as \"nestedGetter.foo\"" + + " from (VALUES (0))"); + + assertThat(result.nestedField.foo, is("AAA")); + assertThat(result.getNestedGetter().foo, is("BBB")); + } + + public static final class ResultClass { + public final NestedClass nestedField = new NestedClass(); + private final NestedClass nestedGetterBackingField = new NestedClass(); + + public NestedClass getNestedGetter() { + return nestedGetterBackingField; + } + } + + public static final class NestedClass { + public String foo; + } +} diff --git a/dalesbred/src/test/java/org/dalesbred/internal/instantiation/PropertyAccessorTest.java b/dalesbred/src/test/java/org/dalesbred/internal/instantiation/PropertyAccessorTest.java index c0ae6fb0..56c1df09 100644 --- a/dalesbred/src/test/java/org/dalesbred/internal/instantiation/PropertyAccessorTest.java +++ b/dalesbred/src/test/java/org/dalesbred/internal/instantiation/PropertyAccessorTest.java @@ -24,12 +24,14 @@ import org.dalesbred.annotation.DalesbredIgnore; import org.dalesbred.annotation.Reflective; +import org.jetbrains.annotations.Nullable; import org.junit.Test; import java.util.Optional; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; public class PropertyAccessorTest { @@ -54,6 +56,53 @@ public void ignoredFields() { assertThat(PropertyAccessor.findAccessor(IgnoredValues.class, "ignoredField"), is(Optional.empty())); } + @Test + public void nestedPathsWithIntermediateFields() { + PropertyAccessor accessor = PropertyAccessor.findAccessor(NestedPaths.class, "namedField.name").orElse(null); + + assertNotNull(accessor); + + NestedPaths paths = new NestedPaths(); + accessor.set(paths, "foo"); + + assertThat(paths.namedField.name, is("foo")); + } + + @Test + public void nestedPathsWithIntermediateGetters() { + PropertyAccessor accessor = PropertyAccessor.findAccessor(NestedPaths.class, "namedGetter.name").orElse(null); + + assertNotNull(accessor); + + NestedPaths paths = new NestedPaths(); + accessor.set(paths, "foo"); + + assertThat(paths.getNamedGetter().name, is("foo")); + } + + @Test(expected = InstantiationFailureException.class) + public void nestedPathsWithNullFields() { + PropertyAccessor accessor = PropertyAccessor.findAccessor(NestedPaths.class, "nullField.name").orElse(null); + + assertNotNull(accessor); + + accessor.set(new NestedPaths(), "foo"); + } + + @Test(expected = InstantiationFailureException.class) + public void nestedPathsWithNullGetters() { + PropertyAccessor accessor = PropertyAccessor.findAccessor(NestedPaths.class, "nullGetter.name").orElse(null); + + assertNotNull(accessor); + + accessor.set(new NestedPaths(), "foo"); + } + + @Test + public void invalidPathElements() { + assertThat(PropertyAccessor.findAccessor(Named.class, "foo.name"), is(Optional.empty())); + } + private static class DepartmentWithFields { @Reflective public String departmentName; @@ -73,6 +122,29 @@ public void setDepartmentName(String departmentName) { } } + public static class NestedPaths { + public final Named namedField = new Named(); + + @Reflective + public final Named nullField = null; + + @Reflective + public Named getNamedGetter() { + return namedField; + } + + @Nullable + @Reflective + public Named getNullGetter() { + return null; + } + } + + public static class Named { + @Reflective + public String name; + } + @SuppressWarnings("unused") public static class IgnoredValues { diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 9994004d..db2ba692 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -83,6 +83,26 @@ columns are set using properties or direct field access. So even the following w } ---- +And if you have nested objects, you can bind to them as well as long as all objects in the path are instantiated: + +[source,java,indent=0] +---- + List departments = + db.findAll(Employee.class, "select id, first_name as \"name.first\", last_name as \"name.last\" from employee"); + + ... + + public final class Employee { + public int id; + public final Name name = new Name(); + } + + public final class Name { + public String first; + public String last; + } +---- + You can also convert the results directly to a map: [source,java,indent=0]