Repository: marklogic/java-client-api
Issue: rowRecord.getString("firstName") returns ...OkHttpServices$OkHttpServiceResult@4dd663d5

Where I expected to get the value of the firstName column extracted by xpath, I'm getting an OkHttpServiceResult object, which has an undesirable toString.  Notice the "firstName: com.marklogic.client.impl.OkHttpServices$OkHttpServiceResult@1b2283a" in the output below.  Can you help me see what I'm doing wrong?
 
With the following Java:

```
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marklogic.client.DatabaseClient;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.DatabaseClientFactory.DigestAuthContext;
import com.marklogic.client.document.DocumentManager;
import com.marklogic.client.expression.PlanBuilder;
import com.marklogic.client.expression.PlanBuilder.Plan;
import com.marklogic.client.io.DocumentMetadataHandle;
import com.marklogic.client.row.RowManager;
import com.marklogic.client.row.RowRecord;
import com.marklogic.client.type.CtsReferenceExpr;
import com.marklogic.client.type.PlanExprCol;

import java.util.HashMap;

public class Optic2 {
  public static void main(String[] args) throws Exception {
    DatabaseClient client = DatabaseClientFactory.newClient("engrlab-129-192.engrlab.marklogic.com", 8000, new DigestAuthContext("admin", "admin"));
    DocumentManager docMgr = client.newDocumentManager();

    ObjectMapper mapper = new ObjectMapper()
      .configure(Feature.ALLOW_SINGLE_QUOTES, true);
    docMgr.writeAs("/optic/test/musician1.json", mapper.readTree(
      "{'musician':{" +
      "  'lastName':'Armstrong', 'firstName':'Louis', 'dob':'1901-08-04', 'instrument':['trumpet', 'vocal']" +
      "}}"));
    docMgr.writeAs("/optic/test/musician2.json", mapper.readTree(
      "{'musician':{" +
      "  'lastName':'Byron', 'firstName':'Don', 'dob':'1958-11-08', 'instrument':['clarinet', 'saxophone']" +
      "}}"));
    docMgr.writeAs("/optic/test/musician3.json", mapper.readTree(
      "{'musician':{" +
      "  'lastName':'Coltrane', 'firstName':'John', 'dob':'1926-09-23', 'instrument':['saxophone']" +
      "}}"));
    docMgr.writeAs("/optic/test/musician4.json", mapper.readTree(
      "{'musician':{" +
      "  'lastName':'Davis', 'firstName':'Miles', 'dob':'1926-05-26', 'instrument':['trumpet']" +
      "}}"));

    RowManager rowMgr = client.newRowManager();
    PlanBuilder p = rowMgr.newPlanBuilder();

    HashMap<String,CtsReferenceExpr> uriLexicon =
      new HashMap<String,CtsReferenceExpr>();
    uriLexicon.put("uri", p.cts.uriReference());
    String[] uris = new String[] {"/optic/test/musician1.json",
      "/optic/test/musician2.json", "/optic/test/musician3.json" };
    PlanExprCol[] columns = new PlanExprCol[] {p.as("firstName", p.xpath("doc", "/musician/firstName"))};
    Plan plan =
      p.fromLexicons(uriLexicon)
       .where(p.cts.documentQuery(p.xs.stringSeq(uris)))
       .joinDoc(p.col("doc"), p.col("uri"))
       .select(columns);
    for (RowRecord row: rowMgr.resultRows(plan)) {
      System.out.println("firstName: " + row.getString("firstName") +
                         ", uri: " + row.getString("uri"));
    }
  }
}
```

I get the following output:
```
09:34:41.599 [main] DEBUG c.m.client.impl.OkHttpServices - Connecting to engrlab-129-192.engrlab.marklogic.com at 8000 as admin
09:34:45.563 [main] INFO  c.m.client.impl.DocumentManagerImpl - Writing content for /optic/test/musician1.json
09:34:45.575 [main] DEBUG c.m.client.impl.OkHttpServices - Sending /optic/test/musician1.json document in transaction null
09:34:46.059 [main] INFO  c.m.client.impl.DocumentManagerImpl - Writing content for /optic/test/musician2.json
09:34:46.059 [main] DEBUG c.m.client.impl.OkHttpServices - Sending /optic/test/musician2.json document in transaction null
09:34:46.207 [main] INFO  c.m.client.impl.DocumentManagerImpl - Writing content for /optic/test/musician3.json
09:34:46.207 [main] DEBUG c.m.client.impl.OkHttpServices - Sending /optic/test/musician3.json document in transaction null
09:34:46.350 [main] INFO  c.m.client.impl.DocumentManagerImpl - Writing content for /optic/test/musician4.json
09:34:46.351 [main] DEBUG c.m.client.impl.OkHttpServices - Sending /optic/test/musician4.json document in transaction null
09:34:47.150 [main] DEBUG c.m.client.impl.OkHttpServices - Posting rows
firstName: com.marklogic.client.impl.OkHttpServices$OkHttpServiceResult@1b2283a, uri: /optic/test/musician1.json
firstName: com.marklogic.client.impl.OkHttpServices$OkHttpServiceResult@a637e7, uri: /optic/test/musician2.json
firstName: com.marklogic.client.impl.OkHttpServices$OkHttpServiceResult@1e7aac8, uri: /optic/test/musician3.json
```

And the following HTTP wire trace for the optic query:
```
POST /v1/rows?output=object&column-types=rows&node-columns=reference&row-format=json HTTP/1.1
ML-Agent-ID: java
Accept: multipart/mixed; boundary=60571458-ff42-4838-8518-8daf9382c4e5
Authorization: Digest username="admin", realm="public", nonce="35c963f0f9cc36:giWRHfuTfU3ag/QKF6Z3eA==", uri="/v1/rows?output=object&column-types=rows&node-columns=reference&row-format=json", response="649bf6fd6f0155494696de6d47b2b32a", qop=auth, nc=00000005, cnonce="5456ce49757347d8", algorithm=MD5, opaque="760201d04a9987db"
Content-Type: application/json
Transfer-Encoding: chunked
Host: engrlab-129-192.engrlab.marklogic.com:8000
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.9.0

40c
{"$optic":{"ns":"op", "fn":"operators", "args":[{"ns":"op", "fn":"from-lexicons", "args":[{"uri":{"ns":"cts", "fn":"uri-reference", "args":[]}}]}, {"ns":"op", "fn":"where", "args":[{"ns":"cts", "fn":"document-query", "args":[[{"ns":"xs", "fn":"string", "args":["/optic/test/musician1.json"]}, {"ns":"xs", "fn":"string", "args":["/optic/test/musician2.json"]}, {"ns":"xs", "fn":"string", "args":["/optic/test/musician3.json"]}]]}]}, {"ns":"op", "fn":"join-doc", "args":[{"ns":"op", "fn":"col", "args":[{"ns":"xs", "fn":"string", "args":["doc"]}]}, {"ns":"op", "fn":"col", "args":[{"ns":"xs", "fn":"string", "args":["uri"]}]}]}, {"ns":"op", "fn":"select", "args":[[{"ns":"op", "fn":"col", "args":[{"ns":"xs", "fn":"string", "args":["uri"]}]}, {"ns":"op", "fn":"as", "args":[{"ns":"op", "fn":"col", "args":[{"ns":"xs", "fn":"string", "args":["firstName"]}]}, {"ns":"op", "fn":"xpath", "args":[{"ns":"op", "fn":"col", "args":[{"ns":"xs", "fn":"string", "args":["doc"]}]}, {"ns":"xs", "fn":"string", "args":["/musician/firstName"]}]}]}]]}]}}
0

HTTP/1.1 200 OK
ML-Effective-Timestamp: 15139604854019900
Content-type: multipart/mixed; boundary=60571458-ff42-4838-8518-8daf9382c4e5
Server: MarkLogic
Content-Length: 1473
Connection: Keep-Alive
Keep-Alive: timeout=5

--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: application/json; charset=utf-8
Content-Disposition: inline; kind=columns

{"columns":[{"name":"uri"},{"name":"firstName"}]}
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: application/json; charset=utf-8
Content-Disposition: inline; kind=row

{"uri":{"type":"xs:string","value":"/optic/test/musician1.json"},"firstName":{"type":"cid","value":"cid:firstName[0]"}}
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: text/plain; charset=utf-8
Content-ID: <firstName[0]>
Content-Disposition: inline; kind=row-attachment

Louis
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: application/json; charset=utf-8
Content-Disposition: inline; kind=row

{"uri":{"type":"xs:string","value":"/optic/test/musician2.json"},"firstName":{"type":"cid","value":"cid:firstName[1]"}}
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: text/plain; charset=utf-8
Content-ID: <firstName[1]>
Content-Disposition: inline; kind=row-attachment

Don
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: application/json; charset=utf-8
Content-Disposition: inline; kind=row

{"uri":{"type":"xs:string","value":"/optic/test/musician3.json"},"firstName":{"type":"cid","value":"cid:firstName[2]"}}
--60571458-ff42-4838-8518-8daf9382c4e5
Content-Type: text/plain; charset=utf-8
Content-ID: <firstName[2]>
Content-Disposition: inline; kind=row-attachment

John
--60571458-ff42-4838-8518-8daf9382c4e5--
```

Changed files:
src/main/java/com/marklogic/client/impl/RowManagerImpl.java, src/test/java/com/marklogic/client/test/RowManagerTest.java

diff --git a/src/main/java/com/marklogic/client/impl/RowManagerImpl.java b/src/main/java/com/marklogic/client/impl/RowManagerImpl.java
index 4b81e3ee8..f2fd3f4b2 100644
--- a/src/main/java/com/marklogic/client/impl/RowManagerImpl.java
+++ b/src/main/java/com/marklogic/client/impl/RowManagerImpl.java
@@ -50,6 +50,7 @@ import com.marklogic.client.impl.RESTServices.RESTServiceResultIterator;
 import com.marklogic.client.io.BaseHandle;
 import com.marklogic.client.io.Format;
 import com.marklogic.client.io.InputStreamHandle;
+import com.marklogic.client.io.StringHandle;
 import com.marklogic.client.io.XMLStreamReaderHandle;
 import com.marklogic.client.io.marker.AbstractReadHandle;
 import com.marklogic.client.io.marker.AbstractWriteHandle;
@@ -1094,7 +1095,11 @@ public class RowManagerImpl
     }
     @Override
     public String getString(String columnName) {
-      return asString(get(columnName));
+      try {
+        return asString(get(columnName));
+      } catch(NodeNotAStringException e) {
+        throw new IllegalArgumentException("value for column \\""+columnName+"\\" not a string");
+      }
     }
 
     private boolean asBoolean(String columnName, Object value) {
@@ -1139,10 +1144,22 @@ public class RowManagerImpl
       }
       throw new IllegalStateException("column "+columnName+" does not have a short value");
     }
-    private String asString(Object value) {
+    /**
+     * @throws NodeNotAStringException when the value is a node but not a single text node
+     *   (with content-type "text/plain")
+     */
+    private String asString(Object value) throws NodeNotAStringException {
       if (value == null || value instanceof String) {
         return (String) value;
       }
+      if (value instanceof RESTServiceResult) {
+        RESTServiceResult result = (RESTServiceResult) value;
+        if ( result.getMimetype() != null && result.getMimetype().startsWith("text/plain") ) {
+          return result.getContent(new StringHandle()).get();
+        } else {
+          throw new NodeNotAStringException();
+        }
+      }
       return value.toString();
     }
 
@@ -1176,27 +1193,27 @@ public class RowManagerImpl
 			}
 			*/
 
-      String valueStr = asString(value);
+      try {
+        String valueStr = asString(value);
 
-      Function<String,? extends XsAnyAtomicTypeVal> factory = getFactory(as);
-      if (factory != null) {
-        return as.cast(factory.apply(valueStr));
-      }
+        Function<String,? extends XsAnyAtomicTypeVal> factory = getFactory(as);
+        if (factory != null) {
+          return as.cast(factory.apply(valueStr));
+        }
 
-      // fallback
-      @SuppressWarnings("unchecked")
-      Constructor<T> constructor = (Constructor<T>) constructors.get(as);
-      if (constructor == null) {
-        try {
+        // fallback
+        @SuppressWarnings("unchecked")
+        Constructor<T> constructor = (Constructor<T>) constructors.get(as);
+        if (constructor == null) {
           constructor = as.getConstructor(String.class);
-        } catch(NoSuchMethodException e) {
-          throw new IllegalArgumentException("cannot construct "+columnName+" value as class: "+as.getName());
+          constructors.put(as, constructor);
         }
-        constructors.put(as, constructor);
-      }
 
-      try {
         return constructor.newInstance(valueStr);
+      } catch(NodeNotAStringException e) {
+        throw new IllegalArgumentException("column \\""+columnName+"\\" is a node, not an atomic");
+      } catch(NoSuchMethodException e) {
+        throw new IllegalArgumentException("cannot construct "+columnName+" value as class: "+as.getName());
       } catch (InstantiationException e) {
         throw new MarkLogicBindingException("could not construct value as class: "+as.getName(), e);
       } catch (IllegalAccessException e) {
@@ -1532,4 +1549,7 @@ public class RowManagerImpl
       return this;
     }
   }
+
+  private static class NodeNotAStringException extends Exception {
+  }
 }
diff --git a/src/test/java/com/marklogic/client/test/RowManagerTest.java b/src/test/java/com/marklogic/client/test/RowManagerTest.java
index f23f362ea..bc3ba2f02 100644
--- a/src/test/java/com/marklogic/client/test/RowManagerTest.java
+++ b/src/test/java/com/marklogic/client/test/RowManagerTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.IOException;
 import java.io.LineNumberReader;
@@ -451,15 +452,25 @@ public class RowManagerTest {
     String[] firstName = {"Louis",      "Miles"};
 
     int rowNum = 0;
+    boolean didARowFail = false;
     for (RowRecord row: rowMgr.resultRows(builtPlan)) {
       assertEquals("unexpected lastName value in row record "+rowNum,  lastName[rowNum],  row.getString("lastName"));
       assertEquals("unexpected firstName value in row record "+rowNum, firstName[rowNum], row.getString("firstName"));
 
-      String instruments = row.getContent("instruments", new StringHandle()).get();
+      String instruments;
+      try {
+        instruments = row.getString("instruments");
+      } catch (IllegalArgumentException e ) {
+        didARowFail = true;
+        instruments = row.getContentAs("instruments", String.class);
+      }
       assertNotNull("null instrucments value in row record "+rowNum,    instruments);
       assertTrue("unexpected instrucments value in row record "+rowNum, instruments.contains("trumpet"));
       rowNum++;
     }
+    if ( didARowFail == false ) {
+      fail("getString(\\"instruments\\") should throw an exception since some rows return an array");
+    }
     assertEquals("unexpected count of result records", 2, rowNum);
   }
   @Test