From c136746f0e4e90b4fe37f4dc2aae1a2b12e48649 Mon Sep 17 00:00:00 2001
From: SQiShER <SQiShER@users.noreply.github.com>
Date: Sun, 12 Feb 2017 23:09:46 +0100
Subject: [PATCH 1/3] Improved introspector

---
 .../introspection/DefaultIntrospector.java    |  42 +++++
 .../diff/introspection/FieldAccessor.java     | 105 +++++++++++++
 .../diff/introspection/FieldIntrospector.java |  34 +++++
 .../GetterSetterIntrospector.java             |  81 ++++++++++
 .../introspection/ObjectDiffProperty.java     |   2 +-
 .../diff/introspection/DtoForTesting.java     |  14 ++
 .../introspection/FieldAccessorTest.groovy    |  91 +++++++++++
 .../FieldIntrospectorTest.groovy              |  38 +++++
 .../GetterSetterIntrospectorTest.groovy       | 143 ++++++++++++++++++
 .../introspection/IntrospectorTestType.java   |  32 ++++
 10 files changed, 581 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
 create mode 100644 src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
 create mode 100644 src/main/java/de/danielbechler/diff/introspection/FieldIntrospector.java
 create mode 100644 src/main/java/de/danielbechler/diff/introspection/GetterSetterIntrospector.java
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/DtoForTesting.java
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/FieldAccessorTest.groovy
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/FieldIntrospectorTest.groovy
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/GetterSetterIntrospectorTest.groovy
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java

diff --git a/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java b/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
new file mode 100644
index 00000000..c84a89f4
--- /dev/null
+++ b/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
@@ -0,0 +1,42 @@
+package de.danielbechler.diff.introspection;
+
+import de.danielbechler.diff.access.PropertyAwareAccessor;
+import de.danielbechler.diff.instantiation.TypeInfo;
+
+import java.util.Collection;
+
+public class DefaultIntrospector implements Introspector
+{
+	private final FieldIntrospector fieldIntrospector = new FieldIntrospector();
+	private final GetterSetterIntrospector getterSetterIntrospector = new GetterSetterIntrospector();
+	private boolean returnFields = false;
+
+	public TypeInfo introspect(final Class<?> type)
+	{
+		final TypeInfo typeInfo = new TypeInfo(type);
+		if (returnFields)
+		{
+			final Collection<PropertyAwareAccessor> fieldAccessors = fieldIntrospector.introspect(type).getAccessors();
+			for (final PropertyAwareAccessor fieldAccessor : fieldAccessors)
+			{
+				typeInfo.addPropertyAccessor(fieldAccessor);
+			}
+		}
+		final Collection<PropertyAwareAccessor> getterSetterAccessors = getterSetterIntrospector.introspect(type).getAccessors();
+		for (final PropertyAwareAccessor getterSetterAccessor : getterSetterAccessors)
+		{
+			typeInfo.addPropertyAccessor(getterSetterAccessor);
+		}
+		return typeInfo;
+	}
+
+	public void setReturnFields(final boolean returnFields)
+	{
+		this.returnFields = returnFields;
+	}
+
+	public void setReturnFinalFields(final boolean returnFinalFields)
+	{
+		fieldIntrospector.setReturnFinalFields(returnFinalFields);
+	}
+}
diff --git a/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java b/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
new file mode 100644
index 00000000..e8031ae8
--- /dev/null
+++ b/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
@@ -0,0 +1,105 @@
+package de.danielbechler.diff.introspection;
+
+import de.danielbechler.diff.access.PropertyAwareAccessor;
+import de.danielbechler.diff.selector.BeanPropertyElementSelector;
+import de.danielbechler.diff.selector.ElementSelector;
+import de.danielbechler.util.Assert;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class FieldAccessor implements PropertyAwareAccessor
+{
+	private final Field field;
+
+	FieldAccessor(final Field field)
+	{
+		Assert.notNull(field, "field");
+		this.field = field;
+	}
+
+	public Class<?> getType()
+	{
+		return field.getType();
+	}
+
+	public Set<String> getCategoriesFromAnnotation()
+	{
+		return Collections.emptySet();
+	}
+
+	public ElementSelector getElementSelector()
+	{
+		return new BeanPropertyElementSelector(getPropertyName());
+	}
+
+	public Object get(Object target)
+	{
+		try
+		{
+			return field.get(target);
+		}
+		catch (IllegalAccessException e)
+		{
+			throw new PropertyReadException(getPropertyName(), getType(), e);
+		}
+	}
+
+	public void set(Object target, Object value)
+	{
+		try
+		{
+			field.setAccessible(true);
+			field.set(target, value);
+		}
+		catch (IllegalAccessException e)
+		{
+			throw new PropertyWriteException(getPropertyName(), getType(), value, e);
+		}
+		finally
+		{
+			field.setAccessible(false);
+		}
+	}
+
+	public void unset(Object target)
+	{
+	}
+
+	public String getPropertyName()
+	{
+		return field.getName();
+	}
+
+	public Set<Annotation> getFieldAnnotations()
+	{
+		final Set<Annotation> fieldAnnotations = new HashSet<Annotation>(field.getAnnotations().length);
+		fieldAnnotations.addAll(Arrays.asList(field.getAnnotations()));
+		return fieldAnnotations;
+	}
+
+	public <T extends Annotation> T getFieldAnnotation(Class<T> annotationClass)
+	{
+		return field.getAnnotation(annotationClass);
+	}
+
+	public Set<Annotation> getReadMethodAnnotations()
+	{
+		return Collections.emptySet();
+	}
+
+	public <T extends Annotation> T getReadMethodAnnotation(Class<T> annotationClass)
+	{
+		return null;
+	}
+
+	public boolean isExcludedByAnnotation()
+	{
+		ObjectDiffProperty annotation = getFieldAnnotation(ObjectDiffProperty.class);
+		return annotation != null && annotation.excluded();
+	}
+}
diff --git a/src/main/java/de/danielbechler/diff/introspection/FieldIntrospector.java b/src/main/java/de/danielbechler/diff/introspection/FieldIntrospector.java
new file mode 100644
index 00000000..c2ea01c1
--- /dev/null
+++ b/src/main/java/de/danielbechler/diff/introspection/FieldIntrospector.java
@@ -0,0 +1,34 @@
+package de.danielbechler.diff.introspection;
+
+import de.danielbechler.diff.instantiation.TypeInfo;
+
+import java.lang.reflect.*;
+
+public class FieldIntrospector implements Introspector
+{
+    private boolean returnFinalFields;
+
+    public TypeInfo introspect(final Class<?> type)
+    {
+        final TypeInfo typeInfo = new TypeInfo(type);
+        for (final Field field : type.getFields())
+        {
+            if (shouldSkip(field))
+            {
+                continue;
+            }
+            typeInfo.addPropertyAccessor(new FieldAccessor(field));
+        }
+        return typeInfo;
+    }
+
+    private boolean shouldSkip(final Field field)
+    {
+        return Modifier.isFinal(field.getModifiers()) && !returnFinalFields;
+    }
+
+    public void setReturnFinalFields(final boolean returnFinalFields)
+    {
+        this.returnFinalFields = returnFinalFields;
+    }
+}
diff --git a/src/main/java/de/danielbechler/diff/introspection/GetterSetterIntrospector.java b/src/main/java/de/danielbechler/diff/introspection/GetterSetterIntrospector.java
new file mode 100644
index 00000000..83ce8d4e
--- /dev/null
+++ b/src/main/java/de/danielbechler/diff/introspection/GetterSetterIntrospector.java
@@ -0,0 +1,81 @@
+package de.danielbechler.diff.introspection;
+
+import de.danielbechler.diff.instantiation.TypeInfo;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GetterSetterIntrospector implements Introspector
+{
+	public TypeInfo introspect(final Class<?> type)
+	{
+		final TypeInfo typeInfo = new TypeInfo(type);
+		for (final Method getter : gettersOf(type))
+		{
+			if (shouldSkip(getter))
+			{
+				continue;
+			}
+			final String propertyName = getPropertyName(getter);
+			final Method setter = getCorrespondingSetter(type, getter);
+			final PropertyAccessor accessor = new PropertyAccessor(propertyName, getter, setter);
+			typeInfo.addPropertyAccessor(accessor);
+		}
+		return typeInfo;
+	}
+
+	private Method getCorrespondingSetter(final Class<?> type, final Method getter)
+	{
+		final String setterMethodName = getter.getName().replaceAll("^get", "set");
+		try
+		{
+			return type.getMethod(setterMethodName, getter.getReturnType());
+		}
+		catch (NoSuchMethodException ignored)
+		{
+			return null;
+		}
+	}
+
+	private String getPropertyName(final Method getter)
+	{
+		final StringBuilder sb = new StringBuilder(getter.getName());
+		sb.delete(0, 3);
+		sb.setCharAt(0, Character.toLowerCase(sb.charAt(0)));
+		return sb.toString();
+	}
+
+	private List<Method> gettersOf(final Class<?> type)
+	{
+		final List<Method> filteredMethods = new ArrayList<Method>(type.getMethods().length);
+		for (final Method method : type.getMethods())
+		{
+			final String methodName = method.getName();
+			if (!methodName.startsWith("get") || methodName.length() <= 3)
+			{
+				continue;
+			}
+			if (method.getGenericParameterTypes().length != 0)
+			{
+				continue;
+			}
+			filteredMethods.add(method);
+		}
+		return filteredMethods;
+	}
+
+	@SuppressWarnings("RedundantIfStatement")
+	private static boolean shouldSkip(final Method getter)
+	{
+		if (getter.getName().equals("getClass")) // Java & Groovy
+		{
+			return true;
+		}
+		if (getter.getName().equals("getMetaClass")) // Groovy
+		{
+			return true;
+		}
+		return false;
+	}
+}
diff --git a/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java
index 63f99c6c..06641a4b 100644
--- a/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java
+++ b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java
@@ -31,7 +31,7 @@
  * @author Daniel Bechler
  */
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
+@Target({ElementType.METHOD, ElementType.FIELD})
 @Inherited
 public @interface ObjectDiffProperty
 {
diff --git a/src/test/java/de/danielbechler/diff/introspection/DtoForTesting.java b/src/test/java/de/danielbechler/diff/introspection/DtoForTesting.java
new file mode 100644
index 00000000..6435ae46
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/DtoForTesting.java
@@ -0,0 +1,14 @@
+package de.danielbechler.diff.introspection;
+
+@SuppressWarnings("WeakerAccess")
+public class DtoForTesting
+{
+	@ObjectDiffProperty(categories = {"foo"}, excluded = true)
+	public String publicField;
+	public final String publicFinalField;
+
+	public DtoForTesting(String publicFinalField)
+	{
+		this.publicFinalField = publicFinalField;
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/FieldAccessorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/FieldAccessorTest.groovy
new file mode 100644
index 00000000..f0902e48
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/FieldAccessorTest.groovy
@@ -0,0 +1,91 @@
+package de.danielbechler.diff.introspection
+
+import spock.lang.Specification
+import spock.lang.Subject
+
+import java.lang.annotation.Annotation
+import java.lang.reflect.Field
+
+class FieldAccessorTest extends Specification {
+
+	@Subject
+	FieldAccessor fieldAccessor
+
+	def setup() {
+		Field field = DtoForTesting.class.fields.find { Field field -> field.name == 'publicField' }
+		fieldAccessor = new FieldAccessor(field)
+	}
+
+	def 'getPropertyName'() {
+		expect:
+		fieldAccessor.propertyName == 'publicField'
+	}
+
+	def 'getType'() {
+		expect:
+		fieldAccessor.type == String
+	}
+
+	def 'get'() {
+		setup:
+		String expectedValue = UUID.randomUUID().toString()
+		DtoForTesting dto = new DtoForTesting('foo')
+		dto.publicField = expectedValue
+		expect:
+		fieldAccessor.get(dto) == expectedValue
+	}
+
+	def 'set'() {
+		given:
+		DtoForTesting dto = new DtoForTesting('foo')
+		when:
+		String expectedValue = UUID.randomUUID().toString()
+		fieldAccessor.set(dto, expectedValue)
+		then:
+		dto.publicField == expectedValue
+	}
+
+	def 'set should be able to change value of static field'() {
+		given:
+		fieldAccessor = new FieldAccessor(DtoForTesting.class.fields.find { Field field ->
+			field.name == 'publicFinalField'
+		})
+		DtoForTesting dto = new DtoForTesting('foo')
+		when:
+		fieldAccessor.set(dto, 'bar')
+		then:
+		dto.publicFinalField == 'bar'
+	}
+
+	def 'getFieldAnnotations'() {
+		when:
+		Set<Annotation> annotations = fieldAccessor.fieldAnnotations
+		then:
+		annotations.size() == 1
+		and:
+		ObjectDiffProperty annotation = annotations.first() as ObjectDiffProperty
+		annotation.categories() as List == ['foo']
+	}
+
+	def 'getFieldAnnotation'() {
+		setup:
+		def annotation = fieldAccessor.getFieldAnnotation(ObjectDiffProperty)
+		expect:
+		annotation != null
+	}
+
+	def 'getReadMethodAnnotations'() {
+		expect:
+		fieldAccessor.readMethodAnnotations == [] as Set
+	}
+
+	def 'getReadMethodAnnotation'() {
+		expect:
+		fieldAccessor.getReadMethodAnnotation(ObjectDiffProperty) == null
+	}
+
+	def 'isExcludedByAnnotation'() {
+		expect:
+		fieldAccessor.isExcludedByAnnotation()
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/FieldIntrospectorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/FieldIntrospectorTest.groovy
new file mode 100644
index 00000000..2db37fe2
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/FieldIntrospectorTest.groovy
@@ -0,0 +1,38 @@
+package de.danielbechler.diff.introspection
+
+import de.danielbechler.diff.access.PropertyAwareAccessor
+import de.danielbechler.diff.instantiation.TypeInfo
+import spock.lang.Specification
+
+class FieldIntrospectorTest extends Specification {
+
+	FieldIntrospector introspector = new FieldIntrospector()
+
+	def 'should not return accessors for final fields if deactivated'() {
+		given:
+		introspector.setReturnFinalFields(false)
+		when:
+		TypeInfo result = introspector.introspect(DtoForTesting.class)
+		then:
+		result.accessors.find { PropertyAwareAccessor accessor -> accessor.propertyName == 'publicFinalField' } == null
+		result.accessors.find { PropertyAwareAccessor accessor -> accessor.propertyName == 'publicField' } != null
+	}
+
+	def 'should return accessors for final fields if activated'() {
+		given:
+		introspector.setReturnFinalFields(true)
+		when:
+		TypeInfo result = introspector.introspect(DtoForTesting.class)
+		then:
+		result.accessors.find { PropertyAwareAccessor accessor -> accessor.propertyName == 'publicFinalField' } != null
+		result.accessors.find { PropertyAwareAccessor accessor -> accessor.propertyName == 'publicField' } != null
+	}
+
+	def 'should returns accessors for public fields'() {
+		when:
+		TypeInfo result = introspector.introspect(DtoForTesting.class)
+		then:
+		result.accessors.size() == 1
+		result.accessors.first().propertyName == 'publicField'
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/GetterSetterIntrospectorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/GetterSetterIntrospectorTest.groovy
new file mode 100644
index 00000000..22d113d1
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/GetterSetterIntrospectorTest.groovy
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2014 Daniel Bechler
+ *
+ * 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 de.danielbechler.diff.introspection
+
+import de.danielbechler.diff.access.PropertyAwareAccessor
+import de.danielbechler.diff.mock.ObjectWithString
+import spock.lang.Specification
+
+import java.beans.BeanInfo
+import java.beans.IntrospectionException
+
+/**
+ * @author Daniel Bechler
+ */
+public class GetterSetterIntrospectorTest extends Specification {
+
+	def introspector = new GetterSetterIntrospector()
+
+	private Map<String, PropertyAwareAccessor> introspect(Class<?> type) {
+		introspector.introspect(type).accessors.collectEntries {
+			accessor -> [accessor.propertyName, accessor]
+		}
+	}
+
+	def 'should return proper accessor for property'() {
+		when:
+		def accessor = introspect(TypeWithOnlyOneProperty).get('value')
+		then:
+		accessor.propertyName == 'value'
+		and:
+		def target = new TypeWithOnlyOneProperty()
+		accessor.get(target) == null
+		and:
+		accessor.set(target, 'bar')
+		accessor.get(target) == 'bar'
+		and:
+		accessor.excludedByAnnotation == false
+		and:
+		accessor.categoriesFromAnnotation.isEmpty()
+	}
+
+	def 'should return PropertyAwareAccessors for each property of the given class'() {
+		when:
+		def accessors = introspect(TypeWithTwoProperties)
+		then:
+		accessors.size() == 2
+		accessors.get('foo') != null
+		accessors.get('bar') != null
+	}
+
+	def 'should apply categories of ObjectDiffProperty annotation to accessor'() {
+		when:
+		def accessor = introspect(TypeWithPropertyAnnotation).get('value')
+		then:
+		accessor.categoriesFromAnnotation.size() == 2
+		accessor.categoriesFromAnnotation.containsAll(['category1', 'category2'])
+	}
+
+	def 'should apply exclusion of ObjectDiffProperty annotation to accessor'() {
+		when:
+		def accessor = introspect(TypeWithPropertyAnnotation).get('value')
+		then:
+		accessor.excludedByAnnotation == true
+	}
+
+	def 'should throw exception when invoked without type'() {
+		when:
+		introspector.introspect(null)
+		then:
+		thrown(IllegalArgumentException)
+	}
+
+	def 'should skip default class properties'() {
+		expect:
+		introspect(TypeWithNothingButDefaultProperties).isEmpty()
+	}
+
+	def 'should skip properties without getter'() {
+		expect:
+		introspect(TypeWithPropertyWithoutGetter).isEmpty()
+	}
+
+	def 'should wrap IntrospectionException with RuntimeException'() {
+		given:
+		introspector = new StandardIntrospector() {
+			@Override
+			protected BeanInfo getBeanInfo(final Class<?> type) throws IntrospectionException {
+				throw new IntrospectionException(type.getCanonicalName());
+			}
+		};
+		when:
+		introspector.introspect(ObjectWithString.class);
+		then:
+		thrown(RuntimeException)
+	}
+
+	private class TypeWithNothingButDefaultProperties {
+	}
+
+	private class TypeWithPropertyWithoutGetter {
+		private String value
+
+		void setValue(String value) {
+			this.value = value
+		}
+	}
+
+	private class TypeWithPropertyAnnotation {
+		private String value
+
+		@ObjectDiffProperty(excluded = true, categories = ['category1', 'category2'])
+		String getValue() {
+			return value
+		}
+
+		void setValue(String value) {
+			this.value = value
+		}
+	}
+
+	private class TypeWithOnlyOneProperty {
+		def value
+	}
+
+	private class TypeWithTwoProperties {
+		def foo
+		def bar
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java b/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java
new file mode 100644
index 00000000..add9e524
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java
@@ -0,0 +1,32 @@
+package de.danielbechler.diff.introspection;
+
+public class IntrospectorTestType
+{
+	public static final String constantField = "constant_field";
+	private String privateField;
+	String packageProtectedField;
+	protected String protectedField;
+	public String publicField;
+	@ObjectDiffProperty
+	public String annotatedPublicField;
+	public final String publicFinalField = "public_final_field";
+	private String readWriteProperty;
+	@ObjectDiffProperty
+	private String fieldAnnotatedReadOnlyProperty;
+
+	@ObjectDiffProperty
+	public String getReadWriteProperty()
+	{
+		return readWriteProperty;
+	}
+
+	public void setReadWriteProperty(final String readWriteProperty)
+	{
+		this.readWriteProperty = readWriteProperty;
+	}
+
+	public String getFieldAnnotatedReadOnlyProperty()
+	{
+		return fieldAnnotatedReadOnlyProperty;
+	}
+}

From 9a7bfb48e714921f76168497949323839d63fb98 Mon Sep 17 00:00:00 2001
From: SQiShER <SQiShER@users.noreply.github.com>
Date: Tue, 16 Jan 2018 20:14:01 +0100
Subject: [PATCH 2/3] Added field access capabilities (work in progress)

---
 .../AbstractIntrospectorSpecification.groovy  | 111 ++++++++++++++++++
 .../DefaultIntrospectorTest.groovy            |  26 ++++
 .../introspection/IntrospectorTestType.java   |  49 ++++++--
 .../StandardIntrospectorTest.groovy           |  65 +++++-----
 4 files changed, 212 insertions(+), 39 deletions(-)
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/AbstractIntrospectorSpecification.groovy
 create mode 100644 src/test/java/de/danielbechler/diff/introspection/DefaultIntrospectorTest.groovy

diff --git a/src/test/java/de/danielbechler/diff/introspection/AbstractIntrospectorSpecification.groovy b/src/test/java/de/danielbechler/diff/introspection/AbstractIntrospectorSpecification.groovy
new file mode 100644
index 00000000..3bdb6336
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/AbstractIntrospectorSpecification.groovy
@@ -0,0 +1,111 @@
+package de.danielbechler.diff.introspection
+
+import de.danielbechler.diff.access.PropertyAwareAccessor
+import spock.lang.Specification
+
+abstract class AbstractIntrospectorSpecification extends Specification {
+
+	abstract Introspector getIntrospector()
+
+	def 'private field accessible via getter and setter'() {
+		given:
+		def testType = new IntrospectorTestType()
+
+		when:
+		def typeInfo = introspector.introspect(IntrospectorTestType)
+		def accessor = getAccessorByName(typeInfo.accessors, 'readWriteProperty')
+
+		then:
+		accessor != null
+
+		and:
+		accessor.fieldAnnotations.size() == 1
+		def fieldAnnotation = accessor.fieldAnnotations.first() as ObjectDiffProperty
+		fieldAnnotation.categories().first() == 'field'
+
+		and:
+		accessor.readMethodAnnotations.size() == 1
+		def getterAnnotation = accessor.readMethodAnnotations.first() as ObjectDiffProperty
+		getterAnnotation.categories().first() == 'getter'
+
+		and:
+		accessor.categoriesFromAnnotation == ['getter'] as Set
+
+		when:
+		def value1 = UUID.randomUUID().toString()
+		accessor.set(testType, value1)
+
+		then:
+		testType.getReadWriteProperty() == value1
+
+		when:
+		def value2 = UUID.randomUUID().toString()
+		testType.setReadWriteProperty(value2)
+
+		then:
+		accessor.get(testType) == value2
+	}
+
+	def 'private field accessible via getter and ambiguous setters'() {
+		given:
+		def testType = new IntrospectorTestType()
+
+		when:
+		def typeInfo = introspector.introspect(IntrospectorTestType)
+		def accessor = getAccessorByName(typeInfo.accessors, 'ambiguousSetterProperty')
+
+		then:
+		accessor != null
+
+		when:
+		def value1 = Integer.valueOf(10)
+		accessor.set(testType, value1)
+
+		then:
+		testType.getAmbiguousSetterProperty() == value1
+
+		when:
+		def value2 = Math.random()
+		testType.setAmbiguousSetterProperty(value2 as Number)
+
+		then:
+		accessor.get(testType) == value2
+	}
+
+	def 'private field only accessible via getter'() {
+		given:
+		def testType = new IntrospectorTestType()
+
+		when:
+		def typeInfo = introspector.introspect(IntrospectorTestType)
+		def accessor = getAccessorByName(typeInfo.accessors, 'readOnlyProperty')
+
+		then:
+		accessor != null
+
+		and:
+		accessor.fieldAnnotations.size() == 1
+		def fieldAnnotation = accessor.fieldAnnotations.first() as ObjectDiffProperty
+		fieldAnnotation.categories().first() == 'field'
+
+		and:
+		accessor.readMethodAnnotations.size() == 1
+		def getterAnnotation = accessor.readMethodAnnotations.first() as ObjectDiffProperty
+		getterAnnotation.categories().first() == 'getter'
+
+		when:
+		def initialReadOnlyPropertyValue = testType.getReadOnlyProperty()
+		def newReadOnlyPropertyValue = UUID.randomUUID().toString()
+		accessor.set(testType, newReadOnlyPropertyValue)
+
+		then:
+		testType.getReadOnlyProperty() == initialReadOnlyPropertyValue
+
+		expect:
+		accessor.get(testType) == testType.getReadOnlyProperty()
+	}
+
+	static PropertyAwareAccessor getAccessorByName(Collection<PropertyAwareAccessor> accessors, String propertyName) {
+		return accessors.find { it.propertyName == propertyName }
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/DefaultIntrospectorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/DefaultIntrospectorTest.groovy
new file mode 100644
index 00000000..0a9bcb77
--- /dev/null
+++ b/src/test/java/de/danielbechler/diff/introspection/DefaultIntrospectorTest.groovy
@@ -0,0 +1,26 @@
+package de.danielbechler.diff.introspection
+
+import spock.lang.Subject
+
+class DefaultIntrospectorTest extends AbstractIntrospectorSpecification {
+
+	@Subject
+	DefaultIntrospector introspector = new DefaultIntrospector()
+
+	def 'public field and accessors of the same name'() {
+		given:
+		introspector.returnFields = true
+
+		when:
+		def typeInfo = introspector.introspect(IntrospectorTestType)
+		def accessors = typeInfo.accessors.grep {
+			it.propertyName == 'publicFieldWithAccessors'
+		}
+
+		then: 'only one accessor with that name should be returned'
+		accessors.size() == 1
+
+		and: 'the getter-setter-accessor should win'
+		accessors.first() instanceof PropertyAccessor
+	}
+}
diff --git a/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java b/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java
index add9e524..8ce6ce4c 100644
--- a/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java
+++ b/src/test/java/de/danielbechler/diff/introspection/IntrospectorTestType.java
@@ -1,20 +1,29 @@
 package de.danielbechler.diff.introspection;
 
+import java.util.UUID;
+
 public class IntrospectorTestType
 {
 	public static final String constantField = "constant_field";
 	private String privateField;
 	String packageProtectedField;
 	protected String protectedField;
-	public String publicField;
 	@ObjectDiffProperty
-	public String annotatedPublicField;
+	public String publicField;
 	public final String publicFinalField = "public_final_field";
+	@ObjectDiffProperty(categories = {"field"})
 	private String readWriteProperty;
-	@ObjectDiffProperty
-	private String fieldAnnotatedReadOnlyProperty;
+	@ObjectDiffProperty(categories = {"field"})
+	private String readOnlyProperty;
+	private Number ambiguousSetterProperty;
+	public String publicFieldWithAccessors;
 
-	@ObjectDiffProperty
+	public IntrospectorTestType()
+	{
+		readOnlyProperty = UUID.randomUUID().toString();
+	}
+
+	@ObjectDiffProperty(categories = {"getter"})
 	public String getReadWriteProperty()
 	{
 		return readWriteProperty;
@@ -25,8 +34,34 @@ public void setReadWriteProperty(final String readWriteProperty)
 		this.readWriteProperty = readWriteProperty;
 	}
 
-	public String getFieldAnnotatedReadOnlyProperty()
+	@ObjectDiffProperty(categories = {"getter"})
+	public String getReadOnlyProperty()
+	{
+		return readOnlyProperty;
+	}
+
+	public Number getAmbiguousSetterProperty()
+	{
+		return ambiguousSetterProperty;
+	}
+
+	public void setAmbiguousSetterProperty(final Integer ambiguousSetterProperty)
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	public void setAmbiguousSetterProperty(final Number ambiguousSetterProperty)
+	{
+		this.ambiguousSetterProperty = ambiguousSetterProperty;
+	}
+
+	public String getPublicFieldWithAccessors()
+	{
+		return publicFieldWithAccessors;
+	}
+
+	public void setPublicFieldWithAccessors(final String publicFieldWithAccessors)
 	{
-		return fieldAnnotatedReadOnlyProperty;
+		this.publicFieldWithAccessors = publicFieldWithAccessors;
 	}
 }
diff --git a/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy
index dc204624..956b3a0f 100644
--- a/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy
+++ b/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy
@@ -18,7 +18,7 @@ package de.danielbechler.diff.introspection
 
 import de.danielbechler.diff.access.PropertyAwareAccessor
 import de.danielbechler.diff.mock.ObjectWithString
-import spock.lang.Specification
+import spock.lang.Subject
 
 import java.beans.BeanInfo
 import java.beans.IntrospectionException
@@ -26,9 +26,10 @@ import java.beans.IntrospectionException
 /**
  * @author Daniel Bechler
  */
-public class StandardIntrospectorTest extends Specification {
+class StandardIntrospectorTest extends AbstractIntrospectorSpecification {
 
-	def introspector = new StandardIntrospector()
+	@Subject
+	StandardIntrospector introspector = new StandardIntrospector()
 
 	private Map<String, PropertyAwareAccessor> introspect(Class<?> type) {
 		introspector.introspect(type).accessors.collectEntries {
@@ -38,74 +39,74 @@ public class StandardIntrospectorTest extends Specification {
 
 	def 'should return proper accessor for property'() {
 		when:
-		  def accessor = introspect(TypeWithOnlyOneProperty).get('value')
+		def accessor = introspect(TypeWithOnlyOneProperty).get('value')
 		then:
-		  accessor.propertyName == 'value'
+		accessor.propertyName == 'value'
 		and:
-		  def target = new TypeWithOnlyOneProperty()
-		  accessor.get(target) == null
+		def target = new TypeWithOnlyOneProperty()
+		accessor.get(target) == null
 		and:
-		  accessor.set(target, 'bar')
-		  accessor.get(target) == 'bar'
+		accessor.set(target, 'bar')
+		accessor.get(target) == 'bar'
 		and:
-		  accessor.excludedByAnnotation == false
+		accessor.excludedByAnnotation == false
 		and:
-		  accessor.categoriesFromAnnotation.isEmpty()
+		accessor.categoriesFromAnnotation.isEmpty()
 	}
 
 	def 'should return PropertyAwareAccessors for each property of the given class'() {
 		when:
-		  def accessors = introspect(TypeWithTwoProperties)
+		def accessors = introspect(TypeWithTwoProperties)
 		then:
-		  accessors.size() == 2
-		  accessors.get('foo') != null
-		  accessors.get('bar') != null
+		accessors.size() == 2
+		accessors.get('foo') != null
+		accessors.get('bar') != null
 	}
 
 	def 'should apply categories of ObjectDiffProperty annotation to accessor'() {
 		when:
-		  def accessor = introspect(TypeWithPropertyAnnotation).get('value')
+		def accessor = introspect(TypeWithPropertyAnnotation).get('value')
 		then:
-		  accessor.categoriesFromAnnotation.size() == 2
-		  accessor.categoriesFromAnnotation.containsAll(['category1', 'category2'])
+		accessor.categoriesFromAnnotation.size() == 2
+		accessor.categoriesFromAnnotation.containsAll(['category1', 'category2'])
 	}
 
 	def 'should apply exclusion of ObjectDiffProperty annotation to accessor'() {
 		when:
-		  def accessor = introspect(TypeWithPropertyAnnotation).get('value')
+		def accessor = introspect(TypeWithPropertyAnnotation).get('value')
 		then:
-		  accessor.excludedByAnnotation == true
+		accessor.excludedByAnnotation == true
 	}
 
 	def 'should throw exception when invoked without type'() {
 		when:
-		  introspector.introspect(null)
+		introspector.introspect(null)
 		then:
-		  thrown(IllegalArgumentException)
+		thrown(IllegalArgumentException)
 	}
 
 	def 'should skip default class properties'() {
 		expect:
-		  introspect(TypeWithNothingButDefaultProperties).isEmpty()
+		introspect(TypeWithNothingButDefaultProperties).isEmpty()
 	}
 
 	def 'should skip properties without getter'() {
 		expect:
-		  introspect(TypeWithPropertyWithoutGetter).isEmpty()
+		introspect(TypeWithPropertyWithoutGetter).isEmpty()
 	}
 
 	def 'should wrap IntrospectionException with RuntimeException'() {
 		given:
-		  introspector = new StandardIntrospector() {
-			  @Override
-			  protected BeanInfo getBeanInfo(final Class<?> type) throws IntrospectionException {
-				  throw new IntrospectionException(type.getCanonicalName());
-			  }
-		  };
+		introspector = new StandardIntrospector() {
+			@Override
+			protected BeanInfo getBeanInfo(final Class<?> type) throws IntrospectionException {
+				throw new IntrospectionException(type.getCanonicalName());
+			}
+		};
 		when:
-		  introspector.introspect(ObjectWithString.class);
+		introspector.introspect(ObjectWithString.class);
 		then:
-		  thrown(RuntimeException)
+		thrown(RuntimeException)
 	}
 
 	private class TypeWithNothingButDefaultProperties {

From 3e88c6e628c8d20e3bc67570f5ae5550bb8faa5a Mon Sep 17 00:00:00 2001
From: SQiShER <SQiShER@users.noreply.github.com>
Date: Tue, 16 Jan 2018 20:14:37 +0100
Subject: [PATCH 3/3] Added field access capabilities (work in progress)

---
 .../diff/instantiation/TypeInfo.java          | 16 ++++++++++++++++
 .../introspection/DefaultIntrospector.java    | 19 +++++++++++++------
 .../diff/introspection/FieldAccessor.java     |  8 ++++++++
 3 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java b/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java
index 4ffdce72..24d10caf 100644
--- a/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java
+++ b/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java
@@ -63,6 +63,22 @@ public Collection<PropertyAwareAccessor> getAccessors()
 		return accessors;
 	}
 
+	/**
+	 * @param name The property name to find an accessor for.
+	 * @return The accessor for the given property name or null if none is found.
+	 */
+	public PropertyAwareAccessor getAccessorByName(final String name)
+	{
+		for (PropertyAwareAccessor accessor : accessors)
+		{
+			if (accessor.getPropertyName().equals(name))
+			{
+				return accessor;
+			}
+		}
+		return null;
+	}
+
 	public void setInstanceFactory(final InstanceFactory instanceFactory)
 	{
 		this.instanceFactory = instanceFactory;
diff --git a/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java b/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
index c84a89f4..d439fa15 100644
--- a/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
+++ b/src/main/java/de/danielbechler/diff/introspection/DefaultIntrospector.java
@@ -14,19 +14,26 @@ public class DefaultIntrospector implements Introspector
 	public TypeInfo introspect(final Class<?> type)
 	{
 		final TypeInfo typeInfo = new TypeInfo(type);
+
+		final Collection<PropertyAwareAccessor> getterSetterAccessors = getterSetterIntrospector.introspect(type).getAccessors();
+		for (final PropertyAwareAccessor getterSetterAccessor : getterSetterAccessors)
+		{
+			typeInfo.addPropertyAccessor(getterSetterAccessor);
+		}
+
 		if (returnFields)
 		{
 			final Collection<PropertyAwareAccessor> fieldAccessors = fieldIntrospector.introspect(type).getAccessors();
 			for (final PropertyAwareAccessor fieldAccessor : fieldAccessors)
 			{
-				typeInfo.addPropertyAccessor(fieldAccessor);
+				final String propertyName = fieldAccessor.getPropertyName();
+				if (typeInfo.getAccessorByName(propertyName) == null)
+				{
+					typeInfo.addPropertyAccessor(fieldAccessor);
+				}
 			}
 		}
-		final Collection<PropertyAwareAccessor> getterSetterAccessors = getterSetterIntrospector.introspect(type).getAccessors();
-		for (final PropertyAwareAccessor getterSetterAccessor : getterSetterAccessors)
-		{
-			typeInfo.addPropertyAccessor(getterSetterAccessor);
-		}
+
 		return typeInfo;
 	}
 
diff --git a/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java b/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
index e8031ae8..7599a7d1 100644
--- a/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
+++ b/src/main/java/de/danielbechler/diff/introspection/FieldAccessor.java
@@ -102,4 +102,12 @@ public boolean isExcludedByAnnotation()
 		ObjectDiffProperty annotation = getFieldAnnotation(ObjectDiffProperty.class);
 		return annotation != null && annotation.excluded();
 	}
+
+	@Override
+	public String toString()
+	{
+		return "FieldAccessor{" +
+				"field=" + field +
+				'}';
+	}
 }