Repository: assertj/assertj (Java)

Issue:
Extracting through field names not supported for optional in java 17
#### Summary
I'm upgrading to java 17 (from java 11), and I have some tests failing due to an incompatibility between java 17 and assertJ.
One of the breaking changes introduced by java 17 is [JEP 396: Strongly Encapsulate JDK Internals by Default](https://openjdk.java.net/jeps/396).
It seems that assertJ try to force access to non public value (not public) when extracting them through field name => extract the value from an optional ("object.value") and since the value is not public I got this exception:
```
org.assertj.core.util.introspection.IntrospectionError: 
Can't find any field or property with name 'value'.
Error when introspecting properties was :
- No getter for property 'value' in java.util.Optional 
Error when introspecting fields was :
- Unable to obtain the value of the field <'value'> from <Optional[john]>

	at org.assertj.core.util.introspection.PropertyOrFieldSupport.getSimpleValue(PropertyOrFieldSupport.java:88)
	at org.assertj.core.util.introspection.PropertyOrFieldSupport.getValueOf(PropertyOrFieldSupport.java:60)
	at org.assertj.core.util.introspection.PropertyOrFieldSupport.getValueOf(PropertyOrFieldSupport.java:57)
	at org.assertj.core.extractor.ByNameSingleExtractor.apply(ByNameSingleExtractor.java:29)
	at org.assertj.core.api.AbstractAssert.extracting(AbstractAssert.java:1059)
	at org.assertj.core.api.AbstractObjectAssert.extracting(AbstractObjectAssert.java:834)
	at fr.witchbird.cl.negotiation.test.functional.VersionIT.should_access(VersionIT.java:165)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.assertj.core.util.introspection.IntrospectionError: Unable to obtain the value of the field <'value'> from <Optional[john]>
	at org.assertj.core.util.introspection.FieldSupport.readSimpleField(FieldSupport.java:248)
	at org.assertj.core.util.introspection.FieldSupport.fieldValue(FieldSupport.java:202)
	at org.assertj.core.util.introspection.PropertyOrFieldSupport.getSimpleValue(PropertyOrFieldSupport.java:70)
	... 28 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.Object java.util.Optional.value accessible: module java.base does not "opens java.util" to unnamed module @7fbe847c
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at org.assertj.core.util.introspection.FieldUtils.getField(FieldUtils.java:67)
	at org.assertj.core.util.introspection.FieldUtils.readField(FieldUtils.java:143)
	at org.assertj.core.util.introspection.FieldSupport.readSimpleField(FieldSupport.java:208)
	... 30 more
```

#### Example

```java
       private static class Person {
		private final Optional<String> name;

		public Person(final Optional<String> name) {
			this.name = name;
		}

		public Optional<String> getName() {
			return name;
		}
	}

	@Test
	public void should_access() {
		final Optional<String> name = Optional.of("john");
		final var person = new Person(name);
		Assertions.assertThat(person)
		        .extracting("name.value")
		        .isEqualTo("john");
	}
```



Hints:
Thanks for reporting this, we'll check if we can handle `Optional` fields differently.

Meanwhile, you could rewrite your assertion in the following way:

```java
    assertThat(person)
      .extracting(Person::getName, as(InstanceOfAssertFactories.OPTIONAL))
      .hasValue("john");
```
I'm afraid we will have a bunch of issues like that for java 17, unfortunately I don't think there are any command line options for libraries to get access to private fields anymore but maybe I'm wrong.
For the example in this issue, maybe we could have a special handling for `Optional` types when the `extracting` parameter is `value`. I'll have a look and come up with a proposal.

Patch:
diff --git a/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java b/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
--- a/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
+++ b/src/main/java/org/assertj/core/util/introspection/PropertyOrFieldSupport.java
@@ -16,6 +16,7 @@
 import static org.assertj.core.util.Preconditions.checkArgument;
 
 import java.util.Map;
+import java.util.Optional;
 
 import org.assertj.core.util.VisibleForTesting;
 
@@ -60,7 +61,11 @@ public Object getValueOf(String propertyOrFieldName, Object input) {
     return getSimpleValue(propertyOrFieldName, input);
   }
 
+  @SuppressWarnings("rawtypes")
   public Object getSimpleValue(String name, Object input) {
+    // if name is "value" and input is an optional, reflection is not necessary
+    if (name.equals("value") && input instanceof Optional) return ((Optional) input).get();
+
     // try to get name as a property, then try as a field, then try as a map key
     try {
       return propertySupport.propertyValueOf(name, Object.class, input);


Test patch:
diff --git a/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
--- a/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
+++ b/src/test/java/org/assertj/core/api/object/ObjectAssert_extracting_with_String_Test.java
@@ -20,19 +20,14 @@
 
 import java.math.BigDecimal;
 import java.util.Comparator;
+import java.util.Optional;
 
-import org.assertj.core.api.ObjectAssert;
 import org.assertj.core.test.Employee;
 import org.assertj.core.test.Name;
 import org.assertj.core.util.introspection.IntrospectionError;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-/**
- * Tests for <code>{@link ObjectAssert#extracting(String)}</code>.
- */
-@DisplayName("ObjectAssert extracting(String)")
 class ObjectAssert_extracting_with_String_Test {
 
   private Employee luke;
@@ -62,6 +57,16 @@ void should_allow_assertions_on_nested_property_extracted_from_given_object_by_n
                     .isEqualTo("Luke");
   }
 
+  @Test
+  void should_allow_assertions_on_nested_optional_value() {
+    // GIVEN
+    Person chewbacca = new Person("Chewbacca");
+    chewbacca.setNickname(Optional.of("Chewie"));
+    // WHEN/THEN
+    assertThat(chewbacca).extracting("nickname.value")
+                         .isEqualTo("Chewie");
+  }
+
   @Test
   void should_use_property_field_name_as_description_when_extracting_single_property() {
     // WHEN
@@ -108,12 +113,17 @@ void should_throw_IntrospectionError_if_given_field_name_cannot_be_read() {
   private static class Person {
 
     private final String name;
+    private Optional<String> nickname = Optional.empty();
     private BigDecimal height;
 
     public Person(String name) {
       this.name = name;
     }
 
+    public void setNickname(Optional<String> nickname) {
+      this.nickname = nickname;
+    }
+
     public void setHeight(BigDecimal height) {
       this.height = height;
     }
diff --git a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
--- a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
+++ b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test.java
@@ -19,28 +19,21 @@
 import java.util.Collection;
 import java.util.HashMap;
 
-import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 
-/**
- * Tests for <code>{@link PropertyOrFieldSupport#getSimpleValue(String, Object)}</code> with {@code Map} input.
- *
- * @author Stefano Cordio
- */
-@DisplayName("PropertyOrFieldSupport getSimpleValue(String, Map)")
 class PropertyOrFieldSupport_getSimpleValue_with_Map_input_Test {
 
   private final PropertyOrFieldSupport underTest = PropertyOrFieldSupport.EXTRACTION;
-  private final AbstractMap<String, String> map = new HashMap<>();
+  private final AbstractMap<String, String> input = new HashMap<>();
 
   @Test
   void should_extract_property_value_even_if_map_key_matches_given_name() {
     // GIVEN
-    map.put("empty", "value"); // key clashes with AbstractMap#isEmpty()
+    input.put("empty", "value"); // key clashes with AbstractMap#isEmpty()
     // WHEN
-    Object value = underTest.getSimpleValue("empty", map);
+    Object value = underTest.getSimpleValue("empty", input);
     // THEN
     then(value).isInstanceOf(Boolean.class);
   }
@@ -48,9 +41,9 @@ void should_extract_property_value_even_if_map_key_matches_given_name() {
   @Test
   void should_extract_field_value_even_if_map_key_matches_given_name() {
     // GIVEN
-    map.put("keySet", "value"); // key clashes with AbstractMap#keySet
+    input.put("keySet", "value"); // key clashes with AbstractMap#keySet
     // WHEN
-    Object value = underTest.getSimpleValue("keySet", map);
+    Object value = underTest.getSimpleValue("keySet", input);
     // THEN
     then(value).isInstanceOf(Collection.class);
   }
@@ -62,9 +55,9 @@ void should_extract_field_value_even_if_map_key_matches_given_name() {
   })
   void should_extract_map_value_when_no_property_or_field_matches_given_name(String key, String expected) {
     // GIVEN
-    map.put(key, expected);
+    input.put(key, expected);
     // WHEN
-    Object value = underTest.getSimpleValue(key, map);
+    Object value = underTest.getSimpleValue(key, input);
     // THEN
     then(value).isEqualTo(expected);
   }
@@ -72,7 +65,7 @@ void should_extract_map_value_when_no_property_or_field_matches_given_name(Strin
   @Test
   void should_fail_when_given_name_is_not_found() {
     // WHEN
-    Throwable thrown = catchThrowable(() -> underTest.getSimpleValue("unknown", map));
+    Throwable thrown = catchThrowable(() -> underTest.getSimpleValue("unknown", input));
     // THEN
     then(thrown).isInstanceOf(IntrospectionError.class);
   }
diff --git a/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Optional_input_Test.java b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Optional_input_Test.java
new file mode 100644
--- /dev/null
+++ b/src/test/java/org/assertj/core/util/introspection/PropertyOrFieldSupport_getSimpleValue_with_Optional_input_Test.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ *
+ * Copyright 2012-2021 the original author or authors.
+ */
+package org.assertj.core.util.introspection;
+
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.util.NoSuchElementException;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+class PropertyOrFieldSupport_getSimpleValue_with_Optional_input_Test {
+
+  private final PropertyOrFieldSupport underTest = PropertyOrFieldSupport.EXTRACTION;
+
+  @Test
+  void should_return_optional_value_if_optional_is_not_empty() {
+    // GIVEN
+    Optional<?> input = Optional.of("string");
+    // WHEN
+    Object value = underTest.getSimpleValue("value", input);
+    // THEN
+    then(value).isEqualTo("string");
+  }
+
+  @Test
+  void should_fail_if_optional_is_empty() {
+    // GIVEN
+    Optional<?> input = Optional.empty();
+    // WHEN
+    Throwable thrown = catchThrowable(() -> underTest.getSimpleValue("value", input));
+    // THEN
+    then(thrown).isInstanceOf(NoSuchElementException.class);
+  }
+
+}