Trac. #78: Sniff file types by contents, support nested archives.
diff --git a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
index c74fe0c..1354556 100644
--- a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
+++ b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
@@ -481,17 +481,9 @@
 				final Resource resource = (Resource) i.next();

 				if (resource.isDirectory() && resource instanceof FileResource) {

 					analyzer.analyzeAll(((FileResource) resource).getFile());

-					continue;

-				}

-				if (resource.getName().toLowerCase().endsWith(".jar")) {

+				} else {

 					final InputStream in = resource.getInputStream();

-					analyzer.analyzeJAR(in);

-					in.close();

-					continue;

-				}

-				if (resource.getName().toLowerCase().endsWith(".class")) {

-					final InputStream in = resource.getInputStream();

-					analyzer.analyze(in);

+					analyzer.analyzeAll(in);

 					in.close();

 				}

 			}

diff --git a/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java b/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
index 9a1cccb..885087c 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/instr/AnalyzerTest.java
@@ -12,12 +12,23 @@
  *******************************************************************************/

 package org.jacoco.core.instr;

 

-import java.io.File;

+import static junit.framework.Assert.assertTrue;

+import static org.junit.Assert.assertEquals;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

 import java.io.IOException;

+import java.util.Collections;

+import java.util.HashSet;

+import java.util.Set;

+import java.util.zip.ZipEntry;

+import java.util.zip.ZipOutputStream;

 

 import org.jacoco.core.data.IClassStructureVisitor;

 import org.jacoco.core.data.IMethodStructureVisitor;

 import org.jacoco.core.data.IStructureVisitor;

+import org.jacoco.core.test.TargetLoader;

+import org.junit.Before;

 import org.junit.Test;

 

 /**

@@ -28,14 +39,11 @@
  */

 public class AnalyzerTest {

 

-	@Test(expected = IOException.class)

-	public void testInvalidDirectory() throws IOException {

-		Analyzer analyzer = new Analyzer(new EmptyStructureVisitor());

-		File invalid = new File("/this/path/should/not/exist/");

-		analyzer.analyzeAll(invalid);

-	}

+	private Analyzer analyzer;

 

-	private static class EmptyStructureVisitor implements IStructureVisitor,

+	private final Set<String> classes = new HashSet<String>();

+

+	private class EmptyStructureVisitor implements IStructureVisitor,

 			IClassStructureVisitor, IMethodStructureVisitor {

 

 		public IClassStructureVisitor visitClassStructure(long id) {

@@ -44,6 +52,7 @@
 

 		public void visit(String name, String signature, String superName,

 				String[] interfaces) {

+			assertTrue("Class already processed: " + name, classes.add(name));

 		}

 

 		public void visitSourceFile(String name) {

@@ -62,4 +71,71 @@
 

 	}

 

+	@Before

+	public void setup() {

+		analyzer = new Analyzer(new EmptyStructureVisitor());

+	}

+

+	@Test

+	public void testAnalyzeClass1() throws IOException {

+		analyzer.analyzeClass(TargetLoader.getClassData(AnalyzerTest.class));

+		assertEquals(Collections

+				.singleton("org/jacoco/core/instr/AnalyzerTest"), classes);

+	}

+

+	@Test

+	public void testAnalyzeClass2() throws IOException {

+		analyzer.analyzeClass(TargetLoader

+				.getClassDataAsBytes(AnalyzerTest.class));

+		assertEquals(Collections

+				.singleton("org/jacoco/core/instr/AnalyzerTest"), classes);

+	}

+

+	@Test

+	public void testAnalyzeArchive() throws IOException {

+		final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

+		final ZipOutputStream zip = new ZipOutputStream(buffer);

+		zip.putNextEntry(new ZipEntry(

+				"org/jacoco/core/instr/AnalyzerTest.class"));

+		zip.write(TargetLoader.getClassDataAsBytes(AnalyzerTest.class));

+		zip.finish();

+		final int count = analyzer.analyzeArchive(new ByteArrayInputStream(

+				buffer.toByteArray()));

+		assertEquals(1, count);

+		assertEquals(Collections

+				.singleton("org/jacoco/core/instr/AnalyzerTest"), classes);

+	}

+

+	@Test

+	public void testAnalyzeAll1() throws IOException {

+		final int count = analyzer.analyzeAll(TargetLoader

+				.getClassData(AnalyzerTest.class));

+		assertEquals(1, count);

+		assertEquals(Collections

+				.singleton("org/jacoco/core/instr/AnalyzerTest"), classes);

+	}

+

+	@Test

+	public void testAnalyzeAll2() throws IOException {

+		final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

+		final ZipOutputStream zip = new ZipOutputStream(buffer);

+		zip.putNextEntry(new ZipEntry(

+				"org/jacoco/core/instr/AnalyzerTest.class"));

+		zip.write(TargetLoader.getClassDataAsBytes(AnalyzerTest.class));

+		zip.finish();

+		final int count = analyzer.analyzeAll(new ByteArrayInputStream(buffer

+				.toByteArray()));

+		assertEquals(1, count);

+		assertEquals(Collections

+				.singleton("org/jacoco/core/instr/AnalyzerTest"), classes);

+	}

+

+	@Test

+	public void testAnalyzeAll3() throws IOException {

+		final int count = analyzer.analyzeAll(new ByteArrayInputStream(

+				new byte[0]));

+		assertEquals(0, count);

+		assertEquals(Collections.emptySet(), classes);

+	}

+

 }

diff --git a/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java b/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java
new file mode 100644
index 0000000..c968a0e
--- /dev/null
+++ b/org.jacoco.core.test/src/org/jacoco/core/instr/ContentTypeDetectorTest.java
@@ -0,0 +1,109 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2010 Mountainminds GmbH & Co. KG and Contributors

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *    Marc R. Hoffmann - initial API and implementation

+ *    

+ * $Id: $

+ *******************************************************************************/

+package org.jacoco.core.instr;

+

+import static junit.framework.Assert.assertEquals;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+import java.util.zip.ZipEntry;

+import java.util.zip.ZipOutputStream;

+

+import org.jacoco.core.test.TargetLoader;

+import org.junit.Test;

+

+/**

+ * Unit tests for {@link ContentTypeDetector}.

+ * 

+ * @author Marc R. Hoffmann

+ * @version $Revision: $

+ */

+public class ContentTypeDetectorTest {

+

+	private byte[] data;

+

+	private ContentTypeDetector detector;

+

+	@Test

+	public void testEmptyStream() throws IOException {

+		initData();

+		assertContent();

+	}

+

+	@Test

+	public void testClassFile() throws IOException {

+		initData(TargetLoader

+				.getClassDataAsBytes(ContentTypeDetectorTest.class));

+		assertEquals(ContentTypeDetector.CLASSFILE, detector.getHeader());

+		assertContent();

+	}

+

+	@Test

+	public void testZipFile() throws IOException {

+		final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

+		final ZipOutputStream zip = new ZipOutputStream(buffer);

+		zip.putNextEntry(new ZipEntry("hello.txt"));

+		zip.write("Hello Zip!".getBytes());

+		zip.close();

+		initData(buffer.toByteArray());

+		System.out.println(Integer.toHexString(detector.getHeader()));

+		assertEquals(ContentTypeDetector.ZIPFILE, detector.getHeader());

+		assertContent();

+	}

+

+	@Test

+	public void testStreamWithoutMarkSupport() throws IOException {

+		initData(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 07);

+		detector = new ContentTypeDetector(new ByteArrayInputStream(data) {

+

+			@Override

+			public void mark(int readlimit) {

+			}

+

+			@Override

+			public void reset() {

+			}

+

+			@Override

+			public boolean markSupported() {

+				return false;

+			}

+

+		});

+		assertContent();

+	}

+

+	private void initData(byte[] bytes) throws IOException {

+		this.data = bytes;

+		this.detector = new ContentTypeDetector(new ByteArrayInputStream(data));

+	}

+

+	private void initData(final int... bytes) throws IOException {

+		byte[] data = new byte[bytes.length];

+		for (int i = 0; i < bytes.length; i++) {

+			data[i] = (byte) bytes[i];

+		}

+		initData(data);

+	}

+

+	private void assertContent() throws IOException {

+		final InputStream actual = detector.getInputStream();

+		for (int b : data) {

+			assertEquals(b, (byte) actual.read());

+		}

+		assertEquals(-1, actual.read());

+	}

+

+}

diff --git a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
index a97bea6..27aa8a8 100644
--- a/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
+++ b/org.jacoco.core.test/src/org/jacoco/core/test/validation/ValidationTestBase.java
@@ -89,7 +89,7 @@
 			final ExecutionDataStore store) {

 		final CoverageBuilder builder = new CoverageBuilder(store);

 		final Analyzer analyzer = new Analyzer(builder);

-		analyzer.analyze(reader);

+		analyzer.analyzeClass(reader);

 		final Collection<ClassCoverage> classes = builder.getClasses();

 		assertEquals(1, classes.size(), 0.0);

 		classCoverage = classes.iterator().next();

diff --git a/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java b/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
index 17b966b..dd08ef0 100644
--- a/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
+++ b/org.jacoco.core/src/org/jacoco/core/instr/Analyzer.java
@@ -12,8 +12,6 @@
  *******************************************************************************/

 package org.jacoco.core.instr;

 

-import static java.lang.String.format;

-

 import java.io.File;

 import java.io.FileInputStream;

 import java.io.IOException;

@@ -66,7 +64,7 @@
 	 * @param reader

 	 *            reader with class definitions

 	 */

-	public void analyze(final ClassReader reader) {

+	public void analyzeClass(final ClassReader reader) {

 		final ClassVisitor visitor = createAnalyzingVisitor(CRC64

 				.checksum(reader.b));

 		reader.accept(visitor, 0);

@@ -78,8 +76,8 @@
 	 * @param buffer

 	 *            class definitions

 	 */

-	public void analyze(final byte[] buffer) {

-		analyze(new ClassReader(buffer));

+	public void analyzeClass(final byte[] buffer) {

+		analyzeClass(new ClassReader(buffer));

 	}

 

 	/**

@@ -89,84 +87,82 @@
 	 *            stream to read class definition from

 	 * @throws IOException

 	 */

-	public void analyze(final InputStream input) throws IOException {

-		analyze(new ClassReader(input));

+	public void analyzeClass(final InputStream input) throws IOException {

+		analyzeClass(new ClassReader(input));

 	}

 

 	/**

-	 * Analyzes the class definition contained in a given file.

-	 * 

-	 * @param file

-	 *            class file

-	 * @throws IOException

-	 */

-	public void analyze(final File file) throws IOException {

-		final InputStream in = new FileInputStream(file);

-		analyze(new ClassReader(in));

-		in.close();

-	}

-

-	/**

-	 * Analyzes all class files contained in the given directory and its

-	 * children.

-	 * 

-	 * @param directory

-	 *            folder to look for class files

-	 * @throws IOException

-	 *             thrown if the given file object does not represent a readable

-	 *             directory

-	 */

-	public void analyzeAll(final File directory) throws IOException {

-		final File[] files = directory.listFiles();

-		if (files == null) {

-			throw new IOException(format("Can't read directory %s.", directory));

-		}

-		for (final File f : files) {

-			if (f.isDirectory()) {

-				analyzeAll(f);

-				continue;

-			}

-			if (f.getName().endsWith(".class")) {

-				analyze(f);

-			}

-		}

-	}

-

-	/**

-	 * Analyzes all class files contained in a JAR file.

+	 * Analyzes all classes contained in the ZIP archive (jar, war, ear, etc.)

+	 * given as an input stream. Contained archives are read recursively.

 	 * 

 	 * @param input

-	 *            stream to read the JAR file from

+	 *            ZIP archive data

+	 * @return number of class files found

 	 * @throws IOException

 	 */

-	public void analyzeJAR(final InputStream input) throws IOException {

+	public int analyzeArchive(final InputStream input) throws IOException {

 		final ZipInputStream zip = new ZipInputStream(input);

+		int count = 0;

 		while (true) {

 			final ZipEntry entry = zip.getNextEntry();

 			if (entry == null) {

 				break;

 			}

-			if (entry.getName().endsWith(".class")) {

-				analyze(zip);

-			}

+			count += analyzeAll(zip);

 		}

+		return count;

 	}

 

 	/**

-	 * Analyzes all class files contained in a JAR file.

+	 * Analyzes all classes found in the given input stream. The input stream

+	 * may either represent a single class file or a ZIP archive that is

+	 * searched recursively for class files. All other content types are

+	 * ignored.

 	 * 

-	 * @param jarfile

-	 *            JAR file

+	 * @param input

+	 *            input data

+	 * @return number of class files found

 	 * @throws IOException

 	 */

-	public void analyzeJAR(final File jarfile) throws IOException {

-		final InputStream in = new FileInputStream(jarfile);

-		analyzeJAR(in);

-		in.close();

+	public int analyzeAll(final InputStream input) throws IOException {

+		final ContentTypeDetector detector = new ContentTypeDetector(input);

+		switch (detector.getHeader()) {

+		case ContentTypeDetector.CLASSFILE:

+			analyzeClass(detector.getInputStream());

+			return 1;

+		case ContentTypeDetector.ZIPFILE:

+			return analyzeArchive(detector.getInputStream());

+		}

+		return 0;

 	}

 

 	/**

-	 * Analyzes all class from the given class path.

+	 * Analyzes all class files contained in the given file or folder. Class

+	 * files as well as ZIP files are considered. Folders are searched

+	 * recursively.

+	 * 

+	 * @param file

+	 *            file or folder to look for class files

+	 * @return number of class files found

+	 * @throws IOException

+	 */

+	public int analyzeAll(final File file) throws IOException {

+		int count = 0;

+		if (file.isDirectory()) {

+			for (final File f : file.listFiles()) {

+				count += analyzeAll(f);

+			}

+		} else {

+			final InputStream in = new FileInputStream(file);

+			count += analyzeAll(in);

+			in.close();

+		}

+		return count;

+	}

+

+	/**

+	 * Analyzes all classes from the given class path. Directories containing

+	 * class files as well as archive files are considered.

 	 * 

 	 * @param path

 	 *            path definition

@@ -174,22 +170,17 @@
 	 *            optional base directory, if <code>null</code> the current

 	 *            working directory is used as the base for relative path

 	 *            entries

+	 * @return number of class files found

 	 * @throws IOException

 	 */

-	public void analyzePath(final String path, final File basedir)

+	public int analyzeAll(final String path, final File basedir)

 			throws IOException {

-		final StringTokenizer tokenizer = new StringTokenizer(path,

-				File.pathSeparator);

-		while (tokenizer.hasMoreTokens()) {

-			final File entry = new File(basedir, tokenizer.nextToken());

-			if (entry.isDirectory()) {

-				analyzeAll(entry);

-				continue;

-			}

-			if (entry.isFile() && entry.getName().endsWith(".jar")) {

-				analyzeJAR(entry);

-			}

+		int count = 0;

+		final StringTokenizer st = new StringTokenizer(path, File.pathSeparator);

+		while (st.hasMoreTokens()) {

+			count += analyzeAll(new File(basedir, st.nextToken()));

 		}

+		return count;

 	}

 

 }

diff --git a/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java b/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java
new file mode 100644
index 0000000..57f6e22
--- /dev/null
+++ b/org.jacoco.core/src/org/jacoco/core/instr/ContentTypeDetector.java
@@ -0,0 +1,82 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2010 Mountainminds GmbH & Co. KG and Contributors

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *    Marc R. Hoffmann - initial API and implementation

+ *    

+ * $Id: $

+ *******************************************************************************/

+package org.jacoco.core.instr;

+

+import java.io.BufferedInputStream;

+import java.io.IOException;

+import java.io.InputStream;

+

+/**

+ * Detector for content types of binary streams based on a magic headers.

+ * 

+ * @author Marc R. Hoffmann

+ * @version $Revision: $

+ */

+class ContentTypeDetector {

+

+	/** Header of Java class files */

+	public static final int CLASSFILE = 0xcafebabe;

+

+	/** Header of ZIP files */

+	public static final int ZIPFILE = 0x504b0304;

+

+	private static final int HEADER_SIZE = 4;

+

+	private final InputStream in;

+

+	private final int header;

+

+	/**

+	 * Creates a new detector based on the given input. To process the complete

+	 * original input afterwards use the stream returned by

+	 * {@link #getInputStream()}.

+	 * 

+	 * @param in

+	 *            input to read the header from

+	 * @throws IOException

+	 */

+	ContentTypeDetector(final InputStream in) throws IOException {

+		if (in.markSupported()) {

+			this.in = in;

+		} else {

+			this.in = new BufferedInputStream(in, HEADER_SIZE);

+		}

+		this.in.mark(HEADER_SIZE);

+		this.header = readHeader(this.in);

+		this.in.reset();

+	}

+

+	private static int readHeader(final InputStream in) throws IOException {

+		return in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();

+	}

+

+	/**

+	 * Returns an input stream instance to read the complete content (including

+	 * the header) of the underlying stream.

+	 * 

+	 * @return input stream containing the complete content

+	 */

+	public InputStream getInputStream() {

+		return in;

+	}

+

+	/**

+	 * Returns the file header containing the magic number.

+	 * 

+	 * @return file header

+	 */

+	public int getHeader() {

+		return header;

+	}

+

+}

diff --git a/org.jacoco.doc/buildhook.xml b/org.jacoco.doc/buildhook.xml
index 3a05f9a..27ba72f 100644
--- a/org.jacoco.doc/buildhook.xml
+++ b/org.jacoco.doc/buildhook.xml
@@ -46,7 +46,8 @@
 			<structure name="JaCoCo">

 				<group name="org.jacoco.agent">

 					<classfiles>

-						<path refid="bundle-org.jacoco.agent"/>

+						<!-- Process class files only, ignore jacocoagent.jar -->

+						<fileset dir="${toString:bundle-org.jacoco.agent}" includes="**/*.class"/>
 					</classfiles>

 					<sourcefiles>

 						<fileset dir="${workspace.dir}/org.jacoco.agent/src"/>

diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index 4403350..0531576 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -17,6 +17,14 @@
 

 <h1>Change History</h1>

 

+<h2>Next Release</h2>

+

+<h3>New Features</h3>

+<ul>

+  <li>Support for different archives (jar, war, ear etc.) and nested archives

+  (Trac #78).</li>

+</ul>

+

 <h2>Release 0.3.2 (2010/04/01)</h2>

 

 <h3>New Features</h3>

diff --git a/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java b/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
index b3ca24c..4b892c3 100644
--- a/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
+++ b/org.jacoco.examples/src/org/jacoco/examples/CoreTutorial.java
@@ -151,7 +151,7 @@
 		final CoverageBuilder coverageBuilder = new CoverageBuilder(

 				executionData);

 		final Analyzer analyzer = new Analyzer(coverageBuilder);

-		analyzer.analyze(getTargetClass(targetName));

+		analyzer.analyzeClass(getTargetClass(targetName));

 

 		// Let's dump some metrics and line coverage information:

 		for (final ClassCoverage cc : coverageBuilder.getClasses()) {