Skip to content

Commit

Permalink
Fixes #381 : Extraction of nested field/property combination fails
Browse files Browse the repository at this point in the history
  • Loading branch information
a11n authored and joel-costigliola committed Apr 29, 2015
1 parent bce61dc commit 923f296
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 42 deletions.
95 changes: 63 additions & 32 deletions src/main/java/org/assertj/core/extractor/ByNameSingleExtractor.java
Expand Up @@ -12,52 +12,83 @@
*/
package org.assertj.core.extractor;

import static java.lang.String.*;
import static java.lang.String.format;

import java.util.Map;

import org.assertj.core.api.iterable.Extractor;
import org.assertj.core.internal.PropertySupport;
import org.assertj.core.util.introspection.FieldSupport;
import org.assertj.core.util.introspection.IntrospectionError;

import java.util.Map;

class ByNameSingleExtractor<T> implements Extractor<T, Object> {
private static final String SEPARATOR = ".";

private final String propertyOrFieldName;

ByNameSingleExtractor(String propertyOrFieldName) {
this.propertyOrFieldName = propertyOrFieldName;
this.propertyOrFieldName = propertyOrFieldName;
}

@Override
public Object extract(T input) {
if (propertyOrFieldName == null)
throw new IllegalArgumentException("The name of the field/property to read should not be null");
if (propertyOrFieldName.length() == 0)
throw new IllegalArgumentException("The name of the field/property to read should not be empty");
if (input == null)
throw new IllegalArgumentException("The object to extract field/property from should not be null");

// if input is a map, use propertyOrFieldName as a key
if (input instanceof Map) {
Map<?, ?> map = (Map<?, ?>) input;
return map.get(propertyOrFieldName);
}

// first try to get given property values from objects, then try properties
try {
return PropertySupport.instance().propertyValueOf(propertyOrFieldName, Object.class, input);
} catch (IntrospectionError fieldIntrospectionError) {
// no luck with properties, let's try fields
try {
return FieldSupport.extraction().fieldValue(propertyOrFieldName, Object.class, input);
} catch (IntrospectionError propertyIntrospectionError) {
// no field nor property found with given name, it is considered as an error
String message = format("%nCan't find any field or property with name '%s'.%nError when introspecting fields was :%n- %s %nError when introspecting properties was :%n- %s",
propertyOrFieldName, fieldIntrospectionError.getMessage(),
propertyIntrospectionError.getMessage());
throw new IntrospectionError(message);
}
}
if (propertyOrFieldName == null)
throw new IllegalArgumentException("The name of the field/property to read should not be null");
if (propertyOrFieldName.length() == 0)
throw new IllegalArgumentException("The name of the field/property to read should not be empty");
if (input == null)
throw new IllegalArgumentException("The object to extract field/property from should not be null");

// if input is a map, use propertyOrFieldName as a key
if (input instanceof Map) {
Map<?, ?> map = (Map<?, ?>) input;
return map.get(propertyOrFieldName);
}

return extract(propertyOrFieldName, input);
}

private Object extract(String propertyOrFieldName, Object input) {
if (isNested(propertyOrFieldName)) {
String firstPropertyName = popNameFrom(propertyOrFieldName);
Object propertyOrFieldValue = extractValue(firstPropertyName, input);
// extract next sub-property/field value until reaching the last sub-property/field
return extract(nextNameFrom(propertyOrFieldName), propertyOrFieldValue);
}
return extractValue(propertyOrFieldName, input);
}

private Object extractValue(String propertyOrFieldName, Object input) {
// first try to get given property values from objects, then try fields
try {
return PropertySupport.instance().propertyValueOf(propertyOrFieldName, Object.class, input);
} catch (IntrospectionError fieldIntrospectionError) {
// no luck with properties, let's try fields
try {
return FieldSupport.extraction().fieldValue(propertyOrFieldName, Object.class, input);
} catch (IntrospectionError propertyIntrospectionError) {
// no field nor property found with given name, it is considered as an error
String message = format("%nCan't find any field or property with name '%s'.%nError when introspecting fields was :%n- %s %nError when introspecting properties was :%n- %s",
propertyOrFieldName, fieldIntrospectionError.getMessage(),
propertyIntrospectionError.getMessage());
throw new IntrospectionError(message);
}
}
}

private String popNameFrom(String propertyOrFieldNameChain) {
if (!isNested(propertyOrFieldNameChain)) return propertyOrFieldNameChain;
return propertyOrFieldNameChain.substring(0, propertyOrFieldNameChain.indexOf(SEPARATOR));
}

private String nextNameFrom(String propertyOrFieldNameChain) {
if (!isNested(propertyOrFieldNameChain)) return "";
return propertyOrFieldNameChain.substring(propertyOrFieldNameChain.indexOf(SEPARATOR) + 1);
}

private boolean isNested(String propertyOrFieldName) {
return propertyOrFieldName.contains(SEPARATOR)
&& !propertyOrFieldName.startsWith(SEPARATOR)
&& !propertyOrFieldName.endsWith(SEPARATOR);
}
}
Expand Up @@ -14,6 +14,7 @@

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.extractor.Extractors.byName;

import java.util.HashMap;
import java.util.List;
Expand All @@ -33,7 +34,7 @@ public class ByNameSingleExtractorTest {
public ExpectedException thrown = ExpectedException.none();

@Test
public void should_extract_field_values_even_if_property_exist() {
public void should_extract_field_values_even_if_property_does_not_exist() {
Object extractedValues = idExtractor().extract(yoda);

assertThat(extractedValues).isEqualTo(1L);
Expand Down Expand Up @@ -75,30 +76,29 @@ public void should_throw_exception_when_given_name_is_empty() {
}

@Test
public void should_fallback_to_field_if_exception_has_been_thrown_on_property_access() throws Exception {
public void should_fallback_to_field_if_exception_has_been_thrown_on_property_access() {
Object extractedValue = nameExtractor().extract(employeeWithBrokenName("Name"));

assertThat(extractedValue).isEqualTo(new Name("Name"));
}

@Test
public void should_prefer_properties_over_fields() throws Exception {
Object extractedValue = nameExtractor().extract(employeeWithOverriddenName("Overridden Name"));
public void should_prefer_properties_over_fields() {
Object extractedValue = nameExtractor().extract(employeeWithOverridenName("Overriden Name"));

assertThat(extractedValue).isEqualTo(new Name("Overridden Name"));
assertThat(extractedValue).isEqualTo(new Name("Overriden Name"));
}

@Test
public void should_throw_exception_if_property_cannot_be_extracted_due_to_runtime_exception_during_property_access()
throws Exception {
public void should_throw_exception_if_property_cannot_be_extracted_due_to_runtime_exception_during_property_access() {
thrown.expect(IntrospectionError.class);

Employee employee = brokenEmployee();
adultExtractor().extract(employee);
}

@Test
public void should_throw_exception_if_no_object_is_given() throws Exception {
public void should_throw_exception_if_no_object_is_given() {
thrown.expect(IllegalArgumentException.class);

idExtractor().extract(null);
Expand All @@ -125,6 +125,17 @@ public void should_extract_single_value_from_maps_by_key() {
assertThat(maps).extracting("bad key").containsExactly(null, null);
}

@Test
public void should_extract_property_field_combinations() {
Employee darth = new Employee(1L, new Name("Darth", "Vader"), 100);
Employee luke = new Employee(2L, new Name("Luke", "Skywalker"), 26);
darth.field = luke;
luke.field = darth;
luke.surname = new Name("Young", "Padawan");
Object extracted = byName("me.field.me.field.me.field.surname.name").extract(darth);
assertThat(extracted).isEqualTo("Young Padawan");
}

private Employee employeeWithBrokenName(String name) {
return new Employee(1L, new Name(name), 0) {
@Override
Expand All @@ -134,11 +145,11 @@ public Name getName() {
};
}

private Employee employeeWithOverriddenName(final String overriddenName) {
private Employee employeeWithOverridenName(final String overridenName) {
return new Employee(1L, new Name("Name"), 0) {
@Override
public Name getName() {
return new Name(overriddenName);
return new Name(overridenName);
}
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/org/assertj/core/test/Employee.java
Expand Up @@ -57,6 +57,12 @@ public void setAge(int age) {
public boolean isAdult() {
return age > 18;
}

// testing nested combinations of field/property
public Employee field;
public Employee getMe(){
return this;
}

@Override
public String toString() {
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/org/assertj/core/test/Name.java
Expand Up @@ -51,6 +51,11 @@ public String getLast() {
public void setLast(String last) {
this.last = last;
}

// property without field in order to test field/property combinations
public String getName(){
return String.format("%s %s", getFirst(), getLast());
}

@Override
public String toString() {
Expand Down

0 comments on commit 923f296

Please sign in to comment.