IO-305:  New copyLarge() method in IOUtils that takes additional offset, length arguments

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/io/trunk@1301851 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 960365c..6e61fe8 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -24,6 +24,7 @@
 Changes in this version include:
 
 New features:
+o IO-305:  New copyLarge() method in IOUtils that takes additional offset, length arguments Thanks to Manoj Mokashi. 
 o IO-287:  Use terabyte (TB) , petabyte (PB) and exabyte (EB) in FileUtils.byteCountToDisplaySize(long size) Thanks to Ron Kuris, Gary Gregory. 
 o IO-173:  FileUtils.listFiles() doesn't return directories Thanks to Marcos Vinícius da Silva. 
 o IO-297:  CharSequenceInputStream to efficiently stream content of a CharSequence Thanks to Oleg Kalnichevski. 
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7868227..c6aee9d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -40,6 +40,9 @@
 
   <body>
     <release version="2.2" date="TBA">
+      <action issue="IO-305" dev="sebb" type="add" due-to="Manoj Mokashi">
+        New copyLarge() method in IOUtils that takes additional offset, length arguments
+      </action>        
       <action issue="IO-300" dev="sebb" type="fix">
         FileUtils.moveDirectoryToDirectory removes source directory if destination is a subdirectory
       </action>        
diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java
index 1645c5b..a269223 100644
--- a/src/main/java/org/apache/commons/io/IOUtils.java
+++ b/src/main/java/org/apache/commons/io/IOUtils.java
@@ -1458,6 +1458,51 @@
     }
 
     /**
+     * Copy some or all bytes from a large (over 2GB) <code>InputStream</code> to an
+     * <code>OutputStream</code>, optionally skipping input bytes.
+     * <p>
+     * This method buffers the input internally, so there is no need to use a
+     * <code>BufferedInputStream</code>.
+     * <p>
+     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+     * 
+     * @param input  the <code>InputStream</code> to read from
+     * @param output  the <code>OutputStream</code> to write to
+     * @param offset : number of bytes to skip from input before copying
+     *         -ve values are ignored
+     * @param length : number of bytes to copy. -ve means all
+     * @return the number of bytes copied
+     * @throws NullPointerException if the input or output is null
+     * @throws IOException if an I/O error occurs
+     * @since Commons IO 2.2
+     */
+    public static long copyLarge(InputStream input, OutputStream output, final long offset, final long length)
+            throws IOException {
+        if( offset > 0){
+            skipFully( input, offset);
+        }
+        if (length == 0) {
+            return 0;
+        }
+        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+        int bytesToRead = buffer.length;
+        if (length > 0 && length < buffer.length) {
+            bytesToRead = (int) length;
+        }
+        int read;
+        long totalRead = 0;
+        while(bytesToRead > 0 && -1 != (read=input.read(buffer, 0, bytesToRead))) {
+            output.write( buffer, 0, read);
+            totalRead += read;
+            if (length > 0) { // only adjust length if not reading to the end
+                // Note the cast must work because buffer.length is an integer
+                bytesToRead = (int) Math.min(length - totalRead, buffer.length);
+            }
+        }
+        return totalRead;
+    }
+
+    /**
      * Copy bytes from an <code>InputStream</code> to chars on a
      * <code>Writer</code> using the default character encoding of the platform.
      * <p>
@@ -1562,6 +1607,51 @@
     }
 
     /**
+     * Copy some or all chars from a large (over 2GB) <code>InputStream</code> to an
+     * <code>OutputStream</code>, optionally skipping input chars.
+     * <p>
+     * This method buffers the input internally, so there is no need to use a
+     * <code>BufferedReader</code>.
+     * <p>
+     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.
+     * 
+     * @param input  the <code>Reader</code> to read from
+     * @param output  the <code>Writer</code> to write to
+     * @param offset : number of chars to skip from input before copying
+     *         -ve values are ignored
+     * @param length : number of chars to copy. -ve means all
+     * @return the number of chars copied
+     * @throws NullPointerException if the input or output is null
+     * @throws IOException if an I/O error occurs
+     * @since Commons IO 2.2
+     */
+    public static long copyLarge(Reader input, Writer output, final long offset, final long length)
+            throws IOException {
+        if( offset > 0){
+            skipFully( input, offset);
+        }
+        if (length == 0) {
+            return 0;
+        }
+        char[] buffer = new char[DEFAULT_BUFFER_SIZE];
+        int bytesToRead = buffer.length;
+        if (length > 0 && length < buffer.length) {
+            bytesToRead = (int) length;
+        }
+        int read;
+        long totalRead = 0;
+        while(bytesToRead > 0 && -1 != (read=input.read(buffer, 0, bytesToRead))) {
+            output.write( buffer, 0, read);
+            totalRead += read;
+            if (length > 0) { // only adjust length if not reading to the end
+                // Note the cast must work because buffer.length is an integer
+                bytesToRead = (int) Math.min(length - totalRead, buffer.length);
+            }
+        }
+        return totalRead;
+    }
+
+    /**
      * Copy chars from a <code>Reader</code> to bytes on an
      * <code>OutputStream</code> using the default character encoding of the
      * platform, and calling flush.
diff --git a/src/test/java/org/apache/commons/io/IOUtilsTestCase.java b/src/test/java/org/apache/commons/io/IOUtilsTestCase.java
index 942aed0..2369332 100644
--- a/src/test/java/org/apache/commons/io/IOUtilsTestCase.java
+++ b/src/test/java/org/apache/commons/io/IOUtilsTestCase.java
@@ -17,7 +17,9 @@
 package org.apache.commons.io;
 
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.CharArrayReader;
+import java.io.CharArrayWriter;
 import java.io.EOFException;
 import java.io.File;
 import java.io.FileInputStream;
@@ -73,10 +75,23 @@
             throw new RuntimeException("Can't run this test because " + "environment could not be built: "
                     + ioe.getMessage());
         }
+        // Create and init a byte array as input data
+        iarr = new byte[200];
+        Arrays.fill( iarr, (byte)-1);
+        for( int i=0; i< 80; i++){
+            iarr[i] = (byte) i;
+        }
+        carr = new char[200];
+        Arrays.fill( carr, (char)-1);
+        for( int i=0; i< 80; i++){
+            carr[i] = (char) i;
+        }
     }
 
     @Override
     public void tearDown() {
+        carr = null;
+        iarr = null;
         try {
             FileUtils.deleteDirectory(getTestDirectory());
         } catch (IOException ioe) {
@@ -718,4 +733,259 @@
         IOUtils.closeQuietly(input);
     }
 
+    // Tests from IO-305
+    
+    private byte[] iarr = null;
+    
+    public void testNoSkip() throws IOException {
+        ByteArrayInputStream is = null;
+        ByteArrayOutputStream os = null;
+        try {
+            // Create streams
+            is = new ByteArrayInputStream( iarr);
+            os = new ByteArrayOutputStream();
+
+            // Test our copy method 
+            assertEquals(100, IOUtils.copyLarge( is, os, 0, 100));
+            byte[] oarr = os.toByteArray();
+            
+            // check that output length is correct
+            assertEquals( 100, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals( -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testSkip() throws IOException {
+        ByteArrayInputStream is = null;
+        ByteArrayOutputStream os = null;
+        try {
+            // Create streams
+            is = new ByteArrayInputStream( iarr);
+            os = new ByteArrayOutputStream();
+
+            // Test our copy method 
+            assertEquals(100, IOUtils.copyLarge( is, os, 10, 100));
+            byte[] oarr = os.toByteArray();
+            
+            // check that output length is correct
+            assertEquals( 100, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 11, oarr[1] );
+            assertEquals( 79, oarr[69] );
+            assertEquals( -1, oarr[70] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testSkipInvalid() throws IOException {
+        ByteArrayInputStream is = null;
+        ByteArrayOutputStream os = null;
+        try {
+            // Create streams
+            is = new ByteArrayInputStream( iarr);
+            os = new ByteArrayOutputStream();
+
+            // Test our copy method 
+            IOUtils.copyLarge( is, os, 1000, 100);
+            fail( "Should have thrown EOFException");
+        }
+        catch( EOFException eofe){
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testFullLength() throws IOException {
+        ByteArrayInputStream is = null;
+        ByteArrayOutputStream os = null;
+        try {
+            // Create streams
+            is = new ByteArrayInputStream( iarr);
+            os = new ByteArrayOutputStream();
+
+            // Test our copy method 
+            assertEquals(200, IOUtils.copyLarge( is, os, 0, -1));
+            byte[] oarr = os.toByteArray();
+            
+            // check that output length is correct
+            assertEquals( 200, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals( -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testExtraLength() throws IOException {
+        ByteArrayInputStream is = null;
+        ByteArrayOutputStream os = null;
+        try {
+            // Create streams
+            is = new ByteArrayInputStream( iarr);
+            os = new ByteArrayOutputStream();
+
+            // Test our copy method
+            // for extra length, it reads till EOF
+            assertEquals(200, IOUtils.copyLarge( is, os, 0, 2000));
+            byte[] oarr = os.toByteArray();
+            
+            // check that output length is correct
+            assertEquals( 200, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals( -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    private char[] carr = null;
+
+    public void testCharNoSkip() throws IOException {
+        CharArrayReader is = null;
+        CharArrayWriter os = null;
+        try {
+            // Create streams
+            is = new CharArrayReader( carr);
+            os = new CharArrayWriter();
+
+            // Test our copy method 
+            assertEquals(100, IOUtils.copyLarge( is, os, 0, 100));
+            char[] oarr = os.toCharArray();
+            
+            // check that output length is correct
+            assertEquals( 100, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals((char) -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testCharSkip() throws IOException {
+        CharArrayReader is = null;
+        CharArrayWriter os = null;
+        try {
+            // Create streams
+            is = new CharArrayReader( carr);
+            os = new CharArrayWriter();
+
+            // Test our copy method 
+            assertEquals(100, IOUtils.copyLarge( is, os, 10, 100));
+            char[] oarr = os.toCharArray();
+            
+            // check that output length is correct
+            assertEquals( 100, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 11, oarr[1] );
+            assertEquals( 79, oarr[69] );
+            assertEquals((char) -1, oarr[70] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testCharSkipInvalid() throws IOException {
+        CharArrayReader is = null;
+        CharArrayWriter os = null;
+        try {
+            // Create streams
+            is = new CharArrayReader( carr);
+            os = new CharArrayWriter();
+
+            // Test our copy method 
+            IOUtils.copyLarge( is, os, 1000, 100);
+            fail( "Should have thrown EOFException");
+        }
+        catch( EOFException eofe){
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testCharFullLength() throws IOException {
+        CharArrayReader is = null;
+        CharArrayWriter os = null;
+        try {
+            // Create streams
+            is = new CharArrayReader( carr);
+            os = new CharArrayWriter();
+
+            // Test our copy method 
+            assertEquals(200, IOUtils.copyLarge( is, os, 0, -1));
+            char[] oarr = os.toCharArray();
+            
+            // check that output length is correct
+            assertEquals( 200, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals((char) -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
+
+    public void testCharExtraLength() throws IOException {
+        CharArrayReader is = null;
+        CharArrayWriter os = null;
+        try {
+            // Create streams
+            is = new CharArrayReader( carr);
+            os = new CharArrayWriter();
+
+            // Test our copy method
+            // for extra length, it reads till EOF
+            assertEquals(200, IOUtils.copyLarge( is, os, 0, 2000));
+            char[] oarr = os.toCharArray();
+            
+            // check that output length is correct
+            assertEquals( 200, oarr.length );
+            // check that output data corresponds to input data
+            assertEquals( 1, oarr[1] );
+            assertEquals( 79, oarr[79] );
+            assertEquals((char) -1, oarr[80] );
+            
+        }
+        finally {
+            IOUtils.closeQuietly(is);
+            IOUtils.closeQuietly(os);
+        }
+    }
 }