FELIX-4486: fix possible thread leakage:

- replaced the ExplodingOutputtingInputStream with a much simpler 
  ContentCopyingJarInputStream that levarages the JarInputStream to copy
  entries while they are read;
- added some test cases to verify the new implementation.



git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1587613 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java
new file mode 100644
index 0000000..bbaec0e
--- /dev/null
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.deploymentadmin;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.ZipEntry;
+
+/**
+ * Provides a custom {@link JarInputStream} that copies all entries read from the original 
+ * {@link InputStream} to a given directory and index file. It does this by tracking the
+ * common usage of the {@link JarInputStream} API. For each entry that is read it streams
+ * all read bytes to a separate file compressing it on the fly. The caller does not notice
+ * anything, although it might be that the {@link #read(byte[], int, int)} is blocked for
+ * a little while during the writing of the file contents.
+ * <p>
+ * This implementation replaces the old <tt>ExplodingOutputtingInputStream</tt> that used
+ * at least two threads and was difficult to understand and maintain. See FELIX-4486.
+ * </p>
+ */
+class ContentCopyingJarInputStream extends JarInputStream
+{
+    private static final String MANIFEST_FILE = JarFile.MANIFEST_NAME;
+
+    private final File m_contentDir;
+
+    private PrintWriter m_indexFileWriter;
+    /** Used to copy the contents of the *next* entry. */
+    private OutputStream m_entryOS;
+
+    public ContentCopyingJarInputStream(InputStream in, File indexFile, File contentDir) throws IOException
+    {
+        super(in);
+
+        m_contentDir = contentDir;
+
+        m_indexFileWriter = new PrintWriter(new FileWriter(indexFile));
+        m_entryOS = null;
+
+        // the manifest of the JAR is already read by JarInputStream, so we need to write this one as well...
+        Manifest manifest = getManifest();
+        if (manifest != null)
+        {
+            copyManifest(manifest);
+        }
+    }
+
+    public void close() throws IOException
+    {
+        closeCopy();
+        closeIndex();
+        super.close();
+    }
+
+    public void closeEntry() throws IOException
+    {
+        closeCopy();
+        super.closeEntry();
+    }
+
+    public ZipEntry getNextEntry() throws IOException
+    {
+        closeCopy();
+
+        ZipEntry entry = super.getNextEntry();
+        if (entry != null)
+        {
+            File current = new File(m_contentDir, entry.getName());
+            if (!entry.isDirectory())
+            {
+                addToIndex(entry.getName());
+
+                m_entryOS = createOutputStream(current);
+            }
+        }
+
+        return entry;
+    }
+
+    public int read(byte[] b, int off, int len) throws IOException
+    {
+        int r = super.read(b, off, len);
+        if (m_entryOS != null)
+        {
+            if (r > 0)
+            {
+                m_entryOS.write(b, off, r);
+            }
+            else
+            {
+                closeCopy();
+            }
+        }
+        return r;
+    }
+
+    private void addToIndex(String name) throws IOException
+    {
+        m_indexFileWriter.println(name);
+        m_indexFileWriter.flush();
+    }
+
+    private void closeCopy()
+    {
+        closeSilently(m_entryOS);
+        m_entryOS = null;
+    }
+
+    private void closeIndex()
+    {
+        closeSilently(m_indexFileWriter);
+        m_indexFileWriter = null;
+    }
+
+    private void closeSilently(Closeable resource)
+    {
+        try
+        {
+            if (resource != null)
+            {
+                resource.close();
+            }
+        }
+        catch (IOException e)
+        {
+            // Ignore, not much we can do about this...
+        }
+    }
+
+    /**
+     * Creates a verbatim copy of the manifest, when it is read from the original JAR.
+     */
+    private void copyManifest(Manifest manifest) throws IOException
+    {
+        addToIndex(MANIFEST_FILE);
+
+        OutputStream os = createOutputStream(new File(m_contentDir, MANIFEST_FILE));
+        try
+        {
+            manifest.write(os);
+        }
+        finally
+        {
+            closeSilently(os);
+        }
+    }
+
+    private OutputStream createOutputStream(File file) throws IOException
+    {
+        File parent = file.getParentFile();
+        if (parent != null)
+        {
+            parent.mkdirs();
+        }
+        if (!file.createNewFile())
+        {
+            throw new IOException("Attempt to overwrite file: " + file);
+        }
+        return new GZIPOutputStream(new FileOutputStream(file));
+    }
+}
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
index aeb2624..4a3b497 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java
@@ -131,8 +131,8 @@
         return m_packageAdmin;
     }
 
-    public DeploymentPackage installDeploymentPackage(InputStream input) throws DeploymentException {
-        if (input == null) {
+    public DeploymentPackage installDeploymentPackage(InputStream sourceInput) throws DeploymentException {
+        if (sourceInput == null) {
             throw new IllegalArgumentException("Inputstream may not be null");
         }
 
@@ -163,7 +163,6 @@
                 tempIndex = new File(tempPackage, PACKAGEINDEX_FILE);
                 tempContents = new File(tempPackage, PACKAGECONTENTS_DIR);
                 tempContents.mkdirs();
-                input = new ExplodingOutputtingInputStream(input, tempIndex, tempContents);
             }
             catch (IOException e) {
                 m_log.log(LogService.LOG_ERROR, "Error writing package to disk", e);
@@ -171,7 +170,7 @@
             }
 
             try {
-                jarInput = new JarInputStream(input);
+                jarInput = new ContentCopyingJarInputStream(sourceInput, tempIndex, tempContents);
                 
                 if (jarInput.getManifest() == null) {
                     m_log.log(LogService.LOG_ERROR, "Stream does not contain a valid deployment package: missing manifest!");
@@ -206,31 +205,12 @@
                 verifySourcePackage(source);
             }
 
-            // To keep track whether or not we're masking an exception during the close of the input stream...
-            boolean installFailed = false;
-            
             try {
                 m_session = new DeploymentSessionImpl(source, target, createInstallCommandChain(), this);
                 m_session.call(false /* ignoreExceptions */);
             }
             catch (DeploymentException de) {
-                installFailed = true;
                 throw de;
-            } finally {
-                try {
-                    // make sure we've read until the end-of-stream, so the explodingoutput-wrapper can process all bytes
-                    Utils.readUntilEndOfStream(input);
-
-                    // note that calling close on this stream will wait until asynchronous processing is done
-                    input.close();
-                }
-                catch (IOException e) {
-                    m_log.log(LogService.LOG_ERROR, "Could not close stream properly", e);
-                    // Do not mask out any originally thrown exceptions...
-                    if (!installFailed) {
-                        throw new DeploymentException(DeploymentException.CODE_OTHER_ERROR, "Could not close stream properly", e);
-                    }
-                }
             }
 
             String dpInstallBaseDirectory = PACKAGE_DIR + File.separator + dpSymbolicName;
@@ -277,6 +257,7 @@
                 	succeeded = false;
                 }
             }
+
     	    sendCompleteEvent(source, target, succeeded);
             m_semaphore.release();
         }
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStream.java
deleted file mode 100644
index 001159e..0000000
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStream.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.felix.deploymentadmin;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.io.PrintWriter;
-import java.util.zip.GZIPOutputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- * This class will write all entries encountered in an input stream to disk. An index of files written to disk is kept in an index file in the
- * order they were encountered. Each file is compressed using GZIP. All the work is done on a separate thread.
- */
-class ExplodingOutputtingInputStream extends OutputtingInputStream {
-    
-    static class ReaderThread extends Thread {
-        
-        private final File m_contentDir;
-        private final File m_indexFile;
-        private final PipedInputStream m_input;
-        
-        private volatile Exception m_exception;
-
-        public ReaderThread(PipedOutputStream output, File index, File root) throws IOException {
-            super("Apache Felix DeploymentAdmin - ExplodingOutputtingInputStream");
-            
-            m_contentDir = root;
-            m_indexFile = index;
-            m_input = new PipedInputStream(output);
-        }
-        
-        public void run() {
-            byte[] buffer = new byte[4096];
-
-            ZipInputStream input = null;
-            PrintWriter writer = null;
-            try {
-                input = new ZipInputStream(m_input);
-                writer = new PrintWriter(new FileWriter(m_indexFile));
-
-                for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) {
-                    File current = new File(m_contentDir, entry.getName());
-                    if (entry.isDirectory()) {
-                        current.mkdirs();
-                    }
-                    else {
-                        writer.println(entry.getName());
-                        File parent = current.getParentFile();
-                        if (parent != null) {
-                            parent.mkdirs();
-                        }
-                        OutputStream output = null;
-                        try {
-                            output = new GZIPOutputStream(new FileOutputStream(current));
-                            for (int i = input.read(buffer); i > -1; i = input.read(buffer)) {
-                                output.write(buffer, 0, i);
-                            }
-                        }
-                        finally {
-                            output.close();
-                        }
-                    }
-                    input.closeEntry();
-                    writer.flush();
-                }
-            }
-            catch (IOException ex) {
-                pushException(ex);
-            }
-            finally {
-                if (writer != null) {
-                    writer.close();
-                }
-            }
-
-            try {
-                Utils.readUntilEndOfStream(m_input);
-            }
-            catch (IOException e) {
-                pushException(e);
-            }
-            finally {
-                if (input != null) {
-                    try {
-                        input.close();
-                    }
-                    catch (IOException e) {
-                        pushException(e);
-                    }
-                }
-            }
-        }
-
-        private void pushException(Exception e) {
-            Exception e2 = new Exception(e.getMessage());
-            e2.setStackTrace(e.getStackTrace());
-            if (m_exception != null) {
-                e2.initCause(m_exception);
-            }
-            m_exception = e2;
-        }
-    }
-
-    private final ReaderThread m_task;
-
-    /**
-     * Creates an instance of this class.
-     *
-     * @param inputStream The input stream that will be written to disk as individual entries as it's read.
-     * @param indexFile File to be used to write the index of all encountered files.
-     * @param contentDir File to be used as the directory to hold all files encountered in the stream.
-     * @throws IOException If a problem occurs reading the stream resources.
-     */
-    public ExplodingOutputtingInputStream(InputStream inputStream, File indexFile, File contentDir) throws IOException {
-        this(inputStream, new PipedOutputStream(), indexFile, contentDir);
-    }
-
-    private ExplodingOutputtingInputStream(InputStream inputStream, PipedOutputStream output, File index, File root) throws IOException {
-        super(inputStream, output);
-        m_task = new ReaderThread(output, index, root);
-        m_task.start();
-    }
-
-    public void close() throws IOException {
-        try {
-            super.close();
-            
-            Exception exception = m_task.m_exception;
-            if (exception != null) {
-                throw (IOException) new IOException("Exception while processing the stream in the background: " + exception.getMessage()).initCause(exception);
-            }
-        }
-        finally {
-            waitFor();
-        }
-    }
-
-    private void waitFor() {
-        try {
-            m_task.join();
-        }
-        catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
-    }
-}
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/OutputtingInputStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/OutputtingInputStream.java
deleted file mode 100644
index ac8fa36..0000000
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/OutputtingInputStream.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.felix.deploymentadmin;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * This extension of the <code>InputStream</code> writes every byte that is read to an
- * <code>OutputStream</code> of choice. The outputstream is closed automatically when
- * the end of the inputstream is reached.
- */
-public class OutputtingInputStream extends InputStream {
-
-    private final InputStream m_inputStream;
-    private final OutputStream m_outputStream;
-
-    /**
-     * Creates an instance of the <code>OutputtingInputStream</code>.
-     *
-     * @param inputStream The inputstream from which bytes will be read.
-     * @param outputStream The outputstream to which every byte that is read should be outputted.
-     */
-    public OutputtingInputStream(InputStream inputStream, OutputStream outputStream) {
-        super();
-        m_inputStream = inputStream;
-        m_outputStream = outputStream;
-    }
-
-    public int read() throws IOException {
-        int i = m_inputStream.read();
-        if (i != -1) {
-            m_outputStream.write(i);
-        }
-        return i;
-    }
-
-    public int read(byte[] buffer) throws IOException {
-        int i = m_inputStream.read(buffer);
-        if (i != -1) {
-            m_outputStream.write(buffer, 0, i);
-        }
-        return i;
-    }
-
-    public int read(byte[] buffer, int off, int len) throws IOException {
-        int i = m_inputStream.read(buffer, off, len);
-        if (i != -1) {
-            m_outputStream.write(buffer, off, i);
-        }
-        return i;
-    }
-
-    public void close() throws IOException {
-        try {
-            m_inputStream.close();
-        }
-        finally {
-            try {
-                m_outputStream.close();
-            }
-            catch (Exception e) {
-                // TODO: review the implications of this
-            }
-        }
-    }
-}
diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java
index 2627b3b..9279b50 100644
--- a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java
+++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java
@@ -33,12 +33,15 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.jar.Attributes;
+import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 import java.util.jar.Attributes.Name;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 
 public class Utils {
+    private static final String MANIFEST_NAME = JarFile.MANIFEST_NAME;
+
     public static Manifest readManifest(File manifestFile) throws IOException {
         InputStream is = null;
         Manifest mf = null;
@@ -51,14 +54,6 @@
         }
         return mf;
     }
-    
-    public static void readUntilEndOfStream(InputStream is) throws IOException {
-        byte[] buffer = new byte[1024];
-        int c = is.read(buffer);
-        while (c != -1) {
-            c = is.read(buffer);
-        }
-    }
 
     public static boolean replace(File target, File source) {
         return delete(target, true /* deleteRoot */) && rename(source, target);
@@ -153,7 +148,7 @@
 
         for (Iterator i = result.iterator(); i.hasNext();) {
             String targetFile = (String) i.next();
-            if (!"META-INF/MANIFEST.MF".equals(targetFile) && !resultManifest.getEntries().containsKey(targetFile)) {
+            if (!MANIFEST_NAME.equals(targetFile) && !resultManifest.getEntries().containsKey(targetFile)) {
                 i.remove();
             }
         }
@@ -193,7 +188,7 @@
             }
         }
 
-        GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(new File(target, "META-INF/MANIFEST.MF")));
+        GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(new File(target, MANIFEST_NAME)));
         try {
             resultManifest.write(outputStream);
         } finally {
diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java
new file mode 100644
index 0000000..e7cbccc
--- /dev/null
+++ b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.deploymentadmin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.ZipEntry;
+
+import junit.framework.TestCase;
+
+/**
+ * Test cases for {@link ContentCopyingJarInputStream}.
+ */
+public class ContentCopyingJarInputStreamTest extends TestCase
+{
+    private static final String MANIFEST_NAME = JarFile.MANIFEST_NAME;
+    private static final String INDEX_NAME = "META-INF/INDEX.LIST";
+    
+    private File m_tempDir;
+    private File m_jarFile;
+
+    /**
+     * Tests that we can copy a simple {@link JarInputStream}. 
+     */
+    public void testCopyEmptyJarOk() throws Exception
+    {
+        createEmptyJar();
+
+        assertJarContents(null);
+    }
+
+    /**
+     * Tests that we can copy a simple {@link JarInputStream}. 
+     */
+    public void testCopyJarWithIndexAndWithManifestOk() throws Exception
+    {
+        Manifest man = createManifest();
+
+        createJar(man, true /* includeIndex */);
+
+        assertJarContents(man);
+    }
+
+    /**
+     * Tests that we can copy a {@link JarInputStream} even if it does not contains a manifest file. 
+     */
+    public void testCopyJarWithIndexAndWithoutManifestOk() throws Exception
+    {
+        Manifest man = null;
+
+        createJar(man, true /* includeIndex */);
+
+        assertJarContents(man);
+    }
+
+    /**
+     * Tests that we can copy a simple {@link JarInputStream}. 
+     */
+    public void testCopyJarWithoutIndexAndWithManifestOk() throws Exception
+    {
+        Manifest man = createManifest();
+
+        createJar(man, false /* includeIndex */);
+
+        assertJarContents(man);
+    }
+
+    /**
+     * Tests that we can copy a {@link JarInputStream} even if it does not contains a manifest file. 
+     */
+    public void testCopyJarWithoutIndexAndWithoutManifestOk() throws Exception
+    {
+        Manifest man = null;
+
+        createJar(man, false /* includeIndex */);
+
+        assertJarContents(man);
+    }
+
+    protected void setUp() throws IOException
+    {
+        m_tempDir = createTempDir();
+        m_jarFile = new File(m_tempDir, "input.jar");
+    }
+
+    protected void tearDown()
+    {
+        Utils.delete(m_tempDir, true);
+    }
+
+    private void appendFiles(JarOutputStream jos, int count) throws IOException
+    {
+        int size = 1024;
+
+        for (int i = 0, j = 1; i < count; i++, j++)
+        {
+            JarEntry entry = new JarEntry("sub/" + j);
+            jos.putNextEntry(entry);
+            for (int k = 0; k < size; k++)
+            {
+                jos.write('0' + j);
+            }
+            jos.closeEntry();
+        }
+    }
+
+    private void assertJarContents(Manifest man) throws IOException
+    {
+        File indexFile = new File(m_tempDir, "index.txt");
+
+        FileInputStream fis = new FileInputStream(m_jarFile);
+        JarInputStream jis = new ContentCopyingJarInputStream(fis, indexFile, m_tempDir);
+
+        try
+        {
+            JarEntry entry;
+            while ((entry = jis.getNextJarEntry()) != null)
+            {
+                File f = new File(m_tempDir, entry.getName());
+
+                // Without reading the actual contents, the copy should already exist...
+                assertTrue(entry.getName() + " does not exist?!", f.exists());
+                
+                int size = (INDEX_NAME.equals(entry.getName()) ? 33 : 1024);
+
+                byte[] input = new byte[size];
+                int read = jis.read(input);
+
+                assertEquals("Not all bytes were read: " + entry.getName(), size, read);
+
+                // Contents will only be completely written after closing the JAR entry itself...
+                jis.closeEntry();
+
+                verifyContents(f, input);
+            }
+
+            assertEquals("Manifest not as expected", man, jis.getManifest());
+        }
+        finally
+        {
+            jis.close();
+        }
+    }
+    
+    private void createEmptyJar() throws IOException {
+        FileOutputStream fos = new FileOutputStream(m_jarFile);
+        JarOutputStream jos = new JarOutputStream(fos);
+        jos.close();
+    }
+
+    private void createJar(Manifest man, boolean includeIndex) throws IOException
+    {
+        FileOutputStream fos = new FileOutputStream(m_jarFile);
+        JarOutputStream jos;
+
+        if (man == null || includeIndex)
+        {
+            jos = new JarOutputStream(fos);
+        }
+        else
+        {
+            jos = new JarOutputStream(fos, man);
+        }
+
+        if (includeIndex)
+        {
+            // Write the INDEX.LIST file as first entry...
+            jos.putNextEntry(new ZipEntry(INDEX_NAME));
+            jos.write(("JarIndex-Version: 1.0\n\n" + m_jarFile.getName() + "\n").getBytes());
+            jos.closeEntry();
+
+            if (man != null)
+            {
+                jos.putNextEntry(new ZipEntry(MANIFEST_NAME));
+                man.write(jos);
+                jos.closeEntry();
+            }
+        }
+
+        try
+        {
+            appendFiles(jos, 5);
+        }
+        finally
+        {
+            jos.close();
+        }
+    }
+
+    private Manifest createManifest()
+    {
+        Manifest mf = new Manifest();
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+        mf.getMainAttributes().putValue("Bundle-ManifestVersion", "2");
+        mf.getMainAttributes().putValue("Bundle-Version", "1.0.0");
+        mf.getMainAttributes().putValue("Bundle-SymbolicName", "com.foo.bar");
+        return mf;
+    }
+
+    private File createTempDir() throws IOException
+    {
+        File tmpFile = File.createTempFile("ccjis_test", null);
+        tmpFile.delete();
+        tmpFile.mkdir();
+        return tmpFile;
+    }
+
+    private void verifyContents(File file, byte[] expected) throws IOException
+    {
+        FileInputStream fis = new FileInputStream(file);
+        GZIPInputStream gis = new GZIPInputStream(fis);
+        try
+        {
+            byte[] b = new byte[expected.length];
+
+            int read = gis.read(b);
+            assertEquals(b.length, read);
+
+            for (int i = 0; i < expected.length; i++)
+            {
+                assertEquals(expected[i], b[i]);
+            }
+        }
+        finally
+        {
+            gis.close();
+            fis.close();
+        }
+    }
+}
diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStreamTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStreamTest.java
deleted file mode 100644
index bbc9ee7..0000000
--- a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ExplodingOutputtingInputStreamTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.felix.deploymentadmin;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
-import java.util.jar.JarOutputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import junit.framework.TestCase;
-
-public class ExplodingOutputtingInputStreamTest extends TestCase {
-    public void testStream() throws Exception {
-        // fill up a stringbuffer with some test data
-        StringBuffer sb = new StringBuffer();
-        for (int i = 0; i < 1000; i++) {
-            sb.append("DATAdataDATAdata");
-        }
-        String data = sb.toString();
-        
-        // create a temporary folder
-        File tempDir = File.createTempFile("temp", "dir");
-        tempDir.delete();
-        tempDir.mkdirs();
-        System.out.println("Dir: " + tempDir);
-        
-        // create a zip file with two entries in it
-        File zipfile = new File(tempDir, "zipfile");
-        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfile));
-        String dummy1 = "dummy";
-        zos.putNextEntry(new ZipEntry(dummy1));
-        zos.write(data.getBytes());
-        zos.closeEntry();
-        String dummy2 = "dummy2";
-        zos.putNextEntry(new ZipEntry(dummy2));
-        zos.write(data.getBytes());
-        zos.closeEntry();
-        zos.close();
-        
-        // create another temporary folder
-        File dir = new File(tempDir, "dir");
-        dir.mkdirs();
-        File index = new File(tempDir, "list");
-        ExplodingOutputtingInputStream stream = new ExplodingOutputtingInputStream(new FileInputStream(zipfile), index, dir);
-        byte[] buffer = new byte[2];
-        int read = stream.read(buffer);
-        while (read != -1) {
-            read = stream.read(buffer);
-        }
-        stream.close();
-        
-        // create references to the unpacked dummy files
-        File d1 = new File(dir, dummy1);
-        File d2 = new File(dir, dummy2);
-        
-        // cleanup
-        zipfile.delete();
-        index.delete();
-        d1.delete();
-        d2.delete();
-        dir.delete();
-        tempDir.delete();
-    }
-    
-    public void testStreamReadWithJARStream() throws Exception {
-        // fill up a stringbuffer with some test data
-        StringBuffer sb = new StringBuffer();
-        for (int i = 0; i < 1000; i++) {
-            sb.append("DATAdataDATAdata");
-        }
-        String data = sb.toString();
-        
-        // create a temporary folder
-        File tempDir = File.createTempFile("temp", "dir");
-        tempDir.delete();
-        tempDir.mkdirs();
-        System.out.println("Dir: " + tempDir);
-        
-        // create a zip file with two entries in it
-        File jarfile = new File(tempDir, "jarfile");
-        JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarfile));
-        String dummy1 = "dummy";
-        jos.putNextEntry(new JarEntry(dummy1));
-        jos.write(data.getBytes());
-        jos.closeEntry();
-        String dummy2 = "dummy2";
-        jos.putNextEntry(new JarEntry(dummy2));
-        jos.write(data.getBytes());
-        jos.closeEntry();
-        jos.close();
-        
-        // create another temporary folder
-        File dir = new File(tempDir, "dir");
-        dir.mkdirs();
-        File index = new File(tempDir, "list");
-        ExplodingOutputtingInputStream stream = new ExplodingOutputtingInputStream(new FileInputStream(jarfile), index, dir);
-        JarInputStream jarInputStream = new JarInputStream(stream);
-
-        JarEntry entry;
-        while ((entry = jarInputStream.getNextJarEntry()) != null) {
-            int size = 0;
-            byte[] buffer = new byte[4096];
-            for (int i = jarInputStream.read(buffer); i > -1; i = jarInputStream.read(buffer)) {
-                size += i;
-            }
-            System.out.println("read JAR entry: " + entry + " of " + size + " bytes.");
-            jarInputStream.closeEntry();
-        }
-        stream.close();
-        
-        // create references to the unpacked dummy files
-        File d1 = new File(dir, dummy1);
-        File d2 = new File(dir, dummy2);
-        
-        // cleanup
-        jarfile.delete();
-        index.delete();
-        d1.delete();
-        d2.delete();
-        dir.delete();
-        tempDir.delete();
-    }
-}