Merge branch 'master' into feature/5793-fsutil.getFeature-fails-when-called-with-object.class-on-multi-valued-features-v2
diff --git a/uimafit-core/pom.xml b/uimafit-core/pom.xml
index bc4095a..e485c32 100644
--- a/uimafit-core/pom.xml
+++ b/uimafit-core/pom.xml
@@ -70,6 +70,11 @@
       <version>1.5</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
 		<dependency>
 			<groupId>log4j</groupId>
 			<artifactId>log4j</artifactId>
diff --git a/uimafit-core/src/main/java/org/apache/uima/fit/util/FSUtil.java b/uimafit-core/src/main/java/org/apache/uima/fit/util/FSUtil.java
index fd62513..5e6feee 100644
--- a/uimafit-core/src/main/java/org/apache/uima/fit/util/FSUtil.java
+++ b/uimafit-core/src/main/java/org/apache/uima/fit/util/FSUtil.java
@@ -335,8 +335,10 @@
       }
     }
     
+    FeatureStructure value = aFS.getFeatureValue(aFeature);
+    
     // "null" case
-    if (aFS.getFeatureValue(aFeature) == null) {
+    if (value == null) {
       return null;
     }
     
@@ -346,59 +348,67 @@
     int length;
     
     // Handle case where feature is an array
-    if (aFeature.getRange().isArray()) {
-      CommonArrayFS source = (CommonArrayFS) aFS.getFeatureValue(aFeature);
+    if (value instanceof CommonArrayFS) {
+      // Shortcut if the user explicitly requests an array type
+      if (!Object.class.equals(aClazz) && aClazz.isAssignableFrom(value.getClass())) {
+        return (T) value;
+      }
+      
+      CommonArrayFS source = (CommonArrayFS) value;
       length = source.size();
-      switch (aFeature.getRange().getComponentType().getName()) {
-        case CAS.TYPE_NAME_BOOLEAN:
-          target = new boolean[length];
-          ((BooleanArrayFS) source).copyToArray(0, (boolean[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_BYTE:
-          target = new byte[length];
-          ((ByteArrayFS) source).copyToArray(0, (byte[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_DOUBLE:
-          target = new double[length];
-          ((DoubleArrayFS) source).copyToArray(0, (double[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_FLOAT:
-          target = new float[length];
-          ((FloatArrayFS) source).copyToArray(0, (float[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_INTEGER:
-          target = new int[length];
-          ((IntArrayFS) source).copyToArray(0, (int[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_LONG:
-          target = new long[length];
-          ((LongArrayFS) source).copyToArray(0, (long[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_SHORT:
-          target = new short[length];
-          ((ShortArrayFS) source).copyToArray(0, (short[]) target, 0, length);
-          break;
-        case CAS.TYPE_NAME_STRING:
-          target = new String[length];
-          ((StringArrayFS) source).copyToArray(0, (String[]) target, 0, length);
-          break;
-        default:
-          if (aClazz.isArray()) {
-            target = Array.newInstance(aClazz.getComponentType(), length);
-          }
-          else {
-            target = new FeatureStructure[length];
-          }
-          ((ArrayFS) source).copyToArray(0, (FeatureStructure[]) target, 0, length);
-          break;
+      if (value instanceof BooleanArrayFS) {
+        target = new boolean[length];
+        ((BooleanArrayFS) source).copyToArray(0, (boolean[]) target, 0, length);
+      }
+      else if (value instanceof ByteArrayFS) {
+        target = new byte[length];
+        ((ByteArrayFS) source).copyToArray(0, (byte[]) target, 0, length);
+      }
+      else if (value instanceof DoubleArrayFS) {
+        target = new double[length];
+        ((DoubleArrayFS) source).copyToArray(0, (double[]) target, 0, length);
+      }
+      else if (value instanceof FloatArrayFS) {
+        target = new float[length];
+        ((FloatArrayFS) source).copyToArray(0, (float[]) target, 0, length);
+      }
+      else if (value instanceof IntArrayFS) {
+        target = new int[length];
+        ((IntArrayFS) source).copyToArray(0, (int[]) target, 0, length);
+      }
+      else if (value instanceof LongArrayFS) {
+        target = new long[length];
+        ((LongArrayFS) source).copyToArray(0, (long[]) target, 0, length);
+      }
+      else if (value instanceof ShortArrayFS) {
+        target = new short[length];
+        ((ShortArrayFS) source).copyToArray(0, (short[]) target, 0, length);
+      }
+      else if (value instanceof StringArrayFS) {
+        target = new String[length];
+        ((StringArrayFS) source).copyToArray(0, (String[]) target, 0, length);
+      }
+      else {
+        if (aClazz.isArray()) {
+          target = Array.newInstance(aClazz.getComponentType(), length);
+        }
+        else {
+          target = new FeatureStructure[length];
+        }
+        ((ArrayFS) source).copyToArray(0, (FeatureStructure[]) target, 0, length);
       }
     }
     // Handle case where feature is a list
     else if (isListType(aFS.getCAS().getTypeSystem(), aFeature.getRange())) {
+      // Shortcut if the user explicitly requests a list type
+      if (!Object.class.equals(aClazz) && aClazz.isAssignableFrom(value.getClass())) {
+        return (T) value;
+      }
+      
       // Get length of list
       length = 0;
       {
-        FeatureStructure cur = aFS.getFeatureValue(aFeature);
+        FeatureStructure cur = value;
         // We assume to by facing a non-empty element if it has a "head" feature
         while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
           length++;
@@ -410,7 +420,7 @@
         case CAS.TYPE_NAME_FLOAT_LIST: {
           float[] floatTarget = new float[length];
           int i = 0;
-          FeatureStructure cur = aFS.getFeatureValue(aFeature);
+          FeatureStructure cur = value;
           // We assume to by facing a non-empty element if it has a "head" feature
           while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
             floatTarget[i] = cur.getFloatValue(cur.getType().getFeatureByBaseName(
@@ -424,7 +434,7 @@
         case CAS.TYPE_NAME_INTEGER_LIST: {
           int[] intTarget = new int[length];
           int i = 0;
-          FeatureStructure cur = aFS.getFeatureValue(aFeature);
+          FeatureStructure cur = value;
           // We assume to by facing a non-empty element if it has a "head" feature
           while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
             intTarget[i] = cur.getIntValue(cur.getType().getFeatureByBaseName(
@@ -438,7 +448,7 @@
         case CAS.TYPE_NAME_STRING_LIST: {
           String[] stringTarget = new String[length];
           int i = 0;
-          FeatureStructure cur = aFS.getFeatureValue(aFeature);
+          FeatureStructure cur = value;
           // We assume to by facing a non-empty element if it has a "head" feature
           while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
             stringTarget[i] = cur.getStringValue(cur.getType().getFeatureByBaseName(
@@ -456,7 +466,7 @@
             target = new FeatureStructure[length];
           }
           int i = 0;
-          FeatureStructure cur = aFS.getFeatureValue(aFeature);
+          FeatureStructure cur = value;
           // We assume to by facing a non-empty element if it has a "head" feature
           while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
             Array.set(target, i,
@@ -470,9 +480,12 @@
         }
       }
     }    
+    else if (aClazz.isAssignableFrom(value.getClass())) {
+      return (T) value;
+    }
     else if (aFS.getCAS().getTypeSystem()
               .subsumes(CasUtil.getType(aFS.getCAS(), aClazz), aFeature.getRange())) {
-      return (T) aFS.getFeatureValue(aFeature);
+      return (T) value;
     }    
     else {
       throw new IllegalArgumentException("Unable to coerce value of feature [" + aFeature.getName()
@@ -484,39 +497,45 @@
       return aClazz.cast(target);
     }
     
+    // Handle case where return value is Object
+    Class targetClass = aClazz;
+    if (Object.class.equals(aClazz)) {
+      targetClass = List.class;
+    }
+    
     // Handle case where return value is a collection
-    if (Collection.class.isAssignableFrom(aClazz)) {
+    if (Collection.class.isAssignableFrom(targetClass)) {
       Collection targetCollection;
       
-      if (aClazz.isInterface()) {
+      if (targetClass.isInterface()) {
         // If the target is an interface, try using a default implementation;
-        if (List.class.isAssignableFrom(aClazz)) {
+        if (List.class.isAssignableFrom(targetClass)) {
           targetCollection = new ArrayList(length);
         }
-        else if (Set.class.isAssignableFrom(aClazz)) {
+        else if (Set.class.isAssignableFrom(targetClass)) {
           targetCollection = new HashSet(length);
         }
         else {
           throw new IllegalArgumentException("Unable to coerce value of feature [" + aFeature.getName()
-                  + "] with type [" + aFeature.getRange().getName() + "] into [" + aClazz.getName() + "]");
+                  + "] with type [" + aFeature.getRange().getName() + "] into [" + targetClass.getName() + "]");
         }
       }
       else {
         // Try to instantiate using 0-args constructor
         try {
-          targetCollection = (Collection) aClazz.newInstance();
+          targetCollection = (Collection) targetClass.newInstance();
         } catch (InstantiationException | IllegalAccessException e) {
           throw new IllegalArgumentException("Unable to coerce value of feature [" + aFeature.getName()
-                  + "] with type [" + aFeature.getRange().getName() + "] into [" + aClazz.getName() + "]", e);
+                  + "] with type [" + aFeature.getRange().getName() + "] into [" + targetClass.getName() + "]", e);
         }
       }
       for (int i = 0; i < length; i++) {
         targetCollection.add(Array.get(target, i));
       }
-      return aClazz.cast(targetCollection);
+      return (T) targetClass.cast(targetCollection);
     }
     
     throw new IllegalArgumentException("Unable to coerce value of feature [" + aFeature.getName()
-            + "] with type [" + aFeature.getRange().getName() + "] into [" + aClazz.getName() + "]");
+            + "] with type [" + aFeature.getRange().getName() + "] into [" + targetClass.getName() + "]");
   }
 }
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/FSUtilTest.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/FSUtilTest.java
index c49cc69..a611985 100644
--- a/uimafit-core/src/test/java/org/apache/uima/fit/util/FSUtilTest.java
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/FSUtilTest.java
@@ -21,6 +21,7 @@
 import static java.util.Arrays.asList;
 import static org.apache.uima.fit.util.FSUtil.getFeature;
 import static org.apache.uima.fit.util.FSUtil.setFeature;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -30,11 +31,15 @@
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.ArrayFS;
 import org.apache.uima.cas.CAS;
 import org.apache.uima.cas.FeatureStructure;
 import org.apache.uima.cas.Type;
 import org.apache.uima.cas.impl.AnnotationImpl;
+import org.apache.uima.cas.impl.ArrayFSImpl;
 import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.jcas.cas.FSArray;
+import org.apache.uima.jcas.cas.TOP;
 import org.apache.uima.jcas.tcas.Annotation;
 import org.apache.uima.resource.metadata.TypeDescription;
 import org.apache.uima.resource.metadata.TypeSystemDescription;
@@ -95,44 +100,80 @@
     
     Type annotationType = cas.getTypeSystem().getType(CAS.TYPE_NAME_ANNOTATION);
 
-    FeatureStructure fs = cas.createFS(cas.getTypeSystem().getType("MyType"));
+    final FeatureStructure fs = cas.createFS(cas.getTypeSystem().getType("MyType"));
+    
     setFeature(fs, "BooleanValue", true);
     assertEquals(true, getFeature(fs, "BooleanValue", Boolean.class));
+    assertEquals(true, getFeature(fs, "BooleanValue", Object.class));
+    assertThat(getFeature(fs, "BooleanValue", Object.class).getClass()).isEqualTo(Boolean.class);
 
     setFeature(fs, "ByteValue", (byte) 1);
     assertTrue(((byte) 1) == getFeature(fs, "ByteValue", Byte.class));
+    assertTrue(((byte) 1) == (byte) getFeature(fs, "ByteValue", Object.class));
+    assertThat(getFeature(fs, "ByteValue", Object.class).getClass()).isEqualTo(Byte.class);
 
     setFeature(fs, "DoubleValue", 1d);
     assertTrue(1d == getFeature(fs, "DoubleValue", Double.class));
+    assertTrue(1d == (double) getFeature(fs, "DoubleValue", Object.class));
+    assertThat(getFeature(fs, "DoubleValue", Object.class).getClass()).isEqualTo(Double.class);
     
     setFeature(fs, "FloatValue", 1f);
     assertTrue(1f == getFeature(fs, "FloatValue", Float.class));
+    assertTrue(1f == (float) getFeature(fs, "FloatValue", Object.class));
+    assertThat(getFeature(fs, "FloatValue", Object.class).getClass()).isEqualTo(Float.class);
 
     setFeature(fs, "IntegerValue", 1);
     assertTrue(1 == getFeature(fs, "IntegerValue", Integer.class));
+    assertTrue(1 == (int) getFeature(fs, "IntegerValue", Object.class));
+    assertThat(getFeature(fs, "IntegerValue", Object.class).getClass()).isEqualTo(Integer.class);
     
     setFeature(fs, "LongValue", 1l);
     assertTrue(1l == getFeature(fs, "LongValue", Long.class));
+    assertTrue(1l == (long) getFeature(fs, "LongValue", Object.class));
+    assertThat(getFeature(fs, "LongValue", Object.class).getClass()).isEqualTo(Long.class);
     
     setFeature(fs, "ShortValue", (short) 1);
     assertTrue(((short) 1) == getFeature(fs, "ShortValue", Short.class));
+    assertTrue(((short) 1) == (short) getFeature(fs, "ShortValue", Short.class));
+    assertThat(getFeature(fs, "ShortValue", Object.class).getClass()).isEqualTo(Short.class);
     
     setFeature(fs, "StringValue", "set");
     assertEquals("set", getFeature(fs, "StringValue", String.class));
+    assertEquals("set", getFeature(fs, "StringValue", Object.class));
+    assertThat(getFeature(fs, "StringValue", Object.class).getClass()).isEqualTo(String.class);
     
-    setFeature(fs, "TopValue", cas.createArrayFS(1));
+    final ArrayFS arrayArg = cas.createArrayFS(1);
+    final AnnotationFS arrayElem = cas.createAnnotation(annotationType, 0, 1);
+    arrayArg.set(0, arrayElem);
+    setFeature(fs, "TopValue", arrayArg);
+    assertThat(getFeature(fs, "TopValue", FeatureStructure.class)).isEqualTo(arrayArg);
+    assertThat(getFeature(fs, "TopValue", List.class)).containsExactly(arrayElem);
+    assertThat((List) getFeature(fs, "TopValue", Object.class)).containsExactly(arrayElem);
+    assertThat(getFeature(fs, "TopValue", ArrayFS.class).toArray())
+            .containsExactly(arrayElem);
+    if (aActivateJCas) {
+      assertThat(getFeature(fs, "TopValue", TOP.class).getClass())
+              .isEqualTo(FSArray.class);
+    }
+    else {
+      assertThat(getFeature(fs, "TopValue", FeatureStructure.class).getClass())
+              .isEqualTo(ArrayFSImpl.class);
+    }
 
-    setFeature(fs, "AnnotationValue", cas.createAnnotation(annotationType, 0, 1));
+    final AnnotationFS annVal = cas.createAnnotation(annotationType, 0, 1);
+    setFeature(fs, "AnnotationValue", annVal);
     if (aActivateJCas) {
       assertEquals(Annotation.class.getName(),
               getFeature(fs, "AnnotationValue", FeatureStructure.class).getClass().getName());
+      assertThat(getFeature(fs, "AnnotationValue", TOP.class)).isEqualTo(annVal);
     }
     else {
       assertEquals(AnnotationImpl.class.getName(),
               getFeature(fs, "AnnotationValue", FeatureStructure.class).getClass().getName());
     }
-    assertEquals(0, getFeature(fs, "AnnotationValue", AnnotationFS.class).getBegin());
-    assertEquals(1, getFeature(fs, "AnnotationValue", AnnotationFS.class).getEnd());
+    assertThat(getFeature(fs, "AnnotationValue", AnnotationFS.class)).isEqualTo(annVal);
+    assertThat(getFeature(fs, "AnnotationValue", FeatureStructure.class)).isEqualTo(annVal);
+    assertThat(getFeature(fs, "AnnotationValue", Object.class)).isEqualTo(annVal);
 
     setFeature(fs, "BooleanArrayValue", (boolean[]) null);
     assertEquals(null, getFeature(fs, "BooleanArrayValue", boolean[].class));
@@ -144,15 +185,28 @@
     assertEquals(0, getFeature(fs, "BooleanArrayValue", boolean[].class).length);
 
     setFeature(fs, "BooleanArrayValue", true);
-    assertEquals(true, getFeature(fs, "BooleanArrayValue", boolean[].class)[0]);
+    assertThat(getFeature(fs, "BooleanArrayValue", boolean[].class)).containsExactly(true);
+    assertThat(getFeature(fs, "BooleanArrayValue", List.class)).containsExactly(true);
+    assertThat((List<Boolean>) getFeature(fs, "BooleanArrayValue", Object.class))
+            .containsExactly(true);
 
     setFeature(fs, "BooleanArrayValue", true, false);
-    assertEquals(true, getFeature(fs, "BooleanArrayValue", boolean[].class)[0]);
-    assertEquals(false, getFeature(fs, "BooleanArrayValue", boolean[].class)[1]);
-    
+    assertThat(getFeature(fs, "BooleanArrayValue", boolean[].class)).containsExactly(true, false);
+    assertThat(getFeature(fs, "BooleanArrayValue", List.class)).containsExactly(true, false);
+    assertThat((List<Boolean>) getFeature(fs, "BooleanArrayValue", Object.class))
+            .containsExactly(true, false);
+
     setFeature(fs, "BooleanArrayValue", new boolean[] { true, false });
-    assertEquals(true, getFeature(fs, "BooleanArrayValue", boolean[].class)[0]);
-    assertEquals(false, getFeature(fs, "BooleanArrayValue", boolean[].class)[1]);
+    assertThat(getFeature(fs, "BooleanArrayValue", boolean[].class)).containsExactly(true, false);
+    assertThat(getFeature(fs, "BooleanArrayValue", List.class)).containsExactly(true, false);
+    assertThat((List<Boolean>) getFeature(fs, "BooleanArrayValue", Object.class))
+            .containsExactly(true, false);
+
+    setFeature(fs, "BooleanArrayValue", asList(true, false));
+    assertThat(getFeature(fs, "BooleanArrayValue", boolean[].class)).containsExactly(true, false);
+    assertThat(getFeature(fs, "BooleanArrayValue", List.class)).containsExactly(true, false);
+    assertThat((List<Boolean>) getFeature(fs, "BooleanArrayValue", Object.class))
+            .containsExactly(true, false);
     
     setFeature(fs, "ByteArrayValue", new byte[] { 0, 1 });
     setFeature(fs, "DoubleArrayValue", new double[] { 0d, 1d });
@@ -163,15 +217,25 @@
     setFeature(fs, "StringArrayValue", new String[] { "one", "two" });
     setFeature(fs, "TopArrayValue", cas.createArrayFS(1), cas.createDoubleArrayFS(1));
 
-    setFeature(fs, "AnnotationArrayValue", cas.createAnnotation(annotationType, 0, 1), 
-            cas.createAnnotation(annotationType, 1, 2));
-    assertEquals(0, getFeature(fs, "AnnotationArrayValue", AnnotationFS[].class)[0].getBegin());
-    assertEquals(1, getFeature(fs, "AnnotationArrayValue", AnnotationFS[].class)[1].getBegin());
+    final AnnotationFS arg1 = cas.createAnnotation(annotationType, 0, 1);
+    final AnnotationFS arg2 = cas.createAnnotation(annotationType, 1, 2);
+    setFeature(fs, "AnnotationArrayValue", arg1, arg2);
+    assertThat(getFeature(fs, "AnnotationArrayValue", AnnotationFS[].class)).containsExactly(arg1,
+            arg2);
+    assertThat(getFeature(fs, "AnnotationArrayValue", ArrayFS.class).toArray())
+            .containsExactly(arg1, arg2);
+    if (aActivateJCas) {
+      assertThat(getFeature(fs, "AnnotationArrayValue", TOP.class).getClass())
+              .isEqualTo(FSArray.class);
+    }
+    else {
+      assertThat(getFeature(fs, "AnnotationArrayValue", FeatureStructure.class).getClass())
+              .isEqualTo(ArrayFSImpl.class);
+    }
+    assertThat(getFeature(fs, "AnnotationArrayValue", List.class)).containsExactly(arg1, arg2);
+    assertThat((List) getFeature(fs, "AnnotationArrayValue", Object.class)).containsExactly(arg1,
+            arg2);
 
-    setFeature(fs, "BooleanArrayValue", asList(true, false));
-    assertEquals(true, getFeature(fs, "BooleanArrayValue", List.class).get(0));
-    assertEquals(false, getFeature(fs, "BooleanArrayValue", List.class).get(1));
-    
     setFeature(fs, "ByteArrayValue", asList((byte) 0, (byte) 1));
     setFeature(fs, "DoubleArrayValue", asList(0d, 1d));
     setFeature(fs, "FloatArrayValue", asList(0f, 1f));
diff --git a/uimafit-parent/pom.xml b/uimafit-parent/pom.xml
index 92df0b4..4484a60 100644
--- a/uimafit-parent/pom.xml
+++ b/uimafit-parent/pom.xml
@@ -70,6 +70,11 @@
         <version>4.12</version>
       </dependency>
       <dependency>
+        <groupId>org.assertj</groupId>
+        <artifactId>assertj-core</artifactId>
+        <version>3.10.0</version>
+      </dependency>
+      <dependency>
         <groupId>commons-lang</groupId>
         <artifactId>commons-lang</artifactId>
         <version>2.6</version>