closes #827 - implement a copyTo (#841)

diff --git a/modules/api/src/main/java/org/apache/fluo/api/data/Bytes.java b/modules/api/src/main/java/org/apache/fluo/api/data/Bytes.java
index 1e66a22..c490329 100644
--- a/modules/api/src/main/java/org/apache/fluo/api/data/Bytes.java
+++ b/modules/api/src/main/java/org/apache/fluo/api/data/Bytes.java
@@ -565,4 +565,45 @@
   public static BytesBuilder builder(int initialCapacity) {
     return new BytesBuilder(initialCapacity);
   }
+
+  /**
+   * Copy entire Bytes object to specific byte array. Uses the specified offset in the dest byte
+   * array to start the copy.
+   * 
+   * @param dest destination array
+   * @param destPos starting position in the destination data.
+   * @exception IndexOutOfBoundsException if copying would cause access of data outside array
+   *            bounds.
+   * @exception NullPointerException if either <code>src</code> or <code>dest</code> is
+   *            <code>null</code>.
+   * @since 1.1.0
+   */
+  public void copyTo(byte[] dest, int destPos) {
+    arraycopy(0, dest, destPos, this.length);
+  }
+
+  /**
+   * Copy a subsequence of Bytes to specific byte array. Uses the specified offset in the dest byte
+   * array to start the copy.
+   * 
+   * @param start index of subsequence start (inclusive)
+   * @param end index of subsequence end (exclusive)
+   * @param dest destination array
+   * @param destPos starting position in the destination data.
+   * @exception IndexOutOfBoundsException if copying would cause access of data outside array
+   *            bounds.
+   * @exception NullPointerException if either <code>src</code> or <code>dest</code> is
+   *            <code>null</code>.
+   * @since 1.1.0
+   */
+  public void copyTo(int start, int end, byte[] dest, int destPos) {
+    // this.subSequence(start, end).copyTo(dest, destPos) would allocate another Bytes object
+    arraycopy(start, dest, destPos, end - start);
+  }
+
+  private void arraycopy(int start, byte[] dest, int destPos, int length) {
+    // since dest is byte[], we can't get the ArrayStoreException
+    System.arraycopy(this.data, start + this.offset, dest, destPos, length);
+  }
+
 }
diff --git a/modules/api/src/test/java/org/apache/fluo/api/data/BytesTest.java b/modules/api/src/test/java/org/apache/fluo/api/data/BytesTest.java
index 7d3aff9..aa5c2b6 100644
--- a/modules/api/src/test/java/org/apache/fluo/api/data/BytesTest.java
+++ b/modules/api/src/test/java/org/apache/fluo/api/data/BytesTest.java
@@ -15,6 +15,8 @@
 
 package org.apache.fluo.api.data;
 
+import static org.junit.Assert.fail;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -22,7 +24,6 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
-import org.apache.fluo.api.data.Bytes;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -252,4 +253,103 @@
     Assert.assertSame(s1, b1.toString());
     Assert.assertSame(s2, b2.toString());
   }
+
+  @Test
+  public void testCopyTo() {
+    Bytes field1 = Bytes.of("foo");
+    Bytes field2 = Bytes.of("bar");
+
+    byte[] dest = new byte[field1.length() + field2.length() + 1];
+
+    field1.copyTo(dest, 0);
+    dest[field1.length()] = ':';
+    field2.copyTo(dest, field1.length() + 1);
+
+    Assert.assertEquals("foo:bar", new String(dest));
+  }
+
+  @Test
+  public void testCopyToOutOfBounds() {
+    Bytes field = Bytes.of("abcdefg");
+    byte[] dest = new byte[field.length() - 1];
+    String initialDest = new String(dest);
+
+    try {
+      field.copyTo(dest, 0);
+      fail("Should not get here");
+    } catch (ArrayIndexOutOfBoundsException e) {
+      // dest should not have changed
+      Assert.assertEquals(new String(dest), initialDest);
+    }
+  }
+
+  @Test
+  public void testCopyToSubset() {
+    Bytes field = Bytes.of("abcdefg");
+    byte[] dest = new byte[4];
+
+    field.copyTo(3, 6, dest, 0);
+    String expected = "def\0";
+    String actual = new String(dest);
+
+    Assert.assertEquals(expected, actual);
+
+    field.subSequence(3, 6).copyTo(dest, 1);
+    // because offset was 1, it will replace ef\0 with def and leave d at position 0
+    Assert.assertEquals("ddef", new String(dest));
+  }
+
+  @Test
+  public void testCopyToArgsReversed() {
+    Bytes field = Bytes.of("abcdefg");
+    byte[] dest = new byte[4];
+
+    try {
+      field.copyTo(6, 3, dest, 0);
+      fail("should not get here");
+    } catch (java.lang.ArrayIndexOutOfBoundsException e) {
+      Assert.assertEquals("\0\0\0\0", new String(dest));
+    }
+  }
+
+  @Test
+  public void testCopyToNothing() {
+    Bytes field = Bytes.of("abcdefg");
+    byte[] dest = new byte[4];
+
+    field.copyTo(3, 3, dest, 0);
+    // should not have changed
+    Assert.assertEquals("\0\0\0\0", new String(dest));
+  }
+
+  @Test
+  public void testCopyToWithUnicode() {
+    // first observe System.arraycopy
+    String begin = "abc"; // 3 chars, 3 bytes
+    String mid1 = "†"; // 1 char, 3 bytes
+    String mid2 = "𝔊"; // 2 chars, 4 bytes
+    String end = "efghi"; // 5 chars, 5 bytes
+    Assert.assertEquals(11, begin.length() + mid1.length() + mid2.length() + end.length());
+
+    byte[] copyFrom = (begin + mid1 + mid2 + end).getBytes();
+    //@formatter:off
+    // [ a,  b,  c,              †,                    𝔊,   e,   f,   g,   h,   i]
+    // [97, 98, 99, -30, -128, -96, -16, -99, -108, -118, 101, 102, 103, 104, 105]
+    //@formatter:on
+    Assert.assertEquals(15, copyFrom.length);
+
+    byte[] copyTo = new byte[9];
+    System.arraycopy(copyFrom, 2, copyTo, 0, 9);
+    Assert.assertEquals("c†𝔊e", new String(copyTo));
+
+    // now make a Bytes out of the craziness
+    Bytes allBytes = Bytes.of(copyFrom);
+    Assert.assertEquals(15, allBytes.length());
+
+    // and test Bytes.arraycopy works the same
+    byte[] copyTo2 = new byte[9];
+    allBytes.copyTo(2, 11, copyTo2, 0);
+    Assert.assertEquals("c†𝔊e", new String(copyTo2));
+  }
+
 }