Upgrade API: use base interface, return vanilla Future<Data>, throw exceptions instead of returning null, and improve underlying implementation/tests
diff --git a/src/main/java/com/intel/jndn/utils/Client.java b/src/main/java/com/intel/jndn/utils/Client.java
index cb90f6f..6b09f9f 100644
--- a/src/main/java/com/intel/jndn/utils/Client.java
+++ b/src/main/java/com/intel/jndn/utils/Client.java
@@ -14,86 +14,28 @@
 package com.intel.jndn.utils;
 
 import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-import java.util.logging.Level;
+import java.util.concurrent.Future;
 import net.named_data.jndn.Data;
 import net.named_data.jndn.Face;
 import net.named_data.jndn.Interest;
-import net.named_data.jndn.Name;
-import net.named_data.jndn.OnData;
-import net.named_data.jndn.OnTimeout;
-import java.util.logging.Logger;
 
 /**
- * Provide a client to simplify information retrieval over the NDN network.
+ * Base functionality provided by all NDN clients in this package.
  *
  * @author Andrew Brown <andrew.brown@intel.com>
  */
-public class Client {
-
-  public static final long DEFAULT_SLEEP_TIME = 20;
-  public static final long DEFAULT_TIMEOUT = 2000;
-  private static final Logger logger = Logger.getLogger(Client.class.getName());
-  private static Client defaultInstance;
-
-  /**
-   * Singleton access for simpler client use
-   *
-   * @return
-   */
-  public static Client getDefault() {
-    if (defaultInstance == null) {
-      defaultInstance = new Client();
-    }
-    return defaultInstance;
-  }
+public interface Client {
 
   /**
    * Asynchronously request the Data for an Interest. This will send the
    * Interest and return immediately; use futureData.get() to block until the
-   * Data returns (see FutureData) or manage the event processing independently.
+   * Data returns (see Future) or manage the event processing independently.
    *
    * @param face
    * @param interest
    * @return
    */
-  public FutureData getAsync(Face face, Interest interest) {
-    final FutureData futureData = new FutureData(face, interest.getName());
-
-    // send interest
-    try {
-      face.expressInterest(interest, new OnData() {
-        @Override
-        public void onData(Interest interest, Data data) {
-          futureData.resolve(data);
-        }
-      }, new OnTimeout() {
-        @Override
-        public void onTimeout(Interest interest) {
-          futureData.reject(new TimeoutException());
-        }
-      });
-    } catch (IOException e) {
-      logger.log(Level.WARNING, "IO failure while sending interest: ", e);
-      futureData.reject(e);
-    }
-
-    return futureData;
-  }
-
-  /**
-   * Synchronously retrieve the Data for a Name using a default interest (e.g. 2
-   * second timeout); this will block until complete (i.e. either data is
-   * received or the interest times out).
-   *
-   * @param face
-   * @param name
-   * @return
-   */
-  public FutureData getAsync(Face face, Name name) {
-    return getAsync(face, getDefaultInterest(name));
-  }
+  public Future<Data> getAsync(Face face, Interest interest);
 
   /**
    * Synchronously retrieve the Data for an Interest; this will block until
@@ -101,48 +43,8 @@
    *
    * @param face
    * @param interest
-   * @return Data packet or null
-   */
-  public Data getSync(Face face, Interest interest) {
-    // setup event
-    long startTime = System.currentTimeMillis();
-
-    // get future data
-    FutureData futureData = getAsync(face, interest);
-
-    // process eventCount until a response is received or timeout
-    try {
-      Data data = futureData.get();
-      logger.fine("Request time (ms): " + (System.currentTimeMillis() - startTime));
-      return data;
-    } catch (ExecutionException | InterruptedException e) {
-      logger.log(Level.WARNING, "Failed to retrieve data: ", e);
-      return null;
-    }
-  }
-
-  /**
-   * Synchronously retrieve the Data for a Name using a default interest (e.g. 2
-   * second timeout); this will block until complete (i.e. either data is
-   * received or the interest times out).
-   *
-   * @param face
-   * @param name
    * @return
+   * @throws java.io.IOException
    */
-  public Data getSync(Face face, Name name) {
-    return getSync(face, getDefaultInterest(name));
-  }
-
-  /**
-   * Create a default interest for a given Name using some common settings: -
-   * lifetime: 2 seconds
-   *
-   * @param name
-   * @return
-   */
-  public static Interest getDefaultInterest(Name name) {
-    Interest interest = new Interest(name, DEFAULT_TIMEOUT);
-    return interest;
-  }
+  public Data getSync(Face face, Interest interest) throws IOException;
 }
diff --git a/src/main/java/com/intel/jndn/utils/SegmentedClient.java b/src/main/java/com/intel/jndn/utils/SegmentedClient.java
index 38524ef..40ef7bb 100644
--- a/src/main/java/com/intel/jndn/utils/SegmentedClient.java
+++ b/src/main/java/com/intel/jndn/utils/SegmentedClient.java
@@ -13,11 +13,13 @@
  */
 package com.intel.jndn.utils;
 
-import java.io.ByteArrayOutputStream;
+import com.intel.jndn.utils.client.SegmentedFutureData;
+import com.intel.jndn.utils.client.FutureData;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import net.named_data.jndn.Data;
@@ -25,7 +27,6 @@
 import net.named_data.jndn.Interest;
 import net.named_data.jndn.Name;
 import net.named_data.jndn.encoding.EncodingException;
-import net.named_data.jndn.util.Blob;
 
 /**
  * Provide a client to simplify retrieving segmented Data packets over the NDN
@@ -38,14 +39,13 @@
  *
  * @author Andrew Brown <andrew.brown@intel.com>
  */
-public class SegmentedClient {
+public class SegmentedClient implements Client {
 
   private static SegmentedClient defaultInstance;
   private static final Logger logger = Logger.getLogger(SegmentedClient.class.getName());
-  private static final int SLEEP_TIME_MS = 10;
 
   /**
-   * Singleton access for simpler client use
+   * Singleton access for simpler client use.
    *
    * @return
    */
@@ -67,7 +67,37 @@
    * @return a list of FutureData packets; if the first segment fails, the list
    * will contain one FutureData with the failure exception
    */
-  public List<FutureData> getAsync(Face face, Interest interest) {
+  @Override
+  public Future<Data> getAsync(Face face, Interest interest) {
+    List<Future<Data>> segments = getAsyncList(face, interest);
+    return new SegmentedFutureData(interest.getName().getPrefix(-1), segments);
+  }
+
+ /**
+   * Asynchronously send Interest packets for a segmented result; will block
+   * until the first packet is received and then send remaining interests until
+   * the specified FinalBlockId.
+   *
+   * @param face
+   * @param name
+   * @return an aggregated data packet from all received segments
+   */
+  public Future<Data> getAsync(Face face, Name name) {
+    return getAsync(face, SimpleClient.getDefaultInterest(name));
+  }
+
+  /**
+   * Asynchronously send Interest packets for a segmented result; will block
+   * until the first packet is received and then send remaining interests until
+   * the specified FinalBlockId.
+   *
+   * @param face
+   * @param interest should include either a ChildSelector or an initial segment
+   * number
+   * @return a list of FutureData packets; if the first segment fails, the list
+   * will contain one FutureData with the failure exception
+   */
+  public List<Future<Data>> getAsyncList(Face face, Interest interest) {
     // get first segment; default 0 or use a specified start segment
     long firstSegment = 0;
     boolean specifiedSegment = false;
@@ -78,12 +108,13 @@
       // check for interest selector if no initial segment found
       if (interest.getChildSelector() == -1) {
         logger.log(Level.WARNING, "No child selector set for a segmented Interest; this may result in incorrect retrieval.");
+        // allow this interest to pass without a segment marker since it may still succeed
       }
     }
 
     // setup segments
-    final List<FutureData> segments = new ArrayList<>();
-    segments.add(Client.getDefault().getAsync(face, interest));
+    final List<Future<Data>> segments = new ArrayList<>();
+    segments.add(SimpleClient.getDefault().getAsync(face, interest));
 
     // retrieve first packet to find last segment value
     long lastSegment;
@@ -103,7 +134,7 @@
     for (long i = firstSegment + 1; i <= lastSegment; i++) {
       Interest segmentedInterest = new Interest(interest);
       segmentedInterest.getName().appendSegment(i);
-      FutureData futureData = Client.getDefault().getAsync(face, segmentedInterest);
+      Future<Data> futureData = SimpleClient.getDefault().getAsync(face, segmentedInterest);
       segments.add((int) i, futureData);
     }
 
@@ -120,8 +151,8 @@
    * @param name
    * @return
    */
-  public List<FutureData> getAsync(Face face, Name name) {
-    return getAsync(face, Client.getDefaultInterest(name));
+  public List<Future<Data>> getAsyncList(Face face, Name name) {
+    return getAsyncList(face, SimpleClient.getDefaultInterest(name));
   }
 
   /**
@@ -132,40 +163,18 @@
    * @param interest should include either a ChildSelector or an initial segment
    * number
    * @return a Data packet; the name will inherit from the sent Interest, not
-   * the returned packets (TODO or should we parse from returned Data? copy
-   * first Data?) and the content will be a concatenation of all of the packet
-   * contents.
+   * the returned packets and the content will be a concatenation of all of the
+   * packet contents.
+   * @throws java.io.IOException
    */
-  public Data getSync(Face face, Interest interest) {
-    List<FutureData> segments = getAsync(face, interest);
-
-    // process events until complete
-    while (!isSegmentListComplete(segments)) {
-      try {
-        face.processEvents();
-        Thread.sleep(SLEEP_TIME_MS);
-      } catch (EncodingException | IOException e) {
-        logger.log(Level.WARNING, "Failed to retrieve data: ", e);
-        return null;
-      } catch (InterruptedException ex) {
-        // do nothing
-      }
+  @Override
+  public Data getSync(Face face, Interest interest) throws IOException {  
+    try {
+      return getAsync(face, interest).get();
+    } catch (ExecutionException | InterruptedException e) {
+      logger.log(Level.WARNING, "Failed to retrieve data.", e);
+      throw new IOException("Failed to retrieve data.", e);
     }
-
-    // build final blob
-    ByteArrayOutputStream content = new ByteArrayOutputStream();
-    for (FutureData futureData : segments) {
-      try {
-        content.write(futureData.get().getContent().getImmutableArray());
-      } catch (ExecutionException | IOException | InterruptedException e) {
-        logger.log(Level.WARNING, "Failed to parse retrieved data: ", e);
-        return null;
-      }
-    }
-
-    Data data = new Data(interest.getName()); // TODO this name may not be correct; may need to contain additional suffixes
-    data.setContent(new Blob(content.toByteArray()));
-    return data;
   }
 
   /**
@@ -177,9 +186,10 @@
    * @param face
    * @param name
    * @return
+   * @throws java.io.IOException
    */
-  public Data getSync(Face face, Name name) {
-    return getSync(face, Client.getDefaultInterest(name));
+  public Data getSync(Face face, Name name) throws IOException {
+    return getSync(face, SimpleClient.getDefaultInterest(name));
   }
 
   /**
@@ -193,19 +203,4 @@
   public static boolean hasSegment(Name name) {
     return name.get(-1).getValue().buf().get(0) == 0x00;
   }
-
-  /**
-   * Check if a list of segments have returned from the network.
-   *
-   * @param segments
-   * @return
-   */
-  protected boolean isSegmentListComplete(List<FutureData> segments) {
-    for (FutureData futureData : segments) {
-      if (!futureData.isDone()) {
-        return false;
-      }
-    }
-    return true;
-  }
 }
diff --git a/src/main/java/com/intel/jndn/utils/SimpleClient.java b/src/main/java/com/intel/jndn/utils/SimpleClient.java
new file mode 100644
index 0000000..a751f8e
--- /dev/null
+++ b/src/main/java/com/intel/jndn/utils/SimpleClient.java
@@ -0,0 +1,145 @@
+/*
+ * jndn-utils
+ * Copyright (c) 2015, Intel Corporation.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 3, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ */
+package com.intel.jndn.utils;
+
+import com.intel.jndn.utils.client.FutureData;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Face;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.OnData;
+import net.named_data.jndn.OnTimeout;
+import java.util.logging.Logger;
+
+/**
+ * Provide a client to simplify information retrieval over the NDN network.
+ *
+ * @author Andrew Brown <andrew.brown@intel.com>
+ */
+public class SimpleClient implements Client {
+
+  public static final long DEFAULT_SLEEP_TIME = 20;
+  public static final long DEFAULT_TIMEOUT = 2000;
+  private static final Logger logger = Logger.getLogger(SimpleClient.class.getName());
+  private static SimpleClient defaultInstance;
+
+  /**
+   * Singleton access for simpler client use
+   *
+   * @return
+   */
+  public static SimpleClient getDefault() {
+    if (defaultInstance == null) {
+      defaultInstance = new SimpleClient();
+    }
+    return defaultInstance;
+  }
+
+  /**
+   * Asynchronously request the Data for an Interest. This will send the
+   * Interest and return immediately; use futureData.get() to block until the
+   * Data returns (see FutureData) or manage the event processing independently.
+   *
+   * @param face
+   * @param interest
+   * @return
+   */
+  @Override
+  public Future<Data> getAsync(Face face, Interest interest) {
+    final FutureData futureData = new FutureData(face, interest.getName());
+
+    // send interest
+    try {
+      face.expressInterest(interest, new OnData() {
+        @Override
+        public void onData(Interest interest, Data data) {
+          futureData.resolve(data);
+        }
+      }, new OnTimeout() {
+        @Override
+        public void onTimeout(Interest interest) {
+          futureData.reject(new TimeoutException());
+        }
+      });
+    } catch (IOException e) {
+      logger.log(Level.WARNING, "IO failure while sending interest: ", e);
+      futureData.reject(e);
+    }
+
+    return futureData;
+  }
+
+  /**
+   * Synchronously retrieve the Data for a Name using a default interest (e.g. 2
+   * second timeout); this will block until complete (i.e. either data is
+   * received or the interest times out).
+   *
+   * @param face
+   * @param name
+   * @return
+   */
+  public Future<Data> getAsync(Face face, Name name) {
+    return getAsync(face, getDefaultInterest(name));
+  }
+
+  /**
+   * Synchronously retrieve the Data for an Interest; this will block until
+   * complete (i.e. either data is received or the interest times out).
+   *
+   * @param face
+   * @param interest
+   * @return Data packet or null
+   * @throws java.io.IOException
+   */
+  @Override
+  public Data getSync(Face face, Interest interest) throws IOException {
+    try {
+      return getAsync(face, interest).get();
+    } catch (ExecutionException | InterruptedException e) {
+      logger.log(Level.WARNING, "Failed to retrieve data.", e);
+      throw new IOException("Failed to retrieve data.", e);
+    }
+  }
+
+  /**
+   * Synchronously retrieve the Data for a Name using a default interest (e.g. 2
+   * second timeout); this will block until complete (i.e. either data is
+   * received or the interest times out).
+   *
+   * @param face
+   * @param name
+   * @return
+   * @throws java.io.IOException
+   */
+  public Data getSync(Face face, Name name) throws IOException {
+    return getSync(face, getDefaultInterest(name));
+  }
+
+  /**
+   * Create a default interest for a given Name using some common settings: -
+   * lifetime: 2 seconds
+   *
+   * @param name
+   * @return
+   */
+  public static Interest getDefaultInterest(Name name) {
+    Interest interest = new Interest(name, DEFAULT_TIMEOUT);
+    return interest;
+  }
+}