Skip to content

Commit

Permalink
Support binding to property paths
Browse files Browse the repository at this point in the history
  • Loading branch information
komu committed Nov 16, 2015
1 parent 3aa81d5 commit 6c74b7e
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,13 +44,48 @@ abstract class PropertyAccessor {
abstract Type getType();

@NotNull
static Optional<PropertyAccessor> findAccessor(@NotNull Class<?> cl, @NotNull String name) {
Optional<PropertyAccessor> setter = findSetter(cl, name).map(SetterPropertyAccessor::new);
private static final Pattern PERIOD = Pattern.compile("\\.");

@NotNull
static Optional<PropertyAccessor> 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<PropertyAccessor> accessor = findFinalAccessor(currentClass, segments[segments.length - 1]);
return accessor.map(a -> new NestedPathAccessor(readers, path, a));
}

@NotNull
private static Optional<PropertyAccessor> findFinalAccessor(@NotNull Class<?> currentClass, @NotNull String name) {
Optional<PropertyAccessor> 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);
}
}

Expand All @@ -67,14 +105,27 @@ private static Optional<Field> findField(@NotNull Class<?> cl, @NotNull String n

@NotNull
private static Optional<Method> findSetter(@NotNull Class<?> cl, @NotNull String name) {
return findGetterOrSetter(cl, name, false);
}

@NotNull
private static Optional<Method> findGetter(@NotNull Class<?> cl, @NotNull String name) {
return findGetterOrSetter(cl, name, true);
}

@NotNull
private static Optional<Method> 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;
}
}
Expand Down Expand Up @@ -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;
}
}

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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 {

Expand Down
20 changes: 20 additions & 0 deletions src/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Employee> 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]
Expand Down

0 comments on commit 6c74b7e

Please sign in to comment.