diff --git a/.gitignore b/.gitignore
index 31a9c703..9b37d0e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
/build
/classes
+/test-output
.idea/dictionaries
.idea/scopes
.idea/workspace.xml
diff --git a/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java
new file mode 100644
index 00000000..6196ecfb
--- /dev/null
+++ b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanHas.java
@@ -0,0 +1,89 @@
+package org.hamcrest.beans;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+
+/**
+ *
Matches multiple attributes of an object within a single assertion
+ *
+ * How to use it:
+ *
+ * // Static imports
+ * import static org.hamcrest.beans.BeanHas.has;
+ * import static org.hamcrest.beans.BeanProperty.property;
+ * import static org.hamcrest.MatcherAssert.assertThat;
+ * import static org.hamcrest.Matchers.equalTo;
+ * import static org.hamcrest.Matchers.greaterThan;
+ *
+ * Person person = new Person();
+ * person.setFirstName("Sandro");
+ * person.setAge(35);
+ *
+ *
+ * Country uk = new Country();
+ * uk.setName("United Kingdom");
+ *
+ *
+ * Address address = new Address();
+ * address.setPostcode("1234556");
+ * address.setCity("London");
+ * address.setCountry(uk);
+ *
+ *
+ * person.setAddress(address);
+ *
+ *
+ * assertThat(person, has(
+ * property("firstName", equalTo("Sandro")),
+ * property("age", greaterThan(18)),
+ * property("address.city", equalTo("London")),
+ * property("address.postcode", equalTo("1234556")),
+ * property("address.country.name", equalTo("United Kingdom"))));
+ *
+ *
+ * @author Sandro Mancuso
+ */
+public class BeanHas extends BaseMatcher {
+
+ private BeanProperty>[] propertyMatchers;
+ private Description expectedDescription = new StringDescription();
+ private Description mismatchDescription = new StringDescription();
+
+ public BeanHas(BeanProperty>... propertyMatchers) {
+ this.propertyMatchers = propertyMatchers;
+ }
+
+ @Factory
+ public static BeanHas has(BeanProperty>... propertyMatchers) {
+ return new BeanHas(propertyMatchers);
+ }
+
+ public boolean matches(Object item) {
+ boolean matches = true;
+ for (BeanProperty> matcher : propertyMatchers) {
+ if (!matcher.matches(item)) {
+ matches = false;
+ appendDescriptions(item, matcher);
+ }
+ }
+ return matches;
+ }
+
+ public void describeTo(Description description) {
+ description.appendText(expectedDescription.toString());
+ }
+
+ @Override
+ public void describeMismatch(Object item, Description description) {
+ description.appendText(mismatchDescription.toString());
+ }
+
+ private void appendDescriptions(Object item, Matcher> matcher) {
+ matcher.describeTo(expectedDescription);
+ matcher.describeMismatch(item, mismatchDescription);
+ }
+
+}
diff --git a/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java
new file mode 100644
index 00000000..c8d0a0ab
--- /dev/null
+++ b/hamcrest-library/src/main/java/org/hamcrest/beans/BeanProperty.java
@@ -0,0 +1,127 @@
+package org.hamcrest.beans;
+
+import static org.hamcrest.beans.PropertyUtil.NO_ARGUMENTS;
+import static org.hamcrest.beans.PropertyUtil.getPropertyDescriptor;
+
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+
+/**
+ * Property matcher that should be used in conjunction with {@link BeanHas}.
+ *
+ * @see BeanHas
+ *
+ * @author Sandro Mancuso
+ */
+public class BeanProperty extends TypeSafeDiagnosingMatcher {
+
+ private String matchingPropertyName;
+ private Matcher> valueMatcher;
+
+ public BeanProperty(String propertyName, Matcher> valueMatcher) {
+ this.matchingPropertyName = propertyName;
+ this.valueMatcher = valueMatcher;
+ }
+
+ @Factory
+ public static BeanProperty property(String propertyName,Matcher> value) {
+ return new BeanProperty(propertyName, value);
+ }
+
+ @Override
+ public boolean matchesSafely(T bean, Description mismatchDescription) {
+ try {
+ return matchesSafely(bean, matchingPropertyName, mismatchDescription);
+ } catch (Exception e) {
+ mismatchDescription.appendValue(e);
+ return false;
+ }
+ }
+
+ private boolean matchesSafely(Object bean, String propertyName, Description mismatchDescription)
+ throws Exception {
+ Object parentObject = bean;
+ if (isComposedProperty(propertyName)) {
+ String memberObjectProperty = getMemberObjectProperty(propertyName);
+ Object memberObject = getPropertyValue(parentObject, memberObjectProperty);
+ String nextProperty = getNextProperty(propertyName);
+ return matchesSafely(memberObject, nextProperty, mismatchDescription);
+ } else {
+ return matchProperty(bean, propertyName, mismatchDescription);
+ }
+ }
+
+ private String getNextProperty(String composedPropertyName) {
+ return composedPropertyName.substring(composedPropertyName.indexOf(".") + 1);
+ }
+
+ private Object getPropertyValue(Object parentObject, String memberObjectProperty)
+ throws Exception {
+ PropertyDescriptor property = getPropertyDescriptor(memberObjectProperty, parentObject);
+ Method readMethod = property.getReadMethod();
+ return readMethod.invoke(parentObject, NO_ARGUMENTS);
+ }
+
+ private boolean isComposedProperty(String propertyName) {
+ return propertyName.contains(".");
+ }
+
+ private String getMemberObjectProperty(String composedPropertyName) {
+ return composedPropertyName.substring(0, composedPropertyName.indexOf("."));
+ }
+
+ private boolean matchProperty(Object bean, String propertyName, Description mismatchDescription)
+ throws Exception {
+ Method readMethod = findReadMethod(bean, propertyName, mismatchDescription);
+ return (readMethod != null)
+ ? matchPropertyValue(bean, readMethod, mismatchDescription)
+ : false;
+ }
+
+ private boolean matchPropertyValue(Object bean, Method readMethod, Description mismatchDescription)
+ throws Exception {
+ Object propertyValue = readMethod.invoke(bean, NO_ARGUMENTS);
+ boolean valueMatches = valueMatcher.matches(propertyValue);
+ if (!valueMatches) {
+ appendSeparatorTo(mismatchDescription);
+ mismatchDescription.appendText("property \'" + matchingPropertyName + "\' ");
+ valueMatcher.describeMismatch(propertyValue, mismatchDescription);
+ }
+ return valueMatches;
+ }
+
+ private void appendSeparatorTo(Description description) {
+ if (description.toString().length() > 0) {
+ description.appendText(", ");
+ }
+ }
+
+ private Method findReadMethod(Object argument, String propertyName, Description mismatchDescription)
+ throws IllegalArgumentException {
+ PropertyDescriptor propertyDescriptor = getPropertyDescriptor(propertyName, argument);
+ if (null == propertyDescriptor) {
+ mismatchDescription.appendText("No property \"" + matchingPropertyName + "\"");
+ return null;
+ }
+ Method readMethod = propertyDescriptor.getReadMethod();
+ if (null == readMethod) {
+ mismatchDescription.appendText("property \"" + matchingPropertyName + "\" is not readable");
+ }
+ return readMethod;
+ }
+
+ public void describeTo(Description description) {
+ appendSeparatorTo(description);
+ description.appendText("property ");
+ description.appendValue(matchingPropertyName);
+ description.appendText(" = ");
+ description.appendDescriptionOf(valueMatcher);
+ description.appendText(" ");
+ }
+
+}
diff --git a/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java b/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java
index 0032d821..b2fda6d0 100644
--- a/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java
+++ b/hamcrest-unit-test/src/main/java/org/hamcrest/AbstractMatcherTest.java
@@ -33,6 +33,12 @@ public static void assertMismatchDescription(String expected, Matcher supe
Assert.assertEquals("Expected mismatch description", expected, description.toString().trim());
}
+ public static void assertMismatchDescription(String expected, Matcher super T> matcher, T arg, Description description) {
+ Assert.assertFalse("Precondtion: Matcher should not match item.", matcher.matches(arg));
+ matcher.describeMismatch(arg, description);
+ Assert.assertEquals("Expected mismatch description", expected, description.toString().trim());
+ }
+
public void testIsNullSafe() {
// should not throw a NullPointerException
createMatcher().matches(null);
diff --git a/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java
new file mode 100644
index 00000000..977f8e9c
--- /dev/null
+++ b/hamcrest-unit-test/src/main/java/org/hamcrest/beans/BeanHasTest.java
@@ -0,0 +1,123 @@
+package org.hamcrest.beans;
+
+import static org.hamcrest.Matchers.anything;
+import static org.hamcrest.Matchers.isEmptyString;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import junit.framework.TestCase;
+
+import org.hamcrest.Description;
+import org.hamcrest.StringDescription;
+
+public class BeanHasTest extends TestCase {
+
+ private static final String MISMATCH_DESCRIPTION = "MISMATCH DESCRIPTION";
+ private static final String OTHER_MISMATCH_DESCRIPTION = "OTHER MISMATCH DESCRIPTION";
+ private static final String EXPECTED_DESCRIPTION = "EXPECTED DESCRIPTION";
+ private static final String OTHER_EXPECTED_DESCRIPTION = "OTHER EXPECTED DESCRIPTION";
+ private static final boolean MATCHES = true;
+ private static final boolean DOES_NOT_MATCH = false;
+
+ private Object bean;
+ private BeanProperty