Trac #53: Simplified reporting API, step 1.
diff --git a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
index 46f2eb5..511f853 100644
--- a/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
+++ b/org.jacoco.ant/src/org/jacoco/ant/ReportTask.java
@@ -21,13 +21,11 @@
 import java.io.InputStreamReader;

 import java.io.Reader;

 import java.util.ArrayList;

-import java.util.Collection;

 import java.util.HashMap;

 import java.util.Iterator;

 import java.util.List;

 import java.util.Locale;

 import java.util.Map;

-import java.util.zip.ZipOutputStream;

 

 import org.apache.tools.ant.BuildException;

 import org.apache.tools.ant.Project;

@@ -38,22 +36,17 @@
 import org.apache.tools.ant.util.FileUtils;

 import org.jacoco.core.analysis.Analyzer;

 import org.jacoco.core.analysis.CoverageBuilder;

-import org.jacoco.core.analysis.CoverageNodeImpl;

 import org.jacoco.core.analysis.IBundleCoverage;

-import org.jacoco.core.analysis.IClassCoverage;

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.core.analysis.IPackageCoverage;

 import org.jacoco.core.data.ExecutionDataReader;

 import org.jacoco.core.data.ExecutionDataStore;

 import org.jacoco.core.data.SessionInfoStore;

 import org.jacoco.report.FileMultiReportOutput;

-import org.jacoco.report.FileSingleReportOutput;

 import org.jacoco.report.IMultiReportOutput;

-import org.jacoco.report.IReportFormatter;

+import org.jacoco.report.IReportGroupVisitor;

 import org.jacoco.report.IReportVisitor;

 import org.jacoco.report.ISourceFileLocator;

-import org.jacoco.report.MultiFormatter;

+import org.jacoco.report.MultiReportVisitor;

 import org.jacoco.report.ZipMultiReportOutput;

 import org.jacoco.report.csv.CSVFormatter;

 import org.jacoco.report.html.HTMLFormatter;

@@ -145,9 +138,7 @@
 	 */

 	private interface IFormatterElement {

 

-		IReportFormatter createFormatter() throws IOException;

-

-		void finish() throws IOException;

+		IReportVisitor createVisitor() throws IOException;

 

 	}

 

@@ -166,8 +157,6 @@
 

 		private Locale locale = Locale.getDefault();

 

-		private ZipOutputStream zipOutput;

-

 		/**

 		 * Sets the output directory for the report.

 		 * 

@@ -220,15 +209,15 @@
 			this.locale = locale;

 		}

 

-		public IReportFormatter createFormatter() throws IOException {

+		public IReportVisitor createVisitor() throws IOException {

 			final IMultiReportOutput output;

 			if (destfile != null) {

 				if (destdir != null) {

 					throw new BuildException(

 							"Either destination directory or file must be supplied, not both");

 				}

-				zipOutput = new ZipOutputStream(new FileOutputStream(destfile));

-				output = new ZipMultiReportOutput(zipOutput);

+				final FileOutputStream stream = new FileOutputStream(destfile);

+				output = new ZipMultiReportOutput(stream);

 

 			} else {

 				if (destdir == null) {

@@ -238,17 +227,10 @@
 				output = new FileMultiReportOutput(destdir);

 			}

 			final HTMLFormatter formatter = new HTMLFormatter();

-			formatter.setReportOutput(output);

 			formatter.setFooterText(footer);

 			formatter.setOutputEncoding(encoding);

 			formatter.setLocale(locale);

-			return formatter;

-		}

-

-		public void finish() throws IOException {

-			if (zipOutput != null) {

-				zipOutput.close();

-			}

+			return formatter.createVisitor(output);

 		}

 

 	}

@@ -272,15 +254,14 @@
 			this.destfile = destfile;

 		}

 

-		public IReportFormatter createFormatter() {

+		public IReportVisitor createVisitor() throws IOException {

 			if (destfile == null) {

 				throw new BuildException(

 						"Destination file must be supplied for csv report");

 			}

 			final CSVFormatter formatter = new CSVFormatter();

-			formatter.setReportOutput(new FileSingleReportOutput(destfile));

 			formatter.setOutputEncoding(encoding);

-			return formatter;

+			return formatter.createVisitor(new FileOutputStream(destfile));

 		}

 

 		/**

@@ -293,9 +274,6 @@
 			this.encoding = encoding;

 		}

 

-		public void finish() {

-		}

-

 	}

 

 	/**

@@ -327,18 +305,14 @@
 			this.encoding = encoding;

 		}

 

-		public IReportFormatter createFormatter() {

+		public IReportVisitor createVisitor() throws IOException {

 			if (destfile == null) {

 				throw new BuildException(

 						"Destination file must be supplied for xml report");

 			}

 			final XMLFormatter formatter = new XMLFormatter();

-			formatter.setReportOutput(new FileSingleReportOutput(destfile));

 			formatter.setOutputEncoding(encoding);

-			return formatter;

-		}

-

-		public void finish() {

+			return formatter.createVisitor(new FileOutputStream(destfile));

 		}

 

 	}

@@ -408,9 +382,11 @@
 	public void execute() throws BuildException {

 		loadExecutionData();

 		try {

-			final IReportFormatter formatter = createFormatter();

-			createReport(formatter);

-			finishFormatters();

+			final IReportVisitor visitor = createVisitor();

+			visitor.visitInfo(sessionInfoStore.getInfos(),

+					executionDataStore.getContents());

+			createReport(visitor, structure);

+			visitor.visitEnd();

 		} catch (final IOException e) {

 			throw new BuildException("Error while creating report.", e);

 		}

@@ -437,76 +413,59 @@
 		}

 	}

 

-	private IReportFormatter createFormatter() throws IOException {

-		final MultiFormatter multi = new MultiFormatter();

+	private IReportVisitor createVisitor() throws IOException {

+		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();

 		for (final IFormatterElement f : formatters) {

-			multi.add(f.createFormatter());

+			visitors.add(f.createVisitor());

 		}

-		return multi;

+		return new MultiReportVisitor(visitors);

 	}

 

-	private void finishFormatters() throws IOException {

-		for (final IFormatterElement f : formatters) {

-			f.finish();

-		}

-	}

-

-	private void createReport(final IReportFormatter formatter)

-			throws IOException {

-		final CoverageNodeImpl node = createNode(structure);

-		final IReportVisitor visitor = formatter.createReportVisitor(node,

-				sessionInfoStore.getInfos(), executionDataStore.getContents());

-		final SourceFileCollection sourceFileLocator = new SourceFileCollection(

-				structure.sourcefiles);

-		if (node.getElementType() == ElementType.BUNDLE) {

-			visitBundle(visitor, (IBundleCoverage) node, sourceFileLocator);

-		} else {

-			for (final GroupElement g : structure.children) {

-				createReport(g, visitor, node);

-			}

-		}

-		visitor.visitEnd(sourceFileLocator);

-	}

-

-	private void createReport(final GroupElement group,

-			final IReportVisitor parentVisitor,

-			final CoverageNodeImpl parentNode) throws IOException {

-		final CoverageNodeImpl node = createNode(group);

-		final IReportVisitor visitor = parentVisitor.visitChild(node);

-		final SourceFileCollection sourceFileLocator = new SourceFileCollection(

-				group.sourcefiles);

-		if (node.getElementType() == ElementType.BUNDLE) {

-			visitBundle(visitor, (IBundleCoverage) node, sourceFileLocator);

-		} else {

-			for (final GroupElement g : group.children) {

-				createReport(g, visitor, node);

-			}

-		}

-		parentNode.increment(node);

-		visitor.visitEnd(sourceFileLocator);

-	}

-

-	private CoverageNodeImpl createNode(final GroupElement group)

-			throws IOException {

+	private void createReport(final IReportGroupVisitor visitor,

+			final GroupElement group) throws IOException {

 		if (group.name == null) {

 			throw new BuildException("Group name must be supplied");

 		}

 		if (group.children.size() > 0) {

-			return new CoverageNodeImpl(ElementType.GROUP, group.name);

-		} else {

-			final CoverageBuilder builder = new CoverageBuilder();

-			final Analyzer analyzer = new Analyzer(executionDataStore, builder);

-			for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {

-				final Resource resource = (Resource) i.next();

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

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

-				} else {

-					final InputStream in = resource.getInputStream();

-					analyzer.analyzeAll(in);

-					in.close();

-				}

+			final IReportGroupVisitor groupVisitor = visitor

+					.visitGroup(group.name);

+			for (final GroupElement child : group.children) {

+				createReport(groupVisitor, child);

 			}

-			return builder.getBundle(group.name);

+		} else {

+			final IBundleCoverage bundle = createBundle(group);

+			final SourceFileCollection locator = new SourceFileCollection(

+					group.sourcefiles);

+			if (!locator.isEmpty()) {

+				checkForMissingDebugInformation(bundle);

+			}

+			visitor.visitBundle(bundle, locator);

+		}

+	}

+

+	private IBundleCoverage createBundle(final GroupElement group)

+			throws IOException {

+		final CoverageBuilder builder = new CoverageBuilder();

+		final Analyzer analyzer = new Analyzer(executionDataStore, builder);

+		for (final Iterator<?> i = group.classfiles.iterator(); i.hasNext();) {

+			final Resource resource = (Resource) i.next();

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

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

+			} else {

+				final InputStream in = resource.getInputStream();

+				analyzer.analyzeAll(in);

+				in.close();

+			}

+		}

+		return builder.getBundle(group.name);

+	}

+

+	private void checkForMissingDebugInformation(final ICoverageNode node) {

+		if (node.getClassCounter().getTotalCount() > 0

+				&& node.getLineCounter().getTotalCount() == 0) {

+			log(format(

+					"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",

+					node.getName()), Project.MSG_WARN);

 		}

 	}

 

@@ -542,50 +501,4 @@
 		}

 	}

 

-	private void visitBundle(final IReportVisitor visitor,

-			final IBundleCoverage bundledata,

-			final SourceFileCollection sourceFileLocator) throws IOException {

-		if (!sourceFileLocator.isEmpty()) {

-			checkForMissingDebugInformation(bundledata);

-		}

-		for (final IPackageCoverage p : bundledata.getPackages()) {

-			visitPackage(visitor.visitChild(p), p, sourceFileLocator);

-		}

-	}

-

-	private void checkForMissingDebugInformation(final ICoverageNode node) {

-		if (node.getClassCounter().getTotalCount() > 0

-				&& node.getLineCounter().getTotalCount() == 0) {

-			log(format(

-					"To enable source code annotation class files for bundle '%s' have to be compiled with debug information.",

-					node.getName()), Project.MSG_WARN);

-		}

-	}

-

-	private static void visitPackage(final IReportVisitor visitor,

-			final IPackageCoverage packagedata,

-			final ISourceFileLocator sourceFileLocator) throws IOException {

-		visitLeafs(visitor, packagedata.getSourceFiles(), sourceFileLocator);

-		for (final IClassCoverage c : packagedata.getClasses()) {

-			visitClass(visitor.visitChild(c), c, sourceFileLocator);

-		}

-		visitor.visitEnd(sourceFileLocator);

-	}

-

-	private static void visitClass(final IReportVisitor visitor,

-			final IClassCoverage classdata,

-			final ISourceFileLocator sourceFileLocator) throws IOException {

-		visitLeafs(visitor, classdata.getMethods(), sourceFileLocator);

-		visitor.visitEnd(sourceFileLocator);

-	}

-

-	private static void visitLeafs(final IReportVisitor visitor,

-			final Collection<? extends ICoverageNode> leafs,

-			final ISourceFileLocator sourceFileLocator) throws IOException {

-		for (final ICoverageNode l : leafs) {

-			final IReportVisitor child = visitor.visitChild(l);

-			child.visitEnd(sourceFileLocator);

-		}

-	}

-

 }

diff --git a/org.jacoco.doc/docroot/doc/changes.html b/org.jacoco.doc/docroot/doc/changes.html
index ff2efaa..a887479 100644
--- a/org.jacoco.doc/docroot/doc/changes.html
+++ b/org.jacoco.doc/docroot/doc/changes.html
@@ -32,6 +32,11 @@
   <li>Removed obsolete examples from documentation (Trac #141).</li>

 </ul>

 

+<h3>API Changes</h3>

+<ul>

+  <li>Simplified reporting API (Trac #53).</li>

+</ul>

+

 <h2>Release 0.5.0 (2011/01/19)</h2>

 

 <h3>New Features</h3>

diff --git a/org.jacoco.report.test/src/org/jacoco/report/FileSingleReportOutputTest.java b/org.jacoco.report.test/src/org/jacoco/report/FileSingleReportOutputTest.java
deleted file mode 100644
index fb58957..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/FileSingleReportOutputTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import static org.junit.Assert.assertEquals;

-

-import java.io.File;

-import java.io.FileInputStream;

-import java.io.IOException;

-import java.io.InputStream;

-import java.io.OutputStream;

-

-import org.junit.Rule;

-import org.junit.Test;

-import org.junit.rules.TemporaryFolder;

-

-/**

- * Unit tests for {@link FileSingleReportOutput}.

- */

-public class FileSingleReportOutputTest {

-

-	@Rule

-	public TemporaryFolder folder = new TemporaryFolder();

-

-	@Test

-	public void testCreateFileWithDirectories() throws IOException {

-		final File f = new File(folder.getRoot(), "a/b/c/test");

-

-		final ISingleReportOutput output = new FileSingleReportOutput(f);

-		final OutputStream stream = output.createFile();

-		stream.write(1);

-		stream.write(2);

-		stream.write(3);

-		stream.close();

-

-		final InputStream actual = new FileInputStream(f);

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

-		assertEquals(2, actual.read());

-		assertEquals(3, actual.read());

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

-	}

-

-	@Test(expected = IOException.class)

-	public void testCreateFileNegative() throws IOException {

-		final File d = folder.newFile("a");

-		final File f = new File(d, "b/c/test");

-		final ISingleReportOutput output = new FileSingleReportOutput(f);

-		output.createFile();

-	}

-

-}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/MemoryMultiReportOutput.java b/org.jacoco.report.test/src/org/jacoco/report/MemoryMultiReportOutput.java
index 76caca0..982726b 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/MemoryMultiReportOutput.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/MemoryMultiReportOutput.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertFalse;

 import static org.junit.Assert.assertNotNull;

+import static org.junit.Assert.assertTrue;

 

 import java.io.ByteArrayInputStream;

 import java.io.ByteArrayOutputStream;

@@ -35,6 +36,8 @@
 

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

 

+	private boolean closed = false;

+

 	public OutputStream createFile(final String path) throws IOException {

 		assertFalse("Duplicate output " + path, files.containsKey(path));

 		open.add(path);

@@ -49,6 +52,10 @@
 		return out;

 	}

 

+	public void close() throws IOException {

+		closed = true;

+	}

+

 	public void assertEmpty() {

 		assertEquals(Collections.emptySet(), files.keySet());

 	}

@@ -73,6 +80,7 @@
 

 	public void assertAllClosed() {

 		assertEquals(Collections.emptySet(), open);

+		assertTrue(closed);

 	}

 

 }

diff --git a/org.jacoco.report.test/src/org/jacoco/report/MemoryOutput.java b/org.jacoco.report.test/src/org/jacoco/report/MemoryOutput.java
new file mode 100644
index 0000000..c1f90dc
--- /dev/null
+++ b/org.jacoco.report.test/src/org/jacoco/report/MemoryOutput.java
@@ -0,0 +1,42 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report;

+

+import static org.junit.Assert.assertTrue;

+

+import java.io.ByteArrayInputStream;

+import java.io.ByteArrayOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+

+/**

+ * In-memory report output for test purposes.

+ */

+public class MemoryOutput extends ByteArrayOutputStream {

+

+	private boolean closed = false;

+

+	@Override

+	public void close() throws IOException {

+		super.close();

+		closed = true;

+	}

+

+	public InputStream getContentsAsStream() {

+		return new ByteArrayInputStream(toByteArray());

+	}

+

+	public void assertClosed() {

+		assertTrue(closed);

+	}

+

+}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/MemorySingleReportOutput.java b/org.jacoco.report.test/src/org/jacoco/report/MemorySingleReportOutput.java
deleted file mode 100644
index f3e1948..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/MemorySingleReportOutput.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import static org.junit.Assert.assertNotNull;

-import static org.junit.Assert.assertNull;

-import static org.junit.Assert.assertTrue;

-

-import java.io.ByteArrayInputStream;

-import java.io.ByteArrayOutputStream;

-import java.io.IOException;

-import java.io.InputStream;

-import java.io.OutputStream;

-

-/**

- * In-memory report output for test purposes.

- */

-public class MemorySingleReportOutput implements ISingleReportOutput {

-

-	private ByteArrayOutputStream file;

-

-	private boolean closed = true;

-

-	public OutputStream createFile() throws IOException {

-		assertNull("Duplicate output.", file);

-		closed = false;

-		file = new ByteArrayOutputStream() {

-

-			@Override

-			public void close() throws IOException {

-				closed = true;

-				super.close();

-			}

-		};

-		return file;

-	}

-

-	public byte[] getFile() {

-		assertNotNull("Missing file.", file);

-		return file.toByteArray();

-	}

-

-	public InputStream getFileAsStream() {

-		return new ByteArrayInputStream(getFile());

-	}

-

-	public void assertClosed() {

-		assertTrue(closed);

-	}

-

-}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/MultiFormatterTest.java b/org.jacoco.report.test/src/org/jacoco/report/MultiFormatterTest.java
deleted file mode 100644
index 42adfe5..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/MultiFormatterTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertFalse;

-import static org.junit.Assert.assertTrue;

-

-import java.io.IOException;

-import java.util.ArrayList;

-import java.util.Collection;

-import java.util.Collections;

-import java.util.List;

-

-import org.jacoco.core.analysis.CoverageNodeImpl;

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.core.data.ExecutionData;

-import org.jacoco.core.data.SessionInfo;

-import org.junit.Test;

-

-/**

- * Unit tests for {@link MultiFormatter}.

- */

-public class MultiFormatterTest {

-

-	private static class MockFormatter implements IReportFormatter {

-

-		private MockVisitor visitor;

-

-		public IReportVisitor createReportVisitor(ICoverageNode root,

-				List<SessionInfo> sessionInfos,

-				final Collection<ExecutionData> executionData)

-				throws IOException {

-			visitor = new MockVisitor(root);

-			return visitor;

-		}

-

-		@Override

-		public String toString() {

-			return visitor.toString();

-		}

-

-	}

-

-	private static class MockVisitor implements IReportVisitor {

-

-		private final String name;

-

-		private final List<MockVisitor> children = new ArrayList<MockVisitor>();

-

-		private boolean visitEndCalled = false;

-

-		MockVisitor(ICoverageNode node) {

-			name = node.getName();

-		}

-

-		public IReportVisitor visitChild(ICoverageNode node) throws IOException {

-			assertFalse("visitEnd() already called", visitEndCalled);

-			MockVisitor child = new MockVisitor(node);

-			children.add(child);

-			return child;

-		}

-

-		public void visitEnd(ISourceFileLocator sourceFileLocator)

-				throws IOException {

-			assertFalse("visitEnd() already called", visitEndCalled);

-			visitEndCalled = true;

-		}

-

-		@Override

-		public String toString() {

-			assertTrue("visitEnd() has not been called", visitEndCalled);

-			return name + children;

-		}

-	}

-

-	private CoverageNodeImpl createNode(String name) {

-		return new CoverageNodeImpl(ElementType.GROUP, name);

-	}

-

-	private static final String MOCK_REPORT = "Session[b1[p1[], p2[]], b2[]]";

-

-	private void createMockReport(IReportFormatter formatter)

-			throws IOException {

-		final List<SessionInfo> sessions = Collections.emptyList();

-		final Collection<ExecutionData> data = Collections.emptyList();

-		IReportVisitor root = formatter.createReportVisitor(

-				createNode("Session"), sessions, data);

-		{

-			IReportVisitor b1 = root.visitChild(createNode("b1"));

-			{

-				IReportVisitor p1 = b1.visitChild(createNode("p1"));

-				p1.visitEnd(null);

-			}

-			{

-				IReportVisitor p2 = b1.visitChild(createNode("p2"));

-				p2.visitEnd(null);

-			}

-			b1.visitEnd(null);

-		}

-		{

-			IReportVisitor b2 = root.visitChild(createNode("b2"));

-			b2.visitEnd(null);

-		}

-		root.visitEnd(null);

-	}

-

-	@Test

-	public void testMockFormatter() throws IOException {

-		MockFormatter formatter = new MockFormatter();

-		createMockReport(formatter);

-		assertEquals(MOCK_REPORT, formatter.toString());

-	}

-

-	@Test

-	public void testMultiFormatter() throws IOException {

-		MockFormatter mock1 = new MockFormatter();

-		MockFormatter mock2 = new MockFormatter();

-		MockFormatter mock3 = new MockFormatter();

-		MultiFormatter multi = new MultiFormatter();

-		multi.add(mock1);

-		multi.add(mock2);

-		multi.add(mock3);

-		createMockReport(multi);

-		assertEquals(MOCK_REPORT, mock1.toString());

-		assertEquals(MOCK_REPORT, mock2.toString());

-		assertEquals(MOCK_REPORT, mock3.toString());

-	}

-

-}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/MultiReportVisitorTest.java b/org.jacoco.report.test/src/org/jacoco/report/MultiReportVisitorTest.java
new file mode 100644
index 0000000..30b2ae0
--- /dev/null
+++ b/org.jacoco.report.test/src/org/jacoco/report/MultiReportVisitorTest.java
@@ -0,0 +1,129 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report;

+

+import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertTrue;

+

+import java.io.IOException;

+import java.util.ArrayList;

+import java.util.Arrays;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.List;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.IPackageCoverage;

+import org.jacoco.core.data.ExecutionData;

+import org.jacoco.core.data.SessionInfo;

+import org.jacoco.core.internal.analysis.BundleCoverageImpl;

+import org.junit.Test;

+

+/**

+ * Unit tests for {@link MultiReportVisitor}.

+ */

+public class MultiReportVisitorTest {

+

+	private static class MockVisitor extends MockGroupVisitor implements

+			IReportVisitor {

+

+		MockVisitor() {

+			super("Report");

+		}

+

+		private boolean visitInfosCalled = false;

+

+		private boolean visitEndCalled = false;

+

+		public void visitInfo(List<SessionInfo> sessionInfos,

+				Collection<ExecutionData> executionData) throws IOException {

+			visitInfosCalled = true;

+		}

+

+		public void visitEnd() throws IOException {

+			visitEndCalled = true;

+		}

+

+		@Override

+		public String toString() {

+			assertTrue("visitInfos() has not been called", visitInfosCalled);

+			assertTrue("visitEnd() has not been called", visitEndCalled);

+			return super.toString();

+		}

+

+	}

+

+	private static class MockGroupVisitor implements IReportGroupVisitor {

+

+		private final String name;

+

+		private final List<MockGroupVisitor> children = new ArrayList<MockGroupVisitor>();

+

+		MockGroupVisitor(String name) {

+			this.name = name;

+		}

+

+		public void visitBundle(IBundleCoverage bundle,

+				ISourceFileLocator locator) throws IOException {

+			children.add(new MockGroupVisitor(bundle.getName()));

+		}

+

+		public IReportGroupVisitor visitGroup(String name) throws IOException {

+			MockGroupVisitor child = new MockGroupVisitor(name);

+			children.add(child);

+			return child;

+		}

+

+		@Override

+		public String toString() {

+			return name + children;

+		}

+	}

+

+	private IBundleCoverage createBundle(String name) {

+		final Collection<IPackageCoverage> packages = Collections.emptyList();

+		return new BundleCoverageImpl(name, packages);

+	}

+

+	private static final String MOCK_REPORT = "Report[g1[b1[], b2[]], g2[]]";

+

+	private void createMockReport(IReportVisitor visitor) throws IOException {

+		final List<SessionInfo> sessions = Collections.emptyList();

+		final List<ExecutionData> executionData = Collections.emptyList();

+		visitor.visitInfo(sessions, executionData);

+		IReportGroupVisitor g1 = visitor.visitGroup("g1");

+		g1.visitBundle(createBundle("b1"), null);

+		g1.visitBundle(createBundle("b2"), null);

+		visitor.visitGroup("g2");

+		visitor.visitEnd();

+	}

+

+	@Test

+	public void testMockFormatter() throws IOException {

+		MockVisitor visitor = new MockVisitor();

+		createMockReport(visitor);

+		assertEquals(MOCK_REPORT, visitor.toString());

+	}

+

+	@Test

+	public void testMultiFormatter() throws IOException {

+		IReportVisitor mock1 = new MockVisitor();

+		IReportVisitor mock2 = new MockVisitor();

+		IReportVisitor mock3 = new MockVisitor();

+		List<IReportVisitor> visitors = Arrays.asList(mock1, mock2, mock3);

+		MultiReportVisitor multi = new MultiReportVisitor(visitors);

+		createMockReport(multi);

+		assertEquals(MOCK_REPORT, mock1.toString());

+		assertEquals(MOCK_REPORT, mock2.toString());

+		assertEquals(MOCK_REPORT, mock3.toString());

+	}

+}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/ReportStructureTestDriver.java b/org.jacoco.report.test/src/org/jacoco/report/ReportStructureTestDriver.java
index c281b00..ba7dc32 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/ReportStructureTestDriver.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/ReportStructureTestDriver.java
@@ -17,9 +17,7 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.jacoco.core.analysis.CoverageNodeImpl;
 import org.jacoco.core.analysis.IClassCoverage;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
 import org.jacoco.core.analysis.IMethodCoverage;
 import org.jacoco.core.analysis.IPackageCoverage;
 import org.jacoco.core.analysis.ISourceFileCoverage;
@@ -27,13 +25,14 @@
 import org.jacoco.core.data.SessionInfo;
 import org.jacoco.core.internal.analysis.BundleCoverageImpl;
 import org.jacoco.core.internal.analysis.ClassCoverageImpl;
+import org.jacoco.core.internal.analysis.CounterImpl;
 import org.jacoco.core.internal.analysis.MethodCoverageImpl;
 import org.jacoco.core.internal.analysis.PackageCoverageImpl;
 import org.jacoco.core.internal.analysis.SourceFileCoverageImpl;
 
 /**
  * Creates a simple hierarchy of coverage nodes and feeds it into
- * {@link IReportFormatter} instances.
+ * {@link IReportVisitor} instances.
  */
 public class ReportStructureTestDriver {
 
@@ -60,10 +59,13 @@
 
 	private final BundleCoverageImpl bundleCoverage;
 
-	private final CoverageNodeImpl groupCoverage;
-
 	public ReportStructureTestDriver() {
-		methodCoverage = new MethodCoverageImpl("fooMethod", "()V", null);
+		methodCoverage = new MethodCoverageImpl("fooMethod", "()V", null) {
+			{
+				instructionCounter = CounterImpl.getInstance(2, 22);
+				branchCounter = CounterImpl.getInstance(3, 33);
+			}
+		};
 
 		final ClassCoverageImpl classCoverageImpl = new ClassCoverageImpl(
 				"org/jacoco/example/FooClass", 1001, null, "java/lang/Object",
@@ -72,61 +74,38 @@
 		classCoverageImpl.addMethod(methodCoverage);
 		classCoverage = classCoverageImpl;
 
-		sourceFileCoverage = new SourceFileCoverageImpl("FooClass.java",
-				"org/jacoco/example");
+		final SourceFileCoverageImpl sourceFileCoverageImpl = new SourceFileCoverageImpl(
+				"FooClass.java", "org/jacoco/example");
+		sourceFileCoverageImpl.increment(classCoverage);
+		sourceFileCoverage = sourceFileCoverageImpl;
+
 		packageCoverage = new PackageCoverageImpl("org/jacoco/example",
 				Collections.singleton(classCoverage),
 				Collections.singleton(sourceFileCoverage));
 		bundleCoverage = new BundleCoverageImpl("bundle",
 				Collections.singleton(packageCoverage));
-		groupCoverage = new CoverageNodeImpl(ElementType.GROUP, "group");
 	}
 
-	public void sendGroup(IReportFormatter formatter) throws IOException {
-		final IReportVisitor child = formatter.createReportVisitor(
-				groupCoverage, sessions, executionData);
-		sendBundle(child);
-		child.visitEnd(sourceFileLocator);
+	public void sendGroup(IReportVisitor reportVisitor) throws IOException {
+		reportVisitor.visitInfo(sessions, executionData);
+		final IReportGroupVisitor group = reportVisitor.visitGroup("group");
+		sendBundle(group);
+		reportVisitor.visitEnd();
 	}
 
-	public void sendGroup(IReportVisitor parent) throws IOException {
-		final IReportVisitor child = parent.visitChild(groupCoverage);
-		sendBundle(child);
-		child.visitEnd(sourceFileLocator);
+	public void sendGroup(IReportGroupVisitor groupVisitor) throws IOException {
+		final IReportGroupVisitor group = groupVisitor.visitGroup("group");
+		sendBundle(group);
 	}
 
-	public void sendBundle(IReportFormatter formatter) throws IOException {
-		final IReportVisitor child = formatter.createReportVisitor(
-				bundleCoverage, sessions, executionData);
-		sendPackage(child);
-		child.visitEnd(sourceFileLocator);
+	public void sendBundle(IReportVisitor reportVisitor) throws IOException {
+		reportVisitor.visitInfo(sessions, executionData);
+		reportVisitor.visitBundle(bundleCoverage, sourceFileLocator);
+		reportVisitor.visitEnd();
 	}
 
-	public void sendBundle(IReportVisitor parent) throws IOException {
-		final IReportVisitor child = parent.visitChild(bundleCoverage);
-		sendPackage(child);
-		child.visitEnd(sourceFileLocator);
-	}
-
-	public void sendPackage(IReportVisitor parent) throws IOException {
-		final IReportVisitor child = parent.visitChild(packageCoverage);
-		sendClass(child);
-		sendSourceFile(child);
-		child.visitEnd(sourceFileLocator);
-	}
-
-	public void sendClass(IReportVisitor parent) throws IOException {
-		final IReportVisitor child = parent.visitChild(classCoverage);
-		sendMethod(child);
-		child.visitEnd(sourceFileLocator);
-	}
-
-	public void sendMethod(IReportVisitor parent) throws IOException {
-		parent.visitChild(methodCoverage).visitEnd(sourceFileLocator);
-	}
-
-	public void sendSourceFile(IReportVisitor parent) throws IOException {
-		parent.visitChild(sourceFileCoverage).visitEnd(sourceFileLocator);
+	public void sendBundle(IReportGroupVisitor groupVisitor) throws IOException {
+		groupVisitor.visitBundle(bundleCoverage, sourceFileLocator);
 	}
 
 }
diff --git a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVFormatterTest.java b/org.jacoco.report.test/src/org/jacoco/report/csv/CSVFormatterTest.java
index 340372b..d280dda 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVFormatterTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/csv/CSVFormatterTest.java
@@ -22,14 +22,12 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.jacoco.core.analysis.CoverageNodeImpl;
-import org.jacoco.core.analysis.ICoverageNode;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
 import org.jacoco.core.data.ExecutionData;
 import org.jacoco.core.data.SessionInfo;
 import org.jacoco.report.ILanguageNames;
+import org.jacoco.report.IReportGroupVisitor;
 import org.jacoco.report.IReportVisitor;
-import org.jacoco.report.MemorySingleReportOutput;
+import org.jacoco.report.MemoryOutput;
 import org.jacoco.report.ReportStructureTestDriver;
 import org.junit.After;
 import org.junit.Before;
@@ -40,20 +38,22 @@
  */
 public class CSVFormatterTest {
 
-	private static final String HEADER = "GROUP,PACKAGE,CLASS,METHOD_COVERED,METHOD_MISSED,LINE_COVERED,LINE_MISSED,INSTRUCTION_COVERED,INSTRUCTION_MISSED,BRANCH_COVERED,BRANCH_MISSED";
+	private static final String HEADER = "GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,METHOD_MISSED,METHOD_COVERED";
 
 	private ReportStructureTestDriver driver;
 
 	private CSVFormatter formatter;
 
-	private MemorySingleReportOutput output;
+	private IReportVisitor visitor;
+
+	private MemoryOutput output;
 
 	@Before
-	public void setup() {
+	public void setup() throws Exception {
 		driver = new ReportStructureTestDriver();
 		formatter = new CSVFormatter();
-		output = new MemorySingleReportOutput();
-		formatter.setReportOutput(output);
+		output = new MemoryOutput();
+		visitor = formatter.createVisitor(output);
 	}
 
 	@After
@@ -61,55 +61,48 @@
 		output.assertClosed();
 	}
 
-	@Test(expected = IllegalStateException.class)
-	public void testNoReportOutput() throws IOException {
-		new CSVFormatter().createReportVisitor(null, null, null);
-	}
-
 	@Test
 	public void testStructureWithGroup() throws IOException {
-		driver.sendGroup(formatter);
+		driver.sendGroup(visitor);
 		final List<String> lines = getLines();
 		assertEquals(HEADER, lines.get(0));
 		assertEquals(
-				"group/bundle,org.jacoco.example,FooClass,0,1,0,0,0,0,0,0",
+				"group/bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",
 				lines.get(1));
 	}
 
 	@Test
 	public void testStructureWithNestedGroups() throws IOException {
-		final ICoverageNode root = new CoverageNodeImpl(ElementType.GROUP,
-				"root");
 		final List<SessionInfo> sessions = Collections.emptyList();
 		final Collection<ExecutionData> data = Collections.emptyList();
-		final IReportVisitor child = formatter.createReportVisitor(root,
-				sessions, data);
-		driver.sendGroup(child);
-		driver.sendGroup(child);
-		child.visitEnd(driver.sourceFileLocator);
+		visitor.visitInfo(sessions, data);
+		driver.sendGroup((IReportGroupVisitor) visitor);
+		driver.sendGroup((IReportGroupVisitor) visitor);
+		visitor.visitEnd();
 		final List<String> lines = getLines();
 		assertEquals(HEADER, lines.get(0));
 		assertEquals(
-				"root/group/bundle,org.jacoco.example,FooClass,0,1,0,0,0,0,0,0",
+				"group/bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",
 				lines.get(1));
 		assertEquals(
-				"root/group/bundle,org.jacoco.example,FooClass,0,1,0,0,0,0,0,0",
+				"group/bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",
 				lines.get(2));
 	}
 
 	@Test
 	public void testStructureWithBundleOnly() throws IOException {
-		driver.sendBundle(formatter);
+		driver.sendBundle(visitor);
 		final List<String> lines = getLines();
 		assertEquals(HEADER, lines.get(0));
-		assertEquals("bundle,org.jacoco.example,FooClass,0,1,0,0,0,0,0,0",
+		assertEquals("bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",
 				lines.get(1));
 	}
 
 	@Test
 	public void testSetEncoding() throws Exception {
 		formatter.setOutputEncoding("UTF-16");
-		driver.sendBundle(formatter);
+		visitor = formatter.createVisitor(output);
+		driver.sendBundle(visitor);
 		final List<String> lines = getLines("UTF-16");
 		assertEquals(HEADER, lines.get(0));
 	}
@@ -138,6 +131,7 @@
 		};
 		formatter.setLanguageNames(names);
 		assertSame(names, formatter.getLanguageNames());
+		output.close();
 	}
 
 	private List<String> getLines() throws IOException {
@@ -146,7 +140,7 @@
 
 	private List<String> getLines(String encoding) throws IOException {
 		final BufferedReader reader = new BufferedReader(new InputStreamReader(
-				output.getFileAsStream(), encoding));
+				output.getContentsAsStream(), encoding));
 		final List<String> lines = new ArrayList<String>();
 		String line;
 		while ((line = reader.readLine()) != null) {
diff --git a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVGroupHandlerTest.java b/org.jacoco.report.test/src/org/jacoco/report/csv/CSVGroupHandlerTest.java
index 6ab7703..74c01cd 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVGroupHandlerTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/csv/CSVGroupHandlerTest.java
@@ -11,12 +11,15 @@
  *******************************************************************************/

 package org.jacoco.report.csv;

 

+import static org.junit.Assert.assertEquals;

+

+import java.io.BufferedReader;

+import java.io.StringReader;

 import java.io.StringWriter;

 

-import org.jacoco.core.analysis.CoverageNodeImpl;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.report.IReportVisitor;

+import org.jacoco.report.IReportGroupVisitor;

 import org.jacoco.report.JavaNames;

+import org.jacoco.report.ReportStructureTestDriver;

 import org.junit.Before;

 import org.junit.Test;

 

@@ -25,28 +28,42 @@
  */

 public class CSVGroupHandlerTest {

 

-	private IReportVisitor handler;

+	private IReportGroupVisitor handler;

+

+	private StringWriter result;

+

+	private ReportStructureTestDriver driver;

 

 	@Before

 	public void setup() throws Exception {

-		final DelimitedWriter dw = new DelimitedWriter(new StringWriter());

+		result = new StringWriter();

+		final DelimitedWriter dw = new DelimitedWriter(result);

 		final ClassRowWriter rw = new ClassRowWriter(dw, new JavaNames());

-		handler = new CSVGroupHandler(rw, "group");

+		handler = new CSVGroupHandler(rw);

+		driver = new ReportStructureTestDriver();

 	}

 

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative1() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.CLASS, "Foo"));

+	@Test

+	public void testVisitBundle() throws Exception {

+		driver.sendBundle(handler);

+		final BufferedReader reader = getResultReader();

+		reader.readLine();

+		assertEquals("bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",

+				reader.readLine());

 	}

 

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative2() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.METHOD, "Foo"));

+	@Test

+	public void testVisitGroup() throws Exception {

+		driver.sendGroup(handler);

+		final BufferedReader reader = getResultReader();

+		reader.readLine();

+		assertEquals(

+				"group/bundle,org.jacoco.example,FooClass,2,22,3,33,0,0,1,0",

+				reader.readLine());

 	}

 

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative3() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.SOURCEFILE, "Foo"));

+	private BufferedReader getResultReader() {

+		return new BufferedReader(new StringReader(result.toString()));

 	}

 

 }

diff --git a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVPackageHandlerTest.java b/org.jacoco.report.test/src/org/jacoco/report/csv/CSVPackageHandlerTest.java
deleted file mode 100644
index bbb0356..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/csv/CSVPackageHandlerTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *

- *******************************************************************************/

-package org.jacoco.report.csv;

-

-import java.io.StringWriter;

-

-import org.jacoco.core.analysis.CoverageNodeImpl;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.JavaNames;

-import org.junit.Before;

-import org.junit.Test;

-

-/**

- * Unit tests for {@link CSVPackageHandler}.

- */

-public class CSVPackageHandlerTest {

-

-	private IReportVisitor handler;

-

-	@Before

-	public void setup() throws Exception {

-		final DelimitedWriter dw = new DelimitedWriter(new StringWriter());

-		final ClassRowWriter rw = new ClassRowWriter(dw, new JavaNames());

-		handler = new CSVPackageHandler(rw, "group", "package");

-	}

-

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative1() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.GROUP, "Foo"));

-	}

-

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative2() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.BUNDLE, "Foo"));

-	}

-

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative3() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.PACKAGE, "Foo"));

-	}

-

-	@Test(expected = AssertionError.class)

-	public void testVisitChildNegative4() throws Exception {

-		handler.visitChild(new CoverageNodeImpl(ElementType.METHOD, "Foo"));

-	}

-

-}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/csv/ClassRowWriterTest.java b/org.jacoco.report.test/src/org/jacoco/report/csv/ClassRowWriterTest.java
new file mode 100644
index 0000000..6a5b75c
--- /dev/null
+++ b/org.jacoco.report.test/src/org/jacoco/report/csv/ClassRowWriterTest.java
@@ -0,0 +1,92 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.csv;

+

+import static org.junit.Assert.assertEquals;

+

+import java.io.BufferedReader;

+import java.io.StringReader;

+import java.io.StringWriter;

+

+import org.jacoco.core.analysis.IClassCoverage;

+import org.jacoco.core.internal.analysis.ClassCoverageImpl;

+import org.jacoco.core.internal.analysis.CounterImpl;

+import org.jacoco.report.ILanguageNames;

+import org.junit.Before;

+import org.junit.Test;

+

+/**

+ * Unit tests for {@link ClassRowWriter}.

+ */

+public class ClassRowWriterTest {

+

+	private StringWriter result;

+

+	private ClassRowWriter writer;

+

+	@Before

+	public void setup() throws Exception {

+		ILanguageNames names = new ILanguageNames() {

+			public String getClassName(String vmname, String vmsignature,

+					String vmsuperclass, String[] vminterfaces) {

+				return vmname;

+			}

+

+			public String getPackageName(String vmname) {

+				return vmname;

+			}

+

+			public String getQualifiedClassName(String vmname) {

+				throw new AssertionError();

+			}

+

+			public String getMethodName(String vmclassname,

+					String vmmethodname, String vmdesc, String vmsignature) {

+				throw new AssertionError();

+			}

+		};

+		result = new StringWriter();

+		writer = new ClassRowWriter(new DelimitedWriter(result), names);

+	}

+

+	@Test

+	public void TestHeader() throws Exception {

+		BufferedReader reader = getResultReader();

+		assertEquals(

+				"GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,METHOD_MISSED,METHOD_COVERED",

+				reader.readLine());

+	}

+

+	@Test

+	public void TestRow() throws Exception {

+		IClassCoverage node = new ClassCoverageImpl("test/package/Foo", 123,

+				null, "java/lang/Object", null) {

+			{

+				instructionCounter = CounterImpl.getInstance(1, 11);

+				branchCounter = CounterImpl.getInstance(2, 22);

+				lineCounter = CounterImpl.getInstance(3, 33);

+				methodCounter = CounterImpl.getInstance(4, 44);

+				classCounter = CounterImpl.getInstance(5, 55);

+			}

+		};

+		writer.writeRow("group", "test/package", node);

+		BufferedReader reader = getResultReader();

+		reader.readLine();

+		assertEquals("group,test/package,test/package/Foo,1,11,2,22,3,33,4,44",

+				reader.readLine());

+	}

+

+	private BufferedReader getResultReader() {

+		return new BufferedReader(new StringReader(result.toString()));

+	}

+

+}

diff --git a/org.jacoco.report.test/src/org/jacoco/report/html/HTMLFormatterTest.java b/org.jacoco.report.test/src/org/jacoco/report/html/HTMLFormatterTest.java
index 2899ab4..64e2fbb 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/html/HTMLFormatterTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/html/HTMLFormatterTest.java
@@ -43,7 +43,6 @@
 		driver = new ReportStructureTestDriver();
 		formatter = new HTMLFormatter();
 		output = new MemoryMultiReportOutput();
-		formatter.setReportOutput(output);
 	}
 
 	@After
@@ -51,14 +50,9 @@
 		output.assertAllClosed();
 	}
 
-	@Test(expected = IllegalStateException.class)
-	public void testNoReportOutput() throws IOException {
-		new HTMLFormatter().createReportVisitor(null, null, null);
-	}
-
 	@Test
 	public void testStructureWithGroup() throws IOException {
-		driver.sendGroup(formatter);
+		driver.sendGroup(formatter.createVisitor(output));
 		output.assertFile("index.html");
 		output.assertFile("bundle/index.html");
 		output.assertFile("bundle/org.jacoco.example/index.html");
@@ -67,7 +61,7 @@
 
 	@Test
 	public void testStructureWithBundleOnly() throws IOException {
-		driver.sendBundle(formatter);
+		driver.sendBundle(formatter.createVisitor(output));
 		output.assertFile("index.html");
 		output.assertFile("org.jacoco.example/index.html");
 		output.assertFile("org.jacoco.example/FooClass.html");
@@ -75,7 +69,7 @@
 
 	@Test
 	public void testDefaultEncoding() throws Exception {
-		driver.sendBundle(formatter);
+		driver.sendBundle(formatter.createVisitor(output));
 		final BufferedReader reader = new BufferedReader(new InputStreamReader(
 				output.getFileAsStream("index.html"), "UTF-8"));
 		final String line = reader.readLine();
@@ -86,7 +80,7 @@
 	@Test
 	public void testSetEncoding() throws Exception {
 		formatter.setOutputEncoding("UTF-16");
-		driver.sendBundle(formatter);
+		driver.sendBundle(formatter.createVisitor(output));
 		final BufferedReader reader = new BufferedReader(new InputStreamReader(
 				output.getFileAsStream("index.html"), "UTF-16"));
 		final String line = reader.readLine();
@@ -118,18 +112,21 @@
 		};
 		formatter.setLanguageNames(names);
 		assertSame(names, formatter.getLanguageNames());
+		output.close();
 	}
 
 	@Test
 	public void testGetFooterText() throws Exception {
 		formatter.setFooterText("Custom Footer");
 		assertEquals("Custom Footer", formatter.getFooterText());
+		output.close();
 	}
 
 	@Test
 	public void testGetLocale() throws Exception {
 		formatter.setLocale(Locale.KOREAN);
 		assertEquals(Locale.KOREAN, formatter.getLocale());
+		output.close();
 	}
 
 }
diff --git a/org.jacoco.report.test/src/org/jacoco/report/NormalizedFileNamesTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/NormalizedFileNamesTest.java
similarity index 95%
rename from org.jacoco.report.test/src/org/jacoco/report/NormalizedFileNamesTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/NormalizedFileNamesTest.java
index 297dfb5..53faace 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/NormalizedFileNamesTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/NormalizedFileNamesTest.java
@@ -9,10 +9,11 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report;

+package org.jacoco.report.internal;

 

 import static org.junit.Assert.assertEquals;

 

+import org.jacoco.report.internal.NormalizedFileNames;

 import org.junit.Before;

 import org.junit.Test;

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/ReportOutputFolderTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/ReportOutputFolderTest.java
similarity index 94%
rename from org.jacoco.report.test/src/org/jacoco/report/ReportOutputFolderTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/ReportOutputFolderTest.java
index f7d5d04..3129879 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/ReportOutputFolderTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/ReportOutputFolderTest.java
@@ -9,13 +9,14 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report;

+package org.jacoco.report.internal;

 

 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertSame;

 

 import java.io.IOException;

 

+import org.jacoco.report.MemoryMultiReportOutput;

 import org.junit.After;

 import org.junit.Before;

 import org.junit.Test;

@@ -36,7 +37,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

@@ -58,6 +60,7 @@
 		root.subFolder("folderA").subFolder("folderB").createFile("test.html")

 				.close();

 		output.assertSingleFile("folderA/folderB/test.html");

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/LinkableStub.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/LinkableStub.java
index 805cb07..d262502 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/LinkableStub.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/LinkableStub.java
@@ -11,7 +11,7 @@
  *******************************************************************************/
 package org.jacoco.report.internal.html;
 
-import org.jacoco.report.ReportOutputFolder;
+import org.jacoco.report.internal.ReportOutputFolder;
 
 /**
  * Stub implementation for {@link ILinkable}.
@@ -22,7 +22,7 @@
 	private final String label;
 	private final String style;
 
-	LinkableStub(String link, String label, String style) {
+	public LinkableStub(String link, String label, String style) {
 		this.link = link;
 		this.label = label;
 		this.style = style;
diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/PackagePageTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/PackagePageTest.java
deleted file mode 100644
index 86e63f1..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/PackagePageTest.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2009, 2011 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
- *    
- *******************************************************************************/
-package org.jacoco.report.internal.html;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Locale;
-
-import org.jacoco.core.analysis.CoverageNodeImpl;
-import org.jacoco.core.analysis.IClassCoverage;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
-import org.jacoco.core.analysis.IPackageCoverage;
-import org.jacoco.core.analysis.ISourceFileCoverage;
-import org.jacoco.core.internal.analysis.PackageCoverageImpl;
-import org.jacoco.report.ILanguageNames;
-import org.jacoco.report.MemoryMultiReportOutput;
-import org.jacoco.report.ReportOutputFolder;
-import org.jacoco.report.internal.html.index.IIndexUpdate;
-import org.jacoco.report.internal.html.resources.Resources;
-import org.jacoco.report.internal.html.table.Table;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit tests for {@link PackagePage}.
- */
-public class PackagePageTest {
-
-	private MemoryMultiReportOutput output;
-
-	private ReportOutputFolder root;
-
-	private IHTMLReportContext context;
-
-	private PackagePage page;
-
-	@Before
-	public void setup() {
-		output = new MemoryMultiReportOutput();
-		root = new ReportOutputFolder(output);
-		context = new IHTMLReportContext() {
-
-			public ILanguageNames getLanguageNames() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public Resources getResources() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public Table getTable() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public String getFooterText() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public ILinkable getSessionsPage() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public String getOutputEncoding() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public IIndexUpdate getIndexUpdate() {
-				throw new AssertionError("Unexpected method call.");
-			}
-
-			public Locale getLocale() {
-				throw new AssertionError("Unexpected method call.");
-			}
-		};
-		Collection<IClassCoverage> classes = Collections.emptyList();
-		Collection<ISourceFileCoverage> sources = Collections.emptyList();
-		final IPackageCoverage node = new PackageCoverageImpl("foo", classes,
-				sources);
-		page = new PackagePage(node, null, root, context);
-	}
-
-	@Test(expected = AssertionError.class)
-	public void testVisitChildNegative1() {
-		page.visitChild(new CoverageNodeImpl(ElementType.GROUP, "Foo"));
-	}
-
-	@Test(expected = AssertionError.class)
-	public void testVisitChildNegative2() {
-		page.visitChild(new CoverageNodeImpl(ElementType.BUNDLE, "Foo"));
-	}
-
-	@Test(expected = AssertionError.class)
-	public void testVisitChildNegative3() {
-		page.visitChild(new CoverageNodeImpl(ElementType.PACKAGE, "Foo"));
-	}
-
-	@Test(expected = AssertionError.class)
-	public void testVisitChildNegative4() {
-		page.visitChild(new CoverageNodeImpl(ElementType.METHOD, "Foo"));
-	}
-
-}
diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/NodePageTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/NodePageTest.java
similarity index 77%
rename from org.jacoco.report.test/src/org/jacoco/report/internal/html/NodePageTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/html/page/NodePageTest.java
index 3104053..5067b1e 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/NodePageTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/NodePageTest.java
@@ -9,9 +9,10 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertSame;

 

 import java.io.IOException;

 import java.util.Locale;

@@ -19,11 +20,13 @@
 import org.jacoco.core.analysis.CoverageNodeImpl;

 import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.report.ILanguageNames;

-import org.jacoco.report.IReportVisitor;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.ILinkable;

+import org.jacoco.report.internal.html.LinkableStub;

 import org.jacoco.report.internal.html.index.IIndexUpdate;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.resources.Styles;

@@ -45,9 +48,9 @@
 

 	private CoverageNodeImpl node;

 

-	private NodePage page;

+	private NodePage<ICoverageNode> page;

 

-	private class TestNodePage extends NodePage {

+	private class TestNodePage extends NodePage<ICoverageNode> {

 

 		protected TestNodePage(ICoverageNode node) {

 			super(node, null, root, NodePageTest.this.context);

@@ -62,10 +65,6 @@
 			return "index.html";

 		}

 

-		public IReportVisitor visitChild(ICoverageNode node) {

-			throw new UnsupportedOperationException();

-		}

-

 	}

 

 	@Before

@@ -113,22 +112,14 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

 	@Test

 	public void testGetNode() throws IOException {

-		node.increment(new CoverageNodeImpl(ElementType.GROUP, "Foo") {

-			{

-				branchCounter = CounterImpl.getInstance(15, 8);

-			}

-		});

-		page.visitEnd(null);

-		assertEquals(node.getName(), page.getNode().getName());

-		assertEquals(node.getElementType(), page.getNode().getElementType());

-		assertEquals(CounterImpl.getInstance(15, 8), page.getNode()

-				.getBranchCounter());

+		assertSame(node, page.getNode());

 	}

 

 	@Test

@@ -141,10 +132,4 @@
 		assertEquals("el_group", page.getLinkStyle());

 	}

 

-	@Test

-	public void testVisitEnd() throws IOException {

-		page.visitEnd(null);

-		output.assertSingleFile("index.html");

-	}

-

 }

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/ReportPageTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/ReportPageTest.java
similarity index 88%
rename from org.jacoco.report.test/src/org/jacoco/report/internal/html/ReportPageTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/html/page/ReportPageTest.java
index 20f88ee..6e18fef 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/ReportPageTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/ReportPageTest.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import static org.junit.Assert.assertEquals;

 

@@ -18,7 +18,12 @@
 

 import org.jacoco.report.ILanguageNames;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.HTMLSupport;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.ILinkable;

+import org.jacoco.report.internal.html.LinkableStub;

 import org.jacoco.report.internal.html.index.IIndexUpdate;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.resources.Styles;

@@ -53,17 +58,6 @@
 		}

 

 		@Override

-		protected void headExtra(HTMLElement head) throws IOException {

-			super.headExtra(head);

-			head.script("text/javascript", "test.js");

-		}

-

-		@Override

-		protected String getOnload() {

-			return "init()";

-		}

-

-		@Override

 		protected void content(HTMLElement body) throws IOException {

 			body.div("testcontent").text("Hello Test");

 		}

@@ -129,7 +123,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

@@ -141,7 +136,7 @@
 

 	@Test

 	public void testPageContent() throws Exception {

-		page.renderDocument();

+		page.render();

 		final HTMLSupport support = new HTMLSupport();

 		final Document doc = support.parse(output.getFile("Test.html"));

 

@@ -149,12 +144,6 @@
 		assertEquals(".resources/report.css", support.findStr(doc,

 				"/html/head/link[@rel='stylesheet']/@href"));

 

-		// extra head

-		assertEquals("test.js", support.findStr(doc, "/html/head/script/@src"));

-

-		// onload handler

-		assertEquals("init()", support.findStr(doc, "/html/body/@onload"));

-

 		// bread crumb

 		assertEquals("Report", support.findStr(doc,

 				"/html/body/div[@class='breadcrumb']/a[1]/text()"));

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SessionsPageTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SessionsPageTest.java
similarity index 92%
rename from org.jacoco.report.test/src/org/jacoco/report/internal/html/SessionsPageTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SessionsPageTest.java
index 3fad3e4..f9fe548 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SessionsPageTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SessionsPageTest.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation
  *    
  *******************************************************************************/
-package org.jacoco.report.internal.html;
+package org.jacoco.report.internal.html.page;
 
 import static org.junit.Assert.assertEquals;
 
@@ -25,7 +25,12 @@
 import org.jacoco.report.ILanguageNames;
 import org.jacoco.report.JavaNames;
 import org.jacoco.report.MemoryMultiReportOutput;
-import org.jacoco.report.ReportOutputFolder;
+import org.jacoco.report.internal.ReportOutputFolder;
+import org.jacoco.report.internal.html.HTMLElement;
+import org.jacoco.report.internal.html.HTMLSupport;
+import org.jacoco.report.internal.html.IHTMLReportContext;
+import org.jacoco.report.internal.html.ILinkable;
+import org.jacoco.report.internal.html.LinkableStub;
 import org.jacoco.report.internal.html.index.ElementIndex;
 import org.jacoco.report.internal.html.index.IIndexUpdate;
 import org.jacoco.report.internal.html.resources.Resources;
@@ -99,7 +104,8 @@
 	}
 
 	@After
-	public void teardown() {
+	public void teardown() throws IOException {
+		output.close();
 		output.assertAllClosed();
 	}
 
@@ -128,7 +134,7 @@
 	public void testEmptyContent() throws Exception {
 		final SessionsPage page = new SessionsPage(noSessions, noExecutionData,
 				index, null, root, context);
-		page.renderDocument();
+		page.render();
 		final HTMLSupport support = new HTMLSupport();
 		final Document doc = support.parse(output.getFile(".sessions.html"));
 		assertEquals("No session information available.",
@@ -145,7 +151,7 @@
 		sessions.add(new SessionInfo("Session-C", 0, 0));
 		final SessionsPage page = new SessionsPage(sessions, noExecutionData,
 				index, null, root, context);
-		page.renderDocument();
+		page.render();
 		final HTMLSupport support = new HTMLSupport();
 		final Document doc = support.parse(output.getFile(".sessions.html"));
 		assertEquals("el_session", support.findStr(doc,
@@ -186,7 +192,7 @@
 
 		final SessionsPage page = new SessionsPage(noSessions, data, index,
 				null, root, context);
-		page.renderDocument();
+		page.render();
 		final HTMLSupport support = new HTMLSupport();
 		final Document doc = support.parse(output.getFile(".sessions.html"));
 		assertEquals("el_class", support.findStr(doc,
diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceFilePageTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceFilePageTest.java
similarity index 70%
rename from org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceFilePageTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceFilePageTest.java
index 4575211..0d83f77 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceFilePageTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceFilePageTest.java
@@ -9,24 +9,24 @@
  *    Marc R. Hoffmann - initial API and implementation
  *    
  *******************************************************************************/
-package org.jacoco.report.internal.html;
+package org.jacoco.report.internal.html.page;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
-import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.util.Locale;
 
-import org.jacoco.core.analysis.CoverageNodeImpl;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
 import org.jacoco.core.internal.analysis.SourceFileCoverageImpl;
-import org.jacoco.report.DirectorySourceFileLocator;
 import org.jacoco.report.ILanguageNames;
-import org.jacoco.report.ISourceFileLocator;
 import org.jacoco.report.MemoryMultiReportOutput;
-import org.jacoco.report.ReportOutputFolder;
+import org.jacoco.report.internal.ReportOutputFolder;
+import org.jacoco.report.internal.html.HTMLSupport;
+import org.jacoco.report.internal.html.IHTMLReportContext;
+import org.jacoco.report.internal.html.ILinkable;
+import org.jacoco.report.internal.html.LinkableStub;
 import org.jacoco.report.internal.html.index.IIndexUpdate;
 import org.jacoco.report.internal.html.resources.Resources;
 import org.jacoco.report.internal.html.resources.Styles;
@@ -47,10 +47,10 @@
 
 	private IHTMLReportContext context;
 
-	private ISourceFileLocator locator;
+	private Reader sourceReader;
 
 	@Before
-	public void setup() {
+	public void setup() throws IOException {
 		output = new MemoryMultiReportOutput();
 		root = new ReportOutputFolder(output);
 		final Resources resources = new Resources(root);
@@ -89,32 +89,25 @@
 				return Locale.ENGLISH;
 			}
 		};
-		locator = new DirectorySourceFileLocator(new File("./src"), "UTF-8");
+		sourceReader = new InputStreamReader(
+				new FileInputStream(
+						"./src/org/jacoco/report/internal/html/page/SourceFilePageTest.java"),
+				"UTF-8");
 	}
 
 	@After
-	public void teardown() {
+	public void teardown() throws IOException {
+		output.close();
 		output.assertAllClosed();
 	}
 
-	@Test(expected = AssertionError.class)
-	public void testVisitChildNegative() {
-		final SourceFileCoverageImpl node = new SourceFileCoverageImpl(
-				"SourceFilePageTest.java", "org/jacoco/report/html");
-		final SourceFilePage page = new SourceFilePage(node, null, root,
-				context);
-		page.visitChild(new CoverageNodeImpl(ElementType.CLASS, "Foo"));
-	}
-
 	@Test
 	public void testContents() throws Exception {
 		final SourceFileCoverageImpl node = new SourceFileCoverageImpl(
 				"SourceFilePageTest.java", "org/jacoco/report/internal/html");
-		final SourceFilePage page = new SourceFilePage(node, null, root,
-				context);
-		page.visitEnd(locator);
-
-		assertTrue(page.exists());
+		final SourceFilePage page = new SourceFilePage(node, sourceReader,
+				null, root, context);
+		page.render();
 
 		final HTMLSupport support = new HTMLSupport();
 		final Document result = support.parse(output
@@ -139,16 +132,4 @@
 				support.findStr(result, "/html/body/pre/span[1]/@id"));
 	}
 
-	@Test
-	public void testNoSource() throws IOException {
-		final SourceFileCoverageImpl node = new SourceFileCoverageImpl(
-				"DoesNotExist.java", "org/jacoco/report/html");
-		final SourceFilePage page = new SourceFilePage(node, null, root,
-				context);
-		page.visitEnd(locator);
-
-		assertFalse(page.exists());
-		output.assertEmpty();
-	}
-
 }
diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceHighlighterTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceHighlighterTest.java
similarity index 96%
rename from org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceHighlighterTest.java
rename to org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceHighlighterTest.java
index 9f65a3e..15db391 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/SourceHighlighterTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/page/SourceHighlighterTest.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import static org.junit.Assert.assertEquals;

 

@@ -20,6 +20,9 @@
 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.core.internal.analysis.SourceNodeImpl;

+import org.jacoco.report.internal.html.HTMLDocument;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.HTMLSupport;

 import org.jacoco.report.internal.html.resources.Styles;

 import org.junit.Before;

 import org.junit.Test;

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/resources/ResourcesTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/resources/ResourcesTest.java
index 56d5202..fa7e449 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/resources/ResourcesTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/resources/ResourcesTest.java
@@ -17,7 +17,7 @@
 

 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.junit.Before;

 import org.junit.Test;

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/BarColumnTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/BarColumnTest.java
index 8c1fc26..444dda7 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/BarColumnTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/BarColumnTest.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertTrue;

 

+import java.io.IOException;

 import java.util.Arrays;

 import java.util.Comparator;

 import java.util.Locale;

@@ -24,7 +25,7 @@
 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLDocument;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.HTMLSupport;

@@ -66,7 +67,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/CounterColumnTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/CounterColumnTest.java
index 8430fbc..5bbdbd2 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/CounterColumnTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/CounterColumnTest.java
@@ -15,6 +15,7 @@
 import static org.junit.Assert.assertFalse;

 import static org.junit.Assert.assertTrue;

 

+import java.io.IOException;

 import java.util.Arrays;

 import java.util.Collections;

 import java.util.Comparator;

@@ -26,7 +27,7 @@
 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLDocument;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.HTMLSupport;

@@ -68,7 +69,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/LabelColumnTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/LabelColumnTest.java
index 8c8fe7f..ed2d7cf 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/LabelColumnTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/LabelColumnTest.java
@@ -14,11 +14,13 @@
 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertTrue;

 

+import java.io.IOException;

+

 import org.jacoco.core.analysis.CoverageNodeImpl;

 import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLDocument;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.HTMLSupport;

@@ -60,7 +62,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/PercentageColumnTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/PercentageColumnTest.java
index 263a399..bbe963d 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/PercentageColumnTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/PercentageColumnTest.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertTrue;

 

+import java.io.IOException;

 import java.util.Comparator;

 import java.util.Locale;

 

@@ -23,7 +24,7 @@
 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLDocument;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.HTMLSupport;

@@ -65,7 +66,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/TableTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/TableTest.java
index b72956c..50d2b59 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/TableTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/html/table/TableTest.java
@@ -26,7 +26,7 @@
 import org.jacoco.core.analysis.ICoverageNode.ElementType;

 import org.jacoco.core.internal.analysis.CounterImpl;

 import org.jacoco.report.MemoryMultiReportOutput;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLDocument;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.HTMLSupport;

@@ -65,7 +65,8 @@
 	}

 

 	@After

-	public void teardown() {

+	public void teardown() throws IOException {

+		output.close();

 		output.assertAllClosed();

 	}

 

diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLGroupVisitorTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLGroupVisitorTest.java
new file mode 100644
index 0000000..81e29ff
--- /dev/null
+++ b/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLGroupVisitorTest.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2009, 2011 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
+ *    
+ *******************************************************************************/
+package org.jacoco.report.internal.xml;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.jacoco.report.ReportStructureTestDriver;
+import org.jacoco.report.xml.XMLFormatter;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+/**
+ * Unit tests for {@link XMLGroupVisitor}.
+ */
+public class XMLGroupVisitorTest {
+
+	private XMLElement root;
+
+	private StringWriter buffer;
+
+	private XMLSupport support;
+
+	private XMLGroupVisitor handler;
+
+	private ReportStructureTestDriver driver;
+
+	@Before
+	public void setup() throws Exception {
+		buffer = new StringWriter();
+		support = new XMLSupport(XMLFormatter.class);
+		root = new XMLDocument("report", "-//JACOCO//DTD Report 1.0//EN",
+				"report.dtd", "UTF-8", true, buffer);
+		root.attr("name", "Report");
+		handler = new XMLGroupVisitor(root, null);
+		driver = new ReportStructureTestDriver();
+	}
+
+	@Test
+	public void testVisitBundle() throws Exception {
+		driver.sendBundle(handler);
+		root.close();
+		final Document doc = getDocument();
+		assertEquals("bundle", support.findStr(doc, "//report/group/@name"));
+	}
+
+	@Test
+	public void testVisitGroup() throws Exception {
+		driver.sendGroup(handler);
+		root.close();
+		final Document doc = getDocument();
+		assertEquals("group", support.findStr(doc, "//report/group/@name"));
+	}
+
+	@Test
+	public void testVisitEnd() throws Exception {
+		driver.sendBundle(handler);
+		handler.visitEnd();
+		root.close();
+		final Document doc = getDocument();
+		assertEquals("33", support.findStr(doc,
+				"//report/counter[@type='BRANCH']/@covered"));
+	}
+
+	private Document getDocument() throws SAXException, IOException,
+			ParserConfigurationException {
+		return support.parse(buffer.toString());
+	}
+
+}
diff --git a/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLReportNodeHandlerTest.java b/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLReportNodeHandlerTest.java
deleted file mode 100644
index b873252..0000000
--- a/org.jacoco.report.test/src/org/jacoco/report/internal/xml/XMLReportNodeHandlerTest.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2009, 2011 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
- *    
- *******************************************************************************/
-package org.jacoco.report.internal.xml;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.io.StringWriter;
-
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.jacoco.core.analysis.CoverageNodeImpl;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
-import org.jacoco.core.internal.analysis.CounterImpl;
-import org.jacoco.core.internal.analysis.MethodCoverageImpl;
-import org.jacoco.core.internal.analysis.SourceNodeImpl;
-import org.jacoco.report.IReportVisitor;
-import org.jacoco.report.xml.XMLFormatter;
-import org.junit.Before;
-import org.junit.Test;
-import org.w3c.dom.Document;
-import org.xml.sax.SAXException;
-
-/**
- * Unit tests for {@link XMLReportNodeHandler}.
- */
-public class XMLReportNodeHandlerTest {
-
-	private XMLElement root;
-
-	private StringWriter buffer;
-
-	private XMLSupport support;
-
-	private XMLReportNodeHandler handler;
-
-	@Before
-	public void setup() throws Exception {
-		buffer = new StringWriter();
-		support = new XMLSupport(XMLFormatter.class);
-		root = new XMLDocument("report", "-//JACOCO//DTD Report 1.0//EN",
-				"report.dtd", "UTF-8", true, buffer);
-		handler = new XMLReportNodeHandler(root, new CoverageNodeImpl(
-				ElementType.GROUP, "Sample"));
-	}
-
-	@Test
-	public void testRoot() throws Exception {
-		final Document doc = getDocument();
-		assertEquals("Sample", support.findStr(doc, "//report/@name"));
-	}
-
-	@Test
-	public void testGroup() throws Exception {
-		handler.visitChild(new CoverageNodeImpl(ElementType.GROUP, "Group1"))
-				.visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("Group1", support.findStr(doc, "//report/group/@name"));
-	}
-
-	@Test
-	public void testCounters() throws Exception {
-		final CoverageNodeImpl node = new CoverageNodeImpl(ElementType.GROUP,
-				"Group1") {
-			{
-				classCounter = CounterImpl.getInstance(9, 1);
-				methodCounter = CounterImpl.getInstance(18, 2);
-				branchCounter = CounterImpl.getInstance(27, 3);
-				instructionCounter = CounterImpl.getInstance(36, 4);
-				lineCounter = CounterImpl.getInstance(45, 5);
-			}
-		};
-		handler.visitChild(node).visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("1", support.findStr(doc,
-				"//report/group/counter[@type='CLASS']/@covered"));
-		assertEquals("9", support.findStr(doc,
-				"//report/group/counter[@type='CLASS']/@missed"));
-		assertEquals("2", support.findStr(doc,
-				"//report/group/counter[@type='METHOD']/@covered"));
-		assertEquals("18", support.findStr(doc,
-				"//report/group/counter[@type='METHOD']/@missed"));
-		assertEquals("3", support.findStr(doc,
-				"//report/group/counter[@type='BRANCH']/@covered"));
-		assertEquals("27", support.findStr(doc,
-				"//report/group/counter[@type='BRANCH']/@missed"));
-		assertEquals("4", support.findStr(doc,
-				"//report/group/counter[@type='INSTRUCTION']/@covered"));
-		assertEquals("36", support.findStr(doc,
-				"//report/group/counter[@type='INSTRUCTION']/@missed"));
-		assertEquals("5", support.findStr(doc,
-				"//report/group/counter[@type='LINE']/@covered"));
-		assertEquals("45", support.findStr(doc,
-				"//report/group/counter[@type='LINE']/@missed"));
-	}
-
-	@Test
-	public void testPackage() throws Exception {
-		handler.visitChild(
-				new CoverageNodeImpl(ElementType.PACKAGE, "org.jacoco.example"))
-				.visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("org.jacoco.example",
-				support.findStr(doc, "//report/package/@name"));
-	}
-
-	@Test
-	public void testClass() throws Exception {
-		final IReportVisitor packageHandler = handler
-				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
-						"org.jacoco.example"));
-		packageHandler.visitChild(new SourceNodeImpl(ElementType.CLASS, "Foo"))
-				.visitEnd(null);
-		packageHandler.visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("Foo",
-				support.findStr(doc, "//report/package/class/@name"));
-	}
-
-	@Test
-	public void testMethod() throws Exception {
-		final IReportVisitor packageHandler = handler
-				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
-						"org.jacoco.example"));
-		final IReportVisitor classHandler = packageHandler
-				.visitChild(new SourceNodeImpl(ElementType.CLASS, "Foo"));
-		MethodCoverageImpl node = new MethodCoverageImpl("doit", "()V", null);
-		node.increment(CounterImpl.COUNTER_1_0, CounterImpl.COUNTER_0_0, 15);
-		classHandler.visitChild(node).visitEnd(null);
-		classHandler.visitEnd(null);
-		packageHandler.visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("doit",
-				support.findStr(doc, "//report/package/class/method/@name"));
-		assertEquals("()V",
-				support.findStr(doc, "//report/package/class/method/@desc"));
-		assertEquals("15",
-				support.findStr(doc, "//report/package/class/method/@line"));
-	}
-
-	@Test
-	public void testSourcefile() throws Exception {
-		final IReportVisitor packageHandler = handler
-				.visitChild(new CoverageNodeImpl(ElementType.PACKAGE,
-						"org.jacoco.example"));
-		final SourceNodeImpl node = new SourceNodeImpl(ElementType.SOURCEFILE,
-				"Foo.java");
-		node.increment(CounterImpl.getInstance(1, 2),
-				CounterImpl.getInstance(3, 4), 12);
-		packageHandler.visitChild(node).visitEnd(null);
-		packageHandler.visitEnd(null);
-		final Document doc = getDocument();
-		assertEquals("Foo.java",
-				support.findStr(doc, "//report/package/sourcefile/@name"));
-		assertEquals("12",
-				support.findStr(doc, "//report/package/sourcefile/line/@nr"));
-		assertEquals("1",
-				support.findStr(doc, "//report/package/sourcefile/line/@mi"));
-		assertEquals("2",
-				support.findStr(doc, "//report/package/sourcefile/line/@ci"));
-		assertEquals("3",
-				support.findStr(doc, "//report/package/sourcefile/line/@mb"));
-		assertEquals("4",
-				support.findStr(doc, "//report/package/sourcefile/line/@cb"));
-	}
-
-	private Document getDocument() throws SAXException, IOException,
-			ParserConfigurationException {
-		handler.visitEnd(null);
-		return support.parse(buffer.toString());
-	}
-
-}
diff --git a/org.jacoco.report.test/src/org/jacoco/report/xml/XMLFormatterTest.java b/org.jacoco.report.test/src/org/jacoco/report/xml/XMLFormatterTest.java
index 52125ca..93cee7d 100644
--- a/org.jacoco.report.test/src/org/jacoco/report/xml/XMLFormatterTest.java
+++ b/org.jacoco.report.test/src/org/jacoco/report/xml/XMLFormatterTest.java
@@ -15,19 +15,15 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.BufferedReader;
-import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
-import org.jacoco.core.analysis.CoverageNodeImpl;
-import org.jacoco.core.analysis.ICoverageNode;
-import org.jacoco.core.analysis.ICoverageNode.ElementType;
 import org.jacoco.core.data.ExecutionData;
 import org.jacoco.core.data.SessionInfo;
-import org.jacoco.report.MemorySingleReportOutput;
+import org.jacoco.report.IReportVisitor;
+import org.jacoco.report.MemoryOutput;
 import org.jacoco.report.ReportStructureTestDriver;
 import org.jacoco.report.internal.xml.XMLSupport;
 import org.junit.After;
@@ -44,14 +40,19 @@
 
 	private XMLFormatter formatter;
 
-	private MemorySingleReportOutput output;
+	private MemoryOutput output;
+
+	private List<SessionInfo> infos;
+
+	private Collection<ExecutionData> data;
 
 	@Before
 	public void setup() {
 		driver = new ReportStructureTestDriver();
 		formatter = new XMLFormatter();
-		output = new MemorySingleReportOutput();
-		formatter.setReportOutput(output);
+		output = new MemoryOutput();
+		infos = new ArrayList<SessionInfo>();
+		data = new ArrayList<ExecutionData>();
 	}
 
 	@After
@@ -59,20 +60,15 @@
 		output.assertClosed();
 	}
 
-	@Test(expected = IllegalStateException.class)
-	public void testNoReportOutput() throws IOException {
-		new XMLFormatter().createReportVisitor(null, null, null);
-	}
-
 	@Test
 	public void testSessionInfo() throws Exception {
-		final List<SessionInfo> infos = new ArrayList<SessionInfo>();
 		infos.add(new SessionInfo("session-1", 12345, 67890));
 		infos.add(new SessionInfo("session-2", 1, 2));
 		infos.add(new SessionInfo("session-3", 1, 2));
-		ICoverageNode node = new CoverageNodeImpl(ElementType.GROUP, "Sample");
-		final Collection<ExecutionData> data = Collections.emptyList();
-		formatter.createReportVisitor(node, infos, data).visitEnd(null);
+		final IReportVisitor visitor = formatter.createVisitor(output);
+		visitor.visitInfo(infos, data);
+		visitor.visitGroup("foo");
+		visitor.visitEnd();
 		assertPathMatches("session-1", "/report/sessioninfo[1]/@id");
 		assertPathMatches("12345", "/report/sessioninfo[1]/@start");
 		assertPathMatches("67890", "/report/sessioninfo[1]/@dump");
@@ -82,7 +78,9 @@
 
 	@Test
 	public void testStructureWithGroup() throws Exception {
-		driver.sendGroup(formatter);
+		final IReportVisitor visitor = formatter.createVisitor(output);
+		visitor.visitInfo(infos, data);
+		driver.sendGroup(visitor);
 		assertPathMatches("group", "/report/@name");
 		assertPathMatches("bundle", "/report/group/@name");
 		assertPathMatches("org/jacoco/example", "/report/group/package/@name");
@@ -90,23 +88,29 @@
 				"/report/group/package/class/@name");
 		assertPathMatches("fooMethod",
 				"/report/group/package/class/method/@name");
+		assertPathMatches("2", "report/counter[@type='INSTRUCTION']/@missed");
 	}
 
 	@Test
 	public void testStructureWithBundleOnly() throws Exception {
-		driver.sendBundle(formatter);
+		final IReportVisitor visitor = formatter.createVisitor(output);
+		visitor.visitInfo(infos, data);
+		driver.sendBundle(visitor);
 		assertPathMatches("bundle", "/report/@name");
 		assertPathMatches("org/jacoco/example", "/report/package/@name");
 		assertPathMatches("org/jacoco/example/FooClass",
 				"/report/package/class/@name");
 		assertPathMatches("fooMethod", "/report/package/class/method/@name");
+		assertPathMatches("33", "report/counter[@type='BRANCH']/@covered");
 	}
 
 	@Test
 	public void testDefaultEncoding() throws Exception {
-		driver.sendBundle(formatter);
+		final IReportVisitor visitor = formatter.createVisitor(output);
+		visitor.visitInfo(infos, data);
+		driver.sendBundle(visitor);
 		final BufferedReader reader = new BufferedReader(new InputStreamReader(
-				output.getFileAsStream(), "UTF-8"));
+				output.getContentsAsStream(), "UTF-8"));
 		final String line = reader.readLine();
 		assertTrue(line,
 				line.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\""));
@@ -115,9 +119,11 @@
 	@Test
 	public void testSetEncoding() throws Exception {
 		formatter.setOutputEncoding("UTF-16");
-		driver.sendBundle(formatter);
+		final IReportVisitor visitor = formatter.createVisitor(output);
+		visitor.visitInfo(infos, data);
+		driver.sendBundle(visitor);
 		final BufferedReader reader = new BufferedReader(new InputStreamReader(
-				output.getFileAsStream(), "UTF-16"));
+				output.getContentsAsStream(), "UTF-16"));
 		final String line = reader.readLine();
 		assertTrue(line,
 				line.startsWith("<?xml version=\"1.0\" encoding=\"UTF-16\""));
@@ -126,7 +132,7 @@
 	private void assertPathMatches(String expected, String path)
 			throws Exception {
 		XMLSupport support = new XMLSupport(XMLFormatter.class);
-		Document document = support.parse(output.getFile());
+		Document document = support.parse(output.toByteArray());
 		assertEquals(expected, support.findStr(document, path));
 	}
 
diff --git a/org.jacoco.report/src/org/jacoco/report/FileMultiReportOutput.java b/org.jacoco.report/src/org/jacoco/report/FileMultiReportOutput.java
index 1bcd33d..0df466c 100644
--- a/org.jacoco.report/src/org/jacoco/report/FileMultiReportOutput.java
+++ b/org.jacoco.report/src/org/jacoco/report/FileMultiReportOutput.java
@@ -46,4 +46,8 @@
 		return new BufferedOutputStream(new FileOutputStream(file));

 	}

 

+	public void close() throws IOException {

+		// nothing to do here

+	}

+

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/FileSingleReportOutput.java b/org.jacoco.report/src/org/jacoco/report/FileSingleReportOutput.java
deleted file mode 100644
index 72aeb9a..0000000
--- a/org.jacoco.report/src/org/jacoco/report/FileSingleReportOutput.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import static java.lang.String.format;

-

-import java.io.BufferedOutputStream;

-import java.io.File;

-import java.io.FileOutputStream;

-import java.io.IOException;

-import java.io.OutputStream;

-

-/**

- * Implementation of {@link ISingleReportOutput} that writes the file directly

- * to a given location.

- */

-public class FileSingleReportOutput implements ISingleReportOutput {

-

-	private final File file;

-

-	/**

-	 * Creates a new instance for document output to the given location.

-	 * 

-	 * @param file

-	 */

-	public FileSingleReportOutput(final File file) {

-		this.file = file;

-	}

-

-	public OutputStream createFile() throws IOException {

-		final File parent = file.getParentFile();

-		parent.mkdirs();

-		if (!parent.isDirectory()) {

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

-		}

-		return new BufferedOutputStream(new FileOutputStream(file));

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/IMultiReportOutput.java b/org.jacoco.report/src/org/jacoco/report/IMultiReportOutput.java
index f4f3a90..a179fa8 100644
--- a/org.jacoco.report/src/org/jacoco/report/IMultiReportOutput.java
+++ b/org.jacoco.report/src/org/jacoco/report/IMultiReportOutput.java
@@ -31,4 +31,12 @@
 	 */

 	public OutputStream createFile(String path) throws IOException;

 

+	/**

+	 * Closes the underlying resource container.

+	 * 

+	 * @throws IOException

+	 *             if closing fails

+	 */

+	public void close() throws IOException;

+

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/IReportFormatter.java b/org.jacoco.report/src/org/jacoco/report/IReportFormatter.java
deleted file mode 100644
index 003c96f..0000000
--- a/org.jacoco.report/src/org/jacoco/report/IReportFormatter.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import java.io.IOException;

-import java.util.Collection;

-import java.util.List;

-

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.data.ExecutionData;

-import org.jacoco.core.data.SessionInfo;

-

-/**

- * Interface for all implementations that produce a particular report format.

- */

-public interface IReportFormatter {

-

-	/**

-	 * Creates a visitor for root of a coverage data tree.

-	 * 

-	 * @param root

-	 *            report root node

-	 * @param sessionInfos

-	 *            list of chronological ordered {@link SessionInfo} objects

-	 *            where execution data has been collected for this report.

-	 * @param executionData

-	 *            collection of all {@link ExecutionData} objects that are

-	 *            considered for this report

-	 * 

-	 * @return visitor for the root node

-	 * @throws IOException

-	 */

-	public IReportVisitor createReportVisitor(ICoverageNode root,

-			List<SessionInfo> sessionInfos,

-			Collection<ExecutionData> executionData) throws IOException;

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/IReportGroupVisitor.java b/org.jacoco.report/src/org/jacoco/report/IReportGroupVisitor.java
new file mode 100644
index 0000000..d67bc66
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/IReportGroupVisitor.java
@@ -0,0 +1,64 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+

+/**

+ * Output-Interface for hierarchical report structures. To allow sequential

+ * processing and save memory the group structure has to be traversed in a

+ * "deep first" fashion. The interface is implemented by the report formatters

+ * and can be used to emit coverage report structures.

+ * 

+ * The following constraints apply in using {@link IReportGroupVisitor} instances:

+ * 

+ * <ul>

+ * <li>A visitor instance can be used to either submit bundles (

+ * {@link #visitBundle(IBundleCoverage, ISourceFileLocator)}) or groups

+ * {@link #visitGroup(String)}). Bundles and groups are not allowed for the same

+ * visitor.</li>

+ * <li>When creating nested groups with {@link #visitGroup(String)} the

+ * hierarchy has to be processed in a "deep first" manner.</li>

+ * </ul>

+ */

+public interface IReportGroupVisitor {

+

+	/**

+	 * Called to add a bundle to the the report.

+	 * 

+	 * @param bundle

+	 *            a bundle to include in the report

+	 * @param locator

+	 *            source locator for this bundle

+	 * @throws IOException

+	 *             in case of IO problems with the report writer

+	 */

+	void visitBundle(IBundleCoverage bundle, ISourceFileLocator locator)

+			throws IOException;

+

+	/**

+	 * Called to add a new group to the report. The returned

+	 * {@link IReportGroupVisitor} instance can be used to add nested bundles or

+	 * groups. The content of the group has to be completed before this or any

+	 * parent visitor can be used again ("deep first").

+	 * 

+	 * @param name

+	 *            name of the group

+	 * @return visitor for the group's content

+	 * @throws IOException

+	 *             in case of IO problems with the report writer

+	 */

+	IReportGroupVisitor visitGroup(String name) throws IOException;

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/IReportVisitor.java b/org.jacoco.report/src/org/jacoco/report/IReportVisitor.java
index 0135fcd..5821a61 100644
--- a/org.jacoco.report/src/org/jacoco/report/IReportVisitor.java
+++ b/org.jacoco.report/src/org/jacoco/report/IReportVisitor.java
@@ -12,54 +12,41 @@
 package org.jacoco.report;

 

 import java.io.IOException;

+import java.util.Collection;

+import java.util.List;

 

-import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.core.data.ExecutionData;

+import org.jacoco.core.data.SessionInfo;

 

 /**

- * Output-Interface for hierarchical coverage data information. To allow data

- * streaming and to save memory {@link ICoverageNode}s are traversed in a

- * deep-first fashion. The interface is implemented by the different report

- * writers.

+ * Interface for all implementations to retrieve structured report data. Unlike

+ * nested {@link IReportGroupVisitor} instances the root visitor accepts exactly one

+ * bundle or group.

  */

-public interface IReportVisitor {

+public interface IReportVisitor extends IReportGroupVisitor {

 

 	/**

-	 * Visitor without any operation.

+	 * Initializes the report with global information. This method has to be

+	 * called before any other method can be called.

+	 * 

+	 * @param sessionInfos

+	 *            list of chronological ordered {@link SessionInfo} objects

+	 *            where execution data has been collected for this report.

+	 * @param executionData

+	 *            collection of all {@link ExecutionData} objects that are

+	 *            considered for this report

+	 * @throws IOException

+	 *             in case of IO problems with the report writer

 	 */

-	public static final IReportVisitor NOP = new IReportVisitor() {

-

-		public IReportVisitor visitChild(final ICoverageNode node) {

-			return NOP;

-		}

-

-		public void visitEnd(final ISourceFileLocator sourceFileLocator) {

-		}

-

-	};

+	public void visitInfo(List<SessionInfo> sessionInfos,

+			Collection<ExecutionData> executionData) throws IOException;

 

 	/**

-	 * Called for every direct child.

-	 * 

-	 * @param node

-	 *            Node for the child in the implementation class specific to

-	 *            this type. The counters are may yet be populated.

-	 * 

-	 * @return visitor instance for processing the child node

+	 * Has to be called after all report data has been emitted.

 	 * 

 	 * @throws IOException

 	 *             in case of IO problems with the report writer

 	 */

-	IReportVisitor visitChild(ICoverageNode node) throws IOException;

-

-	/**

-	 * Called at the very end, when all child node have been processed and the

-	 * counters for this node are properly populated.

-	 * 

-	 * @param sourceFileLocator

-	 *            source file locator valid for this node

-	 * @throws IOException

-	 *             in case of IO problems with the report writer

-	 */

-	void visitEnd(ISourceFileLocator sourceFileLocator) throws IOException;

+	public void visitEnd() throws IOException;

 

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/ISingleReportOutput.java b/org.jacoco.report/src/org/jacoco/report/ISingleReportOutput.java
deleted file mode 100644
index 89eb0f1..0000000
--- a/org.jacoco.report/src/org/jacoco/report/ISingleReportOutput.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import java.io.IOException;

-import java.io.OutputStream;

-

-/**

- * Interface to emit a single binary files.

- */

-public interface ISingleReportOutput {

-

-	/**

-	 * Creates the output file. The returned {@link OutputStream} has to be

-	 * closed.

-	 * 

-	 * @return output for the content

-	 * @throws IOException

-	 *             if the creation fails

-	 */

-	public OutputStream createFile() throws IOException;

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/MultiFormatter.java b/org.jacoco.report/src/org/jacoco/report/MultiFormatter.java
deleted file mode 100644
index 278047a..0000000
--- a/org.jacoco.report/src/org/jacoco/report/MultiFormatter.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report;

-

-import java.io.IOException;

-import java.util.ArrayList;

-import java.util.Collection;

-import java.util.List;

-

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.data.ExecutionData;

-import org.jacoco.core.data.SessionInfo;

-

-/**

- * A formatter that is composed from multiple other formatters. This can be used

- * to create more than one report format in one run.

- */

-public class MultiFormatter implements IReportFormatter {

-

-	private final List<IReportFormatter> formatters = new ArrayList<IReportFormatter>();

-

-	/**

-	 * Adds the given formatter to the processing chain.

-	 * 

-	 * @param formatter

-	 *            formatter to add

-	 */

-	public void add(final IReportFormatter formatter) {

-		formatters.add(formatter);

-	}

-

-	public IReportVisitor createReportVisitor(final ICoverageNode root,

-			final List<SessionInfo> sessionInfos,

-			final Collection<ExecutionData> executionData) throws IOException {

-		final List<IReportVisitor> visitors = new ArrayList<IReportVisitor>();

-		for (final IReportFormatter f : formatters) {

-			visitors.add(f.createReportVisitor(root, sessionInfos,

-					executionData));

-		}

-		return new MultiVisitor(visitors);

-	}

-

-	private static class MultiVisitor implements IReportVisitor {

-

-		private final List<IReportVisitor> visitors;

-

-		MultiVisitor(final List<IReportVisitor> visitors) {

-			this.visitors = visitors;

-		}

-

-		public IReportVisitor visitChild(final ICoverageNode node)

-				throws IOException {

-			final List<IReportVisitor> children = new ArrayList<IReportVisitor>();

-			for (final IReportVisitor v : visitors) {

-				children.add(v.visitChild(node));

-			}

-			return new MultiVisitor(children);

-		}

-

-		public void visitEnd(final ISourceFileLocator sourceFileLocator)

-				throws IOException {

-			for (final IReportVisitor v : visitors) {

-				v.visitEnd(sourceFileLocator);

-			}

-		}

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/MultiReportVisitor.java b/org.jacoco.report/src/org/jacoco/report/MultiReportVisitor.java
new file mode 100644
index 0000000..a3210a3
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/MultiReportVisitor.java
@@ -0,0 +1,81 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report;

+

+import java.io.IOException;

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.List;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.data.ExecutionData;

+import org.jacoco.core.data.SessionInfo;

+

+/**

+ * A report visitor that is composed from multiple other visitors. This can be

+ * used to create more than one report format in one run.

+ */

+public class MultiReportVisitor extends MultiGroupVisitor implements

+		IReportVisitor {

+

+	private final List<IReportVisitor> visitors;

+

+	/**

+	 * New visitor delegating to all given visitors.

+	 * 

+	 * @param visitors

+	 *            visitors to delegate to

+	 */

+	public MultiReportVisitor(final List<IReportVisitor> visitors) {

+		super(visitors);

+		this.visitors = visitors;

+	}

+

+	public void visitInfo(final List<SessionInfo> sessionInfos,

+			final Collection<ExecutionData> executionData) throws IOException {

+		for (final IReportVisitor v : visitors) {

+			v.visitInfo(sessionInfos, executionData);

+		}

+	}

+

+	public void visitEnd() throws IOException {

+		for (final IReportVisitor v : visitors) {

+			v.visitEnd();

+		}

+	}

+

+}

+

+class MultiGroupVisitor implements IReportGroupVisitor {

+

+	private final List<? extends IReportGroupVisitor> visitors;

+

+	MultiGroupVisitor(final List<? extends IReportGroupVisitor> visitors) {

+		this.visitors = visitors;

+	}

+

+	public void visitBundle(final IBundleCoverage bundle,

+			final ISourceFileLocator locator) throws IOException {

+		for (final IReportGroupVisitor v : visitors) {

+			v.visitBundle(bundle, locator);

+		}

+	}

+

+	public IReportGroupVisitor visitGroup(final String name) throws IOException {

+		final List<IReportGroupVisitor> children = new ArrayList<IReportGroupVisitor>();

+		for (final IReportGroupVisitor v : visitors) {

+			children.add(v.visitGroup(name));

+		}

+		return new MultiGroupVisitor(children);

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/ZipMultiReportOutput.java b/org.jacoco.report/src/org/jacoco/report/ZipMultiReportOutput.java
index a08545a..b92344d 100644
--- a/org.jacoco.report/src/org/jacoco/report/ZipMultiReportOutput.java
+++ b/org.jacoco.report/src/org/jacoco/report/ZipMultiReportOutput.java
@@ -36,6 +36,16 @@
 		this.zip = zip;
 	}
 
+	/**
+	 * Creates a new instance based on the given {@link OutputStream}.
+	 * 
+	 * @param out
+	 *            stream to write file entries to
+	 */
+	public ZipMultiReportOutput(final OutputStream out) {
+		this.zip = new ZipOutputStream(out);
+	}
+
 	public OutputStream createFile(final String path) throws IOException {
 		if (currentEntry != null) {
 			currentEntry.close();
@@ -46,6 +56,10 @@
 		return currentEntry;
 	}
 
+	public void close() throws IOException {
+		zip.close();
+	}
+
 	private final class EntryOutput extends OutputStream {
 
 		private boolean closed = false;
diff --git a/org.jacoco.report/src/org/jacoco/report/csv/CSVFormatter.java b/org.jacoco.report/src/org/jacoco/report/csv/CSVFormatter.java
index 2e047f5..033ffb9 100644
--- a/org.jacoco.report/src/org/jacoco/report/csv/CSVFormatter.java
+++ b/org.jacoco.report/src/org/jacoco/report/csv/CSVFormatter.java
@@ -12,64 +12,27 @@
 package org.jacoco.report.csv;

 

 import java.io.IOException;

+import java.io.OutputStream;

 import java.io.OutputStreamWriter;

 import java.util.Collection;

 import java.util.List;

 

-import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.data.ExecutionData;

 import org.jacoco.core.data.SessionInfo;

 import org.jacoco.report.ILanguageNames;

-import org.jacoco.report.IReportFormatter;

 import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISingleReportOutput;

-import org.jacoco.report.ISourceFileLocator;

 import org.jacoco.report.JavaNames;

 

 /**

  * Report formatter that will create a single CSV file. By default the filename

  * used will be the name of the session.

  */

-public class CSVFormatter implements IReportFormatter {

-

-	private ISingleReportOutput output;

+public class CSVFormatter {

 

 	private ILanguageNames languageNames = new JavaNames();

 

 	private String outputEncoding = "UTF-8";

 

-	public IReportVisitor createReportVisitor(final ICoverageNode root,

-			final List<SessionInfo> sessionInfos,

-			final Collection<ExecutionData> executionData) throws IOException {

-

-		if (output == null) {

-			throw new IllegalStateException("No report output set.");

-		}

-		final DelimitedWriter writer = new DelimitedWriter(

-				new OutputStreamWriter(output.createFile(), outputEncoding));

-		final ClassRowWriter rowWriter = new ClassRowWriter(writer,

-				languageNames);

-		return new CSVGroupHandler(rowWriter, root.getName()) {

-			@Override

-			public void visitEnd(final ISourceFileLocator sourceFileLocator)

-					throws IOException {

-				writer.close();

-				super.visitEnd(sourceFileLocator);

-			}

-		};

-	}

-

-	/**

-	 * Sets the report output callback for this report formatter. This is a

-	 * mandatory property.

-	 * 

-	 * @param output

-	 *            file output

-	 */

-	public void setReportOutput(final ISingleReportOutput output) {

-		this.output = output;

-	}

-

 	/**

 	 * Sets the implementation for language name display. Java language names

 	 * are defined by default.

@@ -100,4 +63,37 @@
 		this.outputEncoding = outputEncoding;

 	}

 

+	/**

+	 * Creates a new visitor to write a report to the given stream.

+	 * 

+	 * @param output

+	 *            output stream to write the report to

+	 * @return visitor to emit the report data to

+	 * @throws IOException

+	 *             in case of problems with the output stream

+	 */

+	public IReportVisitor createVisitor(final OutputStream output)

+			throws IOException {

+		final DelimitedWriter writer = new DelimitedWriter(

+				new OutputStreamWriter(output, outputEncoding));

+		final ClassRowWriter rowWriter = new ClassRowWriter(writer,

+				languageNames);

+		class Visitor extends CSVGroupHandler implements IReportVisitor {

+			Visitor() {

+				super(rowWriter);

+			}

+

+			public void visitInfo(final List<SessionInfo> sessionInfos,

+					final Collection<ExecutionData> executionData)

+					throws IOException {

+				// Info not used for CSV report

+			}

+

+			public void visitEnd() throws IOException {

+				writer.close();

+			}

+		}

+		return new Visitor();

+	}

+

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/csv/CSVGroupHandler.java b/org.jacoco.report/src/org/jacoco/report/csv/CSVGroupHandler.java
index 25ebf31..b10caeb 100644
--- a/org.jacoco.report/src/org/jacoco/report/csv/CSVGroupHandler.java
+++ b/org.jacoco.report/src/org/jacoco/report/csv/CSVGroupHandler.java
@@ -11,44 +11,49 @@
  *******************************************************************************/

 package org.jacoco.report.csv;

 

-import static java.lang.String.format;

-

 import java.io.IOException;

 

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.report.IReportVisitor;

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.IClassCoverage;

+import org.jacoco.core.analysis.IPackageCoverage;

+import org.jacoco.report.IReportGroupVisitor;

 import org.jacoco.report.ISourceFileLocator;

 

 /**

  * Report visitor that handles coverage information for groups.

  */

-class CSVGroupHandler implements IReportVisitor {

+class CSVGroupHandler implements IReportGroupVisitor {

 

 	private final ClassRowWriter writer;

 

 	private final String groupName;

 

-	public CSVGroupHandler(final ClassRowWriter writer, final String groupName) {

+	public CSVGroupHandler(final ClassRowWriter writer) {

+		this(writer, null);

+	}

+

+	private CSVGroupHandler(final ClassRowWriter writer, final String groupName) {

 		this.writer = writer;

 		this.groupName = groupName;

 	}

 

-	public IReportVisitor visitChild(final ICoverageNode node)

-			throws IOException {

-		final ElementType type = node.getElementType();

-		switch (type) {

-		case PACKAGE:

-			return new CSVPackageHandler(writer, groupName, node.getName());

-		case GROUP:

-		case BUNDLE:

-			return new CSVGroupHandler(writer, groupName + "/" + node.getName());

+	public void visitBundle(final IBundleCoverage bundle,

+			final ISourceFileLocator locator) throws IOException {

+		final String groupName = appendName(bundle.getName());

+		for (final IPackageCoverage p : bundle.getPackages()) {

+			final String packageName = p.getName();

+			for (final IClassCoverage c : p.getClasses()) {

+				writer.writeRow(groupName, packageName, c);

+			}

 		}

-		throw new AssertionError(format("Unexpected child node %s.", type));

 	}

 

-	public void visitEnd(final ISourceFileLocator sourceFileLocator)

-			throws IOException {

+	public IReportGroupVisitor visitGroup(final String name) throws IOException {

+		return new CSVGroupHandler(writer, appendName(name));

+	}

+

+	private String appendName(final String name) {

+		return groupName == null ? name : (groupName + "/" + name);

 	}

 

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/csv/CSVPackageHandler.java b/org.jacoco.report/src/org/jacoco/report/csv/CSVPackageHandler.java
deleted file mode 100644
index 725df71..0000000
--- a/org.jacoco.report/src/org/jacoco/report/csv/CSVPackageHandler.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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:

- *    Brock Janiczak - initial API and implementation

- * 

- *******************************************************************************/

-package org.jacoco.report.csv;

-

-import static java.lang.String.format;

-

-import java.io.IOException;

-

-import org.jacoco.core.analysis.IClassCoverage;

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISourceFileLocator;

-

-/**

- * Report visitor that handles coverage information for packages.

- */

-class CSVPackageHandler implements IReportVisitor {

-

-	private final ClassRowWriter writer;

-

-	private final String groupName;

-

-	private final String packageName;

-

-	public CSVPackageHandler(final ClassRowWriter writer,

-			final String groupName, final String packageName) {

-		this.writer = writer;

-		this.groupName = groupName;

-		this.packageName = packageName;

-	}

-

-	public IReportVisitor visitChild(final ICoverageNode node)

-			throws IOException {

-		final ElementType type = node.getElementType();

-		switch (type) {

-		case CLASS:

-			final IClassCoverage classNode = (IClassCoverage) node;

-			writer.writeRow(groupName, packageName, classNode);

-			return IReportVisitor.NOP;

-		case SOURCEFILE:

-			return IReportVisitor.NOP;

-		}

-		throw new AssertionError(format("Unexpected child node %s.", type));

-	}

-

-	public void visitEnd(final ISourceFileLocator sourceFileLocator) {

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/csv/ClassRowWriter.java b/org.jacoco.report/src/org/jacoco/report/csv/ClassRowWriter.java
index 17f2cfd..25b3e27 100644
--- a/org.jacoco.report/src/org/jacoco/report/csv/ClassRowWriter.java
+++ b/org.jacoco.report/src/org/jacoco/report/csv/ClassRowWriter.java
@@ -24,8 +24,9 @@
  */

 class ClassRowWriter {

 

-	private static final CounterEntity[] COUNTERS = { CounterEntity.METHOD,

-			CounterEntity.LINE, CounterEntity.INSTRUCTION, CounterEntity.BRANCH };

+	private static final CounterEntity[] COUNTERS = {

+			CounterEntity.INSTRUCTION, CounterEntity.BRANCH,

+			CounterEntity.LINE, CounterEntity.METHOD };

 

 	private final DelimitedWriter writer;

 

@@ -52,8 +53,8 @@
 	private void writeHeader() throws IOException {

 		writer.write("GROUP", "PACKAGE", "CLASS");

 		for (final CounterEntity entity : COUNTERS) {

-			writer.write(entity.name() + "_COVERED");

 			writer.write(entity.name() + "_MISSED");

+			writer.write(entity.name() + "_COVERED");

 		}

 		writer.nextLine();

 	}

@@ -81,8 +82,8 @@
 

 		for (final CounterEntity entity : COUNTERS) {

 			final ICounter counter = node.getCounter(entity);

-			writer.write(counter.getCoveredCount());

 			writer.write(counter.getMissedCount());

+			writer.write(counter.getCoveredCount());

 		}

 

 		writer.nextLine();

diff --git a/org.jacoco.report/src/org/jacoco/report/html/HTMLFormatter.java b/org.jacoco.report/src/org/jacoco/report/html/HTMLFormatter.java
index 62bded5..30ea239 100644
--- a/org.jacoco.report/src/org/jacoco/report/html/HTMLFormatter.java
+++ b/org.jacoco.report/src/org/jacoco/report/html/HTMLFormatter.java
@@ -22,23 +22,25 @@
 import java.util.List;

 import java.util.Locale;

 

-import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.core.analysis.IBundleCoverage;

 import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

 import org.jacoco.core.data.ExecutionData;

 import org.jacoco.core.data.SessionInfo;

 import org.jacoco.report.ILanguageNames;

 import org.jacoco.report.IMultiReportOutput;

-import org.jacoco.report.IReportFormatter;

+import org.jacoco.report.IReportGroupVisitor;

 import org.jacoco.report.IReportVisitor;

 import org.jacoco.report.ISourceFileLocator;

 import org.jacoco.report.JavaNames;

-import org.jacoco.report.ReportOutputFolder;

-import org.jacoco.report.internal.html.GroupPage;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLGroupVisitor;

 import org.jacoco.report.internal.html.IHTMLReportContext;

 import org.jacoco.report.internal.html.ILinkable;

-import org.jacoco.report.internal.html.SessionsPage;

 import org.jacoco.report.internal.html.index.ElementIndex;

 import org.jacoco.report.internal.html.index.IIndexUpdate;

+import org.jacoco.report.internal.html.page.BundlePage;

+import org.jacoco.report.internal.html.page.ReportPage;

+import org.jacoco.report.internal.html.page.SessionsPage;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.resources.Styles;

 import org.jacoco.report.internal.html.table.BarColumn;

@@ -50,9 +52,7 @@
 /**

  * Formatter for coverage reports in multiple HTML pages.

  */

-public class HTMLFormatter implements IReportFormatter, IHTMLReportContext {

-

-	private IMultiReportOutput output;

+public class HTMLFormatter implements IHTMLReportContext {

 

 	private ILanguageNames languageNames = new JavaNames();

 

@@ -77,17 +77,6 @@
 	}

 

 	/**

-	 * Defines the output for files created by the formatter. This is a

-	 * mandatory property.

-	 * 

-	 * @param output

-	 *            file output

-	 */

-	public void setReportOutput(final IMultiReportOutput output) {

-		this.output = output;

-	}

-

-	/**

 	 * Sets the implementation for language name display. Java language names

 	 * are defined by default.

 	 * 

@@ -191,34 +180,69 @@
 		return locale;

 	}

 

-	// === IReportFormatter ===

-

-	public IReportVisitor createReportVisitor(final ICoverageNode rootNode,

-			final List<SessionInfo> sessionInfos,

-			final Collection<ExecutionData> executionData) throws IOException {

-		if (output == null) {

-			throw new IllegalStateException("No report output set.");

-		}

+	/**

+	 * Creates a new visitor to write a report to the given output.

+	 * 

+	 * @param output

+	 *            output to write the report to

+	 * @return visitor to emit the report data to

+	 * @throws IOException

+	 *             in case of problems with the output stream

+	 */

+	public IReportVisitor createVisitor(final IMultiReportOutput output)

+			throws IOException {

 		final ReportOutputFolder root = new ReportOutputFolder(output);

 		resources = new Resources(root);

 		resources.copyResources();

 		index = new ElementIndex(root);

-		final GroupPage rootpage = new GroupPage(rootNode, null, root, this) {

-			@Override

-			public String getLinkStyle() {

-				return Styles.EL_REPORT;

+		return new IReportVisitor() {

+

+			private List<SessionInfo> sessionInfos;

+			private Collection<ExecutionData> executionData;

+

+			private HTMLGroupVisitor groupHandler;

+

+			public void visitInfo(final List<SessionInfo> sessionInfos,

+					final Collection<ExecutionData> executionData)

+					throws IOException {

+				this.sessionInfos = sessionInfos;

+				this.executionData = executionData;

 			}

 

-			@Override

-			public void visitEnd(final ISourceFileLocator sourceFileLocator)

+			public void visitBundle(final IBundleCoverage bundle,

+					final ISourceFileLocator locator) throws IOException {

+				final BundlePage page = new BundlePage(bundle, null, locator,

+						root, HTMLFormatter.this) {

+					@Override

+					public String getLinkStyle() {

+						return Styles.EL_REPORT;

+					}

+				};

+				createSessionsPage(page);

+				page.render();

+			}

+

+			public IReportGroupVisitor visitGroup(final String name)

 					throws IOException {

-				super.visitEnd(sourceFileLocator);

-				sessionsPage.renderDocument();

+				groupHandler = new HTMLGroupVisitor(null, root,

+						HTMLFormatter.this, name);

+				createSessionsPage(groupHandler.getPage());

+				return groupHandler;

+

+			}

+

+			private void createSessionsPage(final ReportPage rootpage) {

+				sessionsPage = new SessionsPage(sessionInfos, executionData,

+						index, rootpage, root, HTMLFormatter.this);

+			}

+

+			public void visitEnd() throws IOException {

+				if (groupHandler != null) {

+					groupHandler.visitEnd();

+				}

+				sessionsPage.render();

+				output.close();

 			}

 		};

-		sessionsPage = new SessionsPage(sessionInfos, executionData, index,

-				rootpage, root, this);

-		return rootpage;

 	}

-

 }

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/AbstractGroupVisitor.java b/org.jacoco.report/src/org/jacoco/report/internal/AbstractGroupVisitor.java
new file mode 100644
index 0000000..5c1b6a8
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/AbstractGroupVisitor.java
@@ -0,0 +1,99 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.CoverageNodeImpl;

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.ICoverageNode.ElementType;

+import org.jacoco.report.IReportGroupVisitor;

+import org.jacoco.report.ISourceFileLocator;

+

+/**

+ * Internal base visitor to calculate group counter summaries for hierarchical

+ * reports.

+ */

+public abstract class AbstractGroupVisitor implements IReportGroupVisitor {

+

+	/** coverage node for this group to total counters */

+	protected final CoverageNodeImpl total;

+

+	private AbstractGroupVisitor lastChild;

+

+	/**

+	 * Creates a new group with the given name.

+	 * 

+	 * @param name

+	 *            name for the coverage node created internally

+	 */

+	protected AbstractGroupVisitor(final String name) {

+		total = new CoverageNodeImpl(ElementType.GROUP, name);

+	}

+

+	public final void visitBundle(final IBundleCoverage bundle,

+			final ISourceFileLocator locator) throws IOException {

+		total.increment(bundle);

+		handleBundle(bundle, locator);

+	}

+

+	/**

+	 * Called to handle the given bundle in a specific way.

+	 * 

+	 * @param bundle

+	 * @param locator

+	 * @throws IOException

+	 */

+	protected abstract void handleBundle(IBundleCoverage bundle,

+			ISourceFileLocator locator) throws IOException;

+

+	public final IReportGroupVisitor visitGroup(final String name)

+			throws IOException {

+		if (lastChild != null) {

+			lastChild.visitEnd();

+			total.increment(lastChild.total);

+		}

+		lastChild = handleGroup(name);

+		return lastChild;

+	}

+

+	/**

+	 * Called to handle a group with the given name in a specific way.

+	 * 

+	 * @param name

+	 * @return created child group

+	 * @throws IOException

+	 */

+	protected abstract AbstractGroupVisitor handleGroup(final String name)

+			throws IOException;

+

+	/**

+	 * Must be called at the end of every group.

+	 * 

+	 * @throws IOException

+	 */

+	public final void visitEnd() throws IOException {

+		if (lastChild != null) {

+			lastChild.visitEnd();

+			total.increment(lastChild.total);

+		}

+		handleEnd();

+	}

+

+	/**

+	 * Called to handle the end of this group in a specific way.

+	 * 

+	 * @throws IOException

+	 */

+	protected abstract void handleEnd() throws IOException;

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/NormalizedFileNames.java b/org.jacoco.report/src/org/jacoco/report/internal/NormalizedFileNames.java
similarity index 98%
rename from org.jacoco.report/src/org/jacoco/report/NormalizedFileNames.java
rename to org.jacoco.report/src/org/jacoco/report/internal/NormalizedFileNames.java
index a212659..a5723d2 100644
--- a/org.jacoco.report/src/org/jacoco/report/NormalizedFileNames.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/NormalizedFileNames.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report;

+package org.jacoco.report.internal;

 

 import java.util.BitSet;

 import java.util.HashMap;

diff --git a/org.jacoco.report/src/org/jacoco/report/ReportOutputFolder.java b/org.jacoco.report/src/org/jacoco/report/internal/ReportOutputFolder.java
similarity index 97%
rename from org.jacoco.report/src/org/jacoco/report/ReportOutputFolder.java
rename to org.jacoco.report/src/org/jacoco/report/internal/ReportOutputFolder.java
index 41ec26e..230d02b 100644
--- a/org.jacoco.report/src/org/jacoco/report/ReportOutputFolder.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/ReportOutputFolder.java
@@ -9,13 +9,15 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report;

+package org.jacoco.report.internal;

 

 import java.io.IOException;

 import java.io.OutputStream;

 import java.util.HashMap;

 import java.util.Map;

 

+import org.jacoco.report.IMultiReportOutput;

+

 /**

  * Logical representation of a folder in the output structure. This utility

  * ensures valid and unique file names and helps to create relative links.

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/ClassPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/ClassPage.java
deleted file mode 100644
index 5383a37..0000000
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/ClassPage.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report.internal.html;

-

-import java.io.IOException;

-import java.util.ArrayList;

-import java.util.List;

-import java.util.Map;

-

-import org.jacoco.core.analysis.IClassCoverage;

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.IMethodCoverage;

-import org.jacoco.core.analysis.ISourceNode;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ReportOutputFolder;

-import org.jacoco.report.internal.html.resources.Resources;

-import org.jacoco.report.internal.html.resources.Styles;

-import org.jacoco.report.internal.html.table.ITableItem;

-

-/**

- * Page showing coverage information for a class as a table of methods. The

- * methods are linked to the corresponding source file.

- */

-public class ClassPage extends NodePage {

-

-	private class MethodItem implements ITableItem {

-

-		private final IMethodCoverage node;

-

-		MethodItem(final IMethodCoverage node) {

-			this.node = node;

-		}

-

-		public String getLinkLabel() {

-			return context.getLanguageNames().getMethodName(

-					ClassPage.this.getNode().getName(), node.getName(),

-					node.getDesc(), node.getSignature());

-		}

-

-		public String getLinkStyle() {

-			return Styles.EL_METHOD;

-		}

-

-		public String getLink(final ReportOutputFolder base) {

-			final SourceFilePage sourceFilePage = sourceFiles

-					.get(sourceFileName);

-			if (sourceFilePage == null || !sourceFilePage.exists()) {

-				return null;

-			}

-			final String link = sourceFilePage.getLink(base);

-			final ISourceNode source = node;

-			final int first = source.getFirstLine();

-			return first != ISourceNode.UNKNOWN_LINE ? link + "#L" + first

-					: link;

-		}

-

-		public ICoverageNode getNode() {

-			return node;

-		}

-

-	}

-

-	private final List<MethodItem> methods = new ArrayList<MethodItem>();

-

-	private final Map<String, SourceFilePage> sourceFiles;

-

-	private final String label;

-

-	private final String sourceFileName;

-

-	/**

-	 * Creates a new visitor in the given context.

-	 * 

-	 * @param classNode

-	 * @param parent

-	 * @param sourceFiles

-	 * @param folder

-	 * @param context

-	 */

-	public ClassPage(final IClassCoverage classNode, final ReportPage parent,

-			final Map<String, SourceFilePage> sourceFiles,

-			final ReportOutputFolder folder, final IHTMLReportContext context) {

-		super(classNode, parent, folder, context);

-		this.sourceFiles = sourceFiles;

-		this.label = context.getLanguageNames().getClassName(

-				classNode.getName(), classNode.getSignature(),

-				classNode.getSuperName(), classNode.getInterfaceNames());

-		this.sourceFileName = classNode.getSourceFileName();

-		context.getIndexUpdate().addClass(this, classNode.getId());

-	}

-

-	public IReportVisitor visitChild(final ICoverageNode node) {

-		methods.add(new MethodItem((IMethodCoverage) node));

-		return IReportVisitor.NOP;

-	}

-

-	@Override

-	protected void headExtra(final HTMLElement head) throws IOException {

-		super.headExtra(head);

-		head.script("text/javascript",

-				context.getResources().getLink(folder, Resources.SORT_SCRIPT));

-	}

-

-	@Override

-	protected String getOnload() {

-		return "initialSort(['breadcrumb'])";

-	}

-

-	@Override

-	protected void content(final HTMLElement body) throws IOException {

-		context.getTable().render(body, methods, getNode(),

-				context.getResources(), folder);

-	}

-

-	@Override

-	protected String getFileName() {

-		final String vmname = getNode().getName();

-		final int pos = vmname.lastIndexOf('/');

-		final String shortname = pos == -1 ? vmname : vmname.substring(pos + 1);

-		return shortname + ".html";

-	}

-

-	@Override

-	public String getLinkLabel() {

-		return label;

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/GroupPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/GroupPage.java
deleted file mode 100644
index d612712..0000000
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/GroupPage.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report.internal.html;

-

-import java.io.IOException;

-import java.util.ArrayList;

-import java.util.List;

-

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISourceFileLocator;

-import org.jacoco.report.ReportOutputFolder;

-import org.jacoco.report.internal.html.resources.Resources;

-

-/**

- * Page showing coverage information for a node that groups other nodes. The

- * page shows a table of linked nodes.

- */

-public class GroupPage extends NodePage {

-

-	private final List<NodePage> children = new ArrayList<NodePage>();

-

-	/**

-	 * Creates a new visitor in the given context.

-	 * 

-	 * @param node

-	 * @param parent

-	 * @param folder

-	 * @param context

-	 */

-	public GroupPage(final ICoverageNode node, final ReportPage parent,

-			final ReportOutputFolder folder, final IHTMLReportContext context) {

-		super(node, parent, folder, context);

-	}

-

-	public IReportVisitor visitChild(final ICoverageNode node) {

-		final NodePage child;

-		switch (node.getElementType()) {

-		case PACKAGE:

-			child = new PackagePage(node, this, folder.subFolder(node.getName()

-					.replace('/', '.')), context);

-			break;

-		default:

-			child = new GroupPage(node, this, folder.subFolder(node.getName()),

-					context);

-			break;

-		}

-		children.add(child);

-		return child;

-	}

-

-	@Override

-	public void visitEnd(final ISourceFileLocator sourceFileLocator)

-			throws IOException {

-		super.visitEnd(sourceFileLocator);

-		// free memory, otherwise we will keep the complete tree:

-		children.clear();

-	}

-

-	@Override

-	protected void headExtra(final HTMLElement head) throws IOException {

-		super.headExtra(head);

-		head.script("text/javascript",

-				context.getResources().getLink(folder, Resources.SORT_SCRIPT));

-	}

-

-	@Override

-	protected String getOnload() {

-		return "initialSort(['breadcrumb', 'coveragetable'])";

-	}

-

-	@Override

-	protected void content(final HTMLElement body) throws IOException {

-		context.getTable().render(body, children, getNode(),

-				context.getResources(), folder);

-	}

-

-	@Override

-	protected String getFileName() {

-		return "index.html";

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLElement.java b/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLElement.java
index 617e61e..e94cc3f 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLElement.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLElement.java
@@ -14,7 +14,7 @@
 import java.io.IOException;

 import java.io.Writer;

 

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.xml.XMLElement;

 

 /**

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLGroupVisitor.java b/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLGroupVisitor.java
new file mode 100644
index 0000000..8d98722
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/HTMLGroupVisitor.java
@@ -0,0 +1,86 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.report.ISourceFileLocator;

+import org.jacoco.report.internal.AbstractGroupVisitor;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.page.BundlePage;

+import org.jacoco.report.internal.html.page.GroupPage;

+import org.jacoco.report.internal.html.page.NodePage;

+import org.jacoco.report.internal.html.page.ReportPage;

+

+/**

+ * Group visitor for HTML reports.

+ */

+public class HTMLGroupVisitor extends AbstractGroupVisitor {

+

+	private final ReportOutputFolder folder;

+

+	private final IHTMLReportContext context;

+

+	private final GroupPage page;

+

+	/**

+	 * Create a new group handler.

+	 * 

+	 * @param parent

+	 * @param folder

+	 * @param context

+	 * @param name

+	 */

+	public HTMLGroupVisitor(final ReportPage parent,

+			final ReportOutputFolder folder, final IHTMLReportContext context,

+			final String name) {

+		super(name);

+		this.folder = folder;

+		this.context = context;

+		page = new GroupPage(total, parent, folder, context);

+	}

+

+	/**

+	 * Returns the page rendered for this group.

+	 * 

+	 * @return page for this group

+	 */

+	public NodePage<ICoverageNode> getPage() {

+		return page;

+	}

+

+	@Override

+	protected void handleBundle(final IBundleCoverage bundle,

+			final ISourceFileLocator locator) throws IOException {

+		final BundlePage bundlepage = new BundlePage(bundle, page, locator,

+				folder.subFolder(bundle.getName()), context);

+		bundlepage.render();

+		page.addItem(bundlepage);

+	}

+

+	@Override

+	protected AbstractGroupVisitor handleGroup(final String name)

+			throws IOException {

+		final HTMLGroupVisitor handler = new HTMLGroupVisitor(page,

+				folder.subFolder(name), context, name);

+		page.addItem(handler.getPage());

+		return handler;

+	}

+

+	@Override

+	protected void handleEnd() throws IOException {

+		page.render();

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/ILinkable.java b/org.jacoco.report/src/org/jacoco/report/internal/html/ILinkable.java
index ebcac38..895884b 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/ILinkable.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/ILinkable.java
@@ -11,7 +11,7 @@
  *******************************************************************************/
 package org.jacoco.report.internal.html;
 
-import org.jacoco.report.ReportOutputFolder;
+import org.jacoco.report.internal.ReportOutputFolder;
 
 /**
  * Abstraction for items that can be linked to in a report.
diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/PackagePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/PackagePage.java
index 8103c5c..107c22f 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/PackagePage.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/PackagePage.java
@@ -12,74 +12,77 @@
 package org.jacoco.report.internal.html;

 

 import java.io.IOException;

-import java.util.ArrayList;

+import java.io.Reader;

 import java.util.HashMap;

-import java.util.List;

 import java.util.Map;

 

 import org.jacoco.core.analysis.IClassCoverage;

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

+import org.jacoco.core.analysis.IPackageCoverage;

 import org.jacoco.core.analysis.ISourceFileCoverage;

-import org.jacoco.report.IReportVisitor;

 import org.jacoco.report.ISourceFileLocator;

-import org.jacoco.report.ReportOutputFolder;

-import org.jacoco.report.internal.html.resources.Resources;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.page.ClassPage;

+import org.jacoco.report.internal.html.page.ReportPage;

+import org.jacoco.report.internal.html.page.SourceFilePage;

+import org.jacoco.report.internal.html.page.TablePage;

 

 /**

  * Page showing coverage information for a Java package. The page contains a

  * table with all classes of the package.

  */

-public class PackagePage extends NodePage {

+public class PackagePage extends TablePage<IPackageCoverage> {

 

-	private final List<ClassPage> classes = new ArrayList<ClassPage>();

-

-	private final Map<String, SourceFilePage> sourceFiles = new HashMap<String, SourceFilePage>();

+	private final ISourceFileLocator locator;

 

 	/**

 	 * Creates a new visitor in the given context.

 	 * 

 	 * @param node

 	 * @param parent

+	 * @param locator

 	 * @param folder

 	 * @param context

 	 */

-	public PackagePage(final ICoverageNode node, final ReportPage parent,

-			final ReportOutputFolder folder, final IHTMLReportContext context) {

+	public PackagePage(final IPackageCoverage node, final ReportPage parent,

+			final ISourceFileLocator locator, final ReportOutputFolder folder,

+			final IHTMLReportContext context) {

 		super(node, parent, folder, context);

+		this.locator = locator;

 	}

 

-	public IReportVisitor visitChild(final ICoverageNode node) {

-		final ElementType type = node.getElementType();

-		switch (type) {

-		case SOURCEFILE:

-			final SourceFilePage sourcePage = new SourceFilePage(

-					(ISourceFileCoverage) node, this, folder, context);

-			sourceFiles.put(node.getName(), sourcePage);

-			return sourcePage;

-		case CLASS:

-			final ClassPage classPage = new ClassPage((IClassCoverage) node,

-					this, sourceFiles, folder, context);

-			classes.add(classPage);

-			return classPage;

+	@Override

+	public void render() throws IOException {

+		final Map<String, ILinkable> sourceFiles = renderSourceFiles();

+		renderClasses(sourceFiles);

+		super.render();

+	}

+

+	private final Map<String, ILinkable> renderSourceFiles() throws IOException {

+		final Map<String, ILinkable> sourceFiles = new HashMap<String, ILinkable>();

+		final String packagename = getNode().getName();

+		for (final ISourceFileCoverage s : getNode().getSourceFiles()) {

+			final String sourcename = s.getName();

+			final Reader reader = locator

+					.getSourceFile(packagename, sourcename);

+			if (reader != null) {

+				final SourceFilePage sourcePage = new SourceFilePage(s, reader,

+						this, folder, context);

+				sourcePage.render();

+				sourceFiles.put(sourcename, sourcePage);

+			}

+

 		}

-		throw new AssertionError("Unexpected element type " + type);

+		return sourceFiles;

 	}

 

-	@Override

-	public void visitEnd(final ISourceFileLocator sourceFileLocator)

+	private void renderClasses(final Map<String, ILinkable> sourceFiles)

 			throws IOException {

-		super.visitEnd(sourceFileLocator);

-		// free memory, otherwise we will keep the complete tree:

-		classes.clear();

-		sourceFiles.clear();

-	}

-

-	@Override

-	protected void headExtra(final HTMLElement head) throws IOException {

-		super.headExtra(head);

-		head.script("text/javascript",

-				context.getResources().getLink(folder, Resources.SORT_SCRIPT));

+		for (final IClassCoverage c : getNode().getClasses()) {

+			final ClassPage page = new ClassPage(c, this, sourceFiles.get(c

+					.getSourceFileName()), folder, context);

+			page.render();

+			addItem(page);

+		}

 	}

 

 	@Override

@@ -88,12 +91,6 @@
 	}

 

 	@Override

-	protected void content(final HTMLElement body) throws IOException {

-		context.getTable().render(body, classes, getNode(),

-				context.getResources(), folder);

-	}

-

-	@Override

 	protected String getFileName() {

 		return "index.html";

 	}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/SourceFilePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/SourceFilePage.java
deleted file mode 100644
index a07dbe7..0000000
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/SourceFilePage.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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

- *    

- *******************************************************************************/

-package org.jacoco.report.internal.html;

-

-import java.io.IOException;

-import java.io.Reader;

-

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ISourceFileCoverage;

-import org.jacoco.core.analysis.ISourceNode;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISourceFileLocator;

-import org.jacoco.report.ReportOutputFolder;

-import org.jacoco.report.internal.html.resources.Resources;

-

-/**

- * Page showing the content of a source file with numbered and highlighted

- * source lines.

- */

-public class SourceFilePage extends NodePage {

-

-	private Reader sourceReader;

-

-	private final String packageName;

-

-	private final ISourceNode source;

-

-	/**

-	 * Creates a new page with given information.

-	 * 

-	 * @param sourceFileNode

-	 * @param parent

-	 * @param folder

-	 * @param context

-	 */

-	public SourceFilePage(final ISourceFileCoverage sourceFileNode,

-			final ReportPage parent, final ReportOutputFolder folder,

-			final IHTMLReportContext context) {

-		super(sourceFileNode, parent, folder, context);

-		packageName = sourceFileNode.getPackageName();

-		source = sourceFileNode;

-	}

-

-	public IReportVisitor visitChild(final ICoverageNode node) {

-		throw new AssertionError("Source don't have child nodes.");

-	}

-

-	@Override

-	public void visitEnd(final ISourceFileLocator sourceFileLocator)

-			throws IOException {

-		sourceReader = sourceFileLocator.getSourceFile(packageName, getNode()

-				.getName());

-		if (sourceReader != null) {

-			super.visitEnd(sourceFileLocator);

-		}

-	}

-

-	@Override

-	protected void content(final HTMLElement body) throws IOException {

-		final SourceHighlighter hl = new SourceHighlighter(context.getLocale());

-		hl.render(body, source, sourceReader);

-		sourceReader.close();

-	}

-

-	@Override

-	protected void headExtra(final HTMLElement head) throws IOException {

-		super.headExtra(head);

-		head.link(

-				"stylesheet",

-				context.getResources().getLink(folder,

-						Resources.PRETTIFY_STYLESHEET), "text/css");

-		head.script(

-				"text/javascript",

-				context.getResources().getLink(folder,

-						Resources.PRETTIFY_SCRIPT));

-	}

-

-	@Override

-	protected String getOnload() {

-		return "prettyPrint()";

-	}

-

-	@Override

-	protected String getFileName() {

-		return getNode().getName() + ".html";

-	}

-

-	/**

-	 * Checks whether this page has actually been rendered. This might not be

-	 * the case if no source file has been found.

-	 * 

-	 * @return whether the page has been created

-	 */

-	public boolean exists() {

-		return sourceReader != null;

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/index/ElementIndex.java b/org.jacoco.report/src/org/jacoco/report/internal/html/index/ElementIndex.java
index 423a45e..8d32249 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/index/ElementIndex.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/index/ElementIndex.java
@@ -14,7 +14,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
-import org.jacoco.report.ReportOutputFolder;
+import org.jacoco.report.internal.ReportOutputFolder;
 import org.jacoco.report.internal.html.ILinkable;
 
 /**
diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/page/BundlePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/BundlePage.java
new file mode 100644
index 0000000..c21d067
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/BundlePage.java
@@ -0,0 +1,75 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html.page;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.IPackageCoverage;

+import org.jacoco.report.ISourceFileLocator;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.PackagePage;

+

+/**

+ * Page showing coverage information for a bundle. The page contains a table

+ * with all packages of the bundle.

+ */

+public class BundlePage extends TablePage<IBundleCoverage> {

+

+	private final ISourceFileLocator locator;

+

+	/**

+	 * Creates a new visitor in the given context.

+	 * 

+	 * @param node

+	 * @param parent

+	 * @param locator

+	 * @param folder

+	 * @param context

+	 */

+	public BundlePage(final IBundleCoverage node, final ReportPage parent,

+			final ISourceFileLocator locator, final ReportOutputFolder folder,

+			final IHTMLReportContext context) {

+		super(node, parent, folder, context);

+		this.locator = locator;

+	}

+

+	@Override

+	public void render() throws IOException {

+		renderPackages();

+		super.render();

+	}

+

+	private void renderPackages() throws IOException {

+		for (final IPackageCoverage p : getNode().getPackages()) {

+			final String packagename = p.getName();

+			final String foldername = packagename.length() == 0 ? "default"

+					: packagename.replace('/', '.');

+			final PackagePage page = new PackagePage(p, this, locator,

+					folder.subFolder(foldername), context);

+			page.render();

+			addItem(page);

+		}

+	}

+

+	@Override

+	protected String getOnload() {

+		return "initialSort(['breadcrumb', 'coveragetable'])";

+	}

+

+	@Override

+	protected String getFileName() {

+		return "index.html";

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/page/ClassPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/ClassPage.java
new file mode 100644
index 0000000..8546b5c
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/ClassPage.java
@@ -0,0 +1,114 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html.page;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IClassCoverage;

+import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.core.analysis.IMethodCoverage;

+import org.jacoco.core.analysis.ISourceNode;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.ILinkable;

+import org.jacoco.report.internal.html.resources.Styles;

+import org.jacoco.report.internal.html.table.ITableItem;

+

+/**

+ * Page showing coverage information for a class as a table of methods. The

+ * methods are linked to the corresponding source file.

+ */

+public class ClassPage extends TablePage<IClassCoverage> {

+

+	private class MethodItem implements ITableItem {

+

+		private final IMethodCoverage node;

+

+		MethodItem(final IMethodCoverage node) {

+			this.node = node;

+		}

+

+		public String getLinkLabel() {

+			return context.getLanguageNames().getMethodName(

+					ClassPage.this.getNode().getName(), node.getName(),

+					node.getDesc(), node.getSignature());

+		}

+

+		public String getLinkStyle() {

+			return Styles.EL_METHOD;

+		}

+

+		public String getLink(final ReportOutputFolder base) {

+			if (sourcePage == null) {

+				return null;

+			}

+			final String link = sourcePage.getLink(base);

+			final int first = node.getFirstLine();

+			return first != ISourceNode.UNKNOWN_LINE ? link + "#L" + first

+					: link;

+		}

+

+		public ICoverageNode getNode() {

+			return node;

+		}

+

+	}

+

+	private final ILinkable sourcePage;

+

+	/**

+	 * Creates a new visitor in the given context.

+	 * 

+	 * @param classNode

+	 * @param parent

+	 * @param sourcePage

+	 *            corresponding source page or <code>null</code>

+	 * @param folder

+	 * @param context

+	 */

+	public ClassPage(final IClassCoverage classNode, final ReportPage parent,

+			final ILinkable sourcePage, final ReportOutputFolder folder,

+			final IHTMLReportContext context) {

+		super(classNode, parent, folder, context);

+		this.sourcePage = sourcePage;

+		context.getIndexUpdate().addClass(this, classNode.getId());

+	}

+

+	@Override

+	protected String getOnload() {

+		return "initialSort(['breadcrumb'])";

+	}

+

+	@Override

+	public void render() throws IOException {

+		for (final IMethodCoverage m : getNode().getMethods()) {

+			addItem(new MethodItem(m));

+		}

+		super.render();

+	}

+

+	@Override

+	protected String getFileName() {

+		final String vmname = getNode().getName();

+		final int pos = vmname.lastIndexOf('/');

+		final String shortname = pos == -1 ? vmname : vmname.substring(pos + 1);

+		return shortname + ".html";

+	}

+

+	@Override

+	public String getLinkLabel() {

+		return context.getLanguageNames().getClassName(getNode().getName(),

+				getNode().getSignature(), getNode().getSuperName(),

+				getNode().getInterfaceNames());

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/page/GroupPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/GroupPage.java
new file mode 100644
index 0000000..c362954
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/GroupPage.java
@@ -0,0 +1,47 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html.page;

+

+import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+

+/**

+ * Page showing coverage information for a node that groups other nodes. The

+ * page shows a table of linked nodes.

+ */

+public class GroupPage extends TablePage<ICoverageNode> {

+

+	/**

+	 * Creates a new visitor in the given context.

+	 * 

+	 * @param node

+	 * @param parent

+	 * @param folder

+	 * @param context

+	 */

+	public GroupPage(final ICoverageNode node, final ReportPage parent,

+			final ReportOutputFolder folder, final IHTMLReportContext context) {

+		super(node, parent, folder, context);

+	}

+

+	@Override

+	protected String getOnload() {

+		return "initialSort(['breadcrumb', 'coveragetable'])";

+	}

+

+	@Override

+	protected String getFileName() {

+		return "index.html";

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/NodePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/NodePage.java
similarity index 70%
rename from org.jacoco.report/src/org/jacoco/report/internal/html/NodePage.java
rename to org.jacoco.report/src/org/jacoco/report/internal/html/page/NodePage.java
index e82864f..f54b07c 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/NodePage.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/NodePage.java
@@ -9,24 +9,24 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

-

-import java.io.IOException;

+package org.jacoco.report.internal.html.page;

 

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISourceFileLocator;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.IHTMLReportContext;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.table.ITableItem;

 

 /**

  * Report page that represents a coverage node.

+ * 

+ * @param <NodeType>

+ *            type of the node represented by this page

  */

-public abstract class NodePage extends ReportPage implements IReportVisitor,

-		ITableItem {

+public abstract class NodePage<NodeType extends ICoverageNode> extends

+		ReportPage implements ITableItem {

 

-	private ICoverageNode node;

+	private final NodeType node;

 

 	/**

 	 * Creates a new node page.

@@ -40,29 +40,25 @@
 	 * @param context

 	 *            settings context

 	 */

-	protected NodePage(final ICoverageNode node, final ReportPage parent,

+	protected NodePage(final NodeType node, final ReportPage parent,

 			final ReportOutputFolder folder, final IHTMLReportContext context) {

 		super(parent, folder, context);

 		this.node = node;

 	}

 

+	// === ILinkable ===

+

 	public String getLinkStyle() {

 		return Resources.getElementStyle(node.getElementType());

 	}

 

-	public void visitEnd(final ISourceFileLocator sourceFileLocator)

-			throws IOException {

-		renderDocument();

-		this.node = node.getPlainCopy();

-	}

-

-	// === ICoverageTableItem ===

-

 	public String getLinkLabel() {

 		return node.getName();

 	}

 

-	public ICoverageNode getNode() {

+	// === ICoverageTableItem ===

+

+	public NodeType getNode() {

 		return node;

 	}

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/ReportPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/ReportPage.java
similarity index 90%
rename from org.jacoco.report/src/org/jacoco/report/internal/html/ReportPage.java
rename to org.jacoco.report/src/org/jacoco/report/internal/html/page/ReportPage.java
index afff57c..9cd12ca 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/ReportPage.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/ReportPage.java
@@ -9,12 +9,16 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import java.io.IOException;

 

 import org.jacoco.core.JaCoCo;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLDocument;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.ILinkable;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.resources.Styles;

 

@@ -51,11 +55,12 @@
 	}

 

 	/**

-	 * Renders the page content. This method must be called at most once.

+	 * Renders this page's content and optionally additional pages. This method

+	 * must be called at most once.

 	 * 

 	 * @throws IOException

 	 */

-	public final void renderDocument() throws IOException {

+	public void render() throws IOException {

 		final HTMLDocument doc = new HTMLDocument(

 				folder.createFile(getFileName()), context.getOutputEncoding());

 		head(doc.head());

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/SessionsPage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SessionsPage.java
similarity index 95%
rename from org.jacoco.report/src/org/jacoco/report/internal/html/SessionsPage.java
rename to org.jacoco.report/src/org/jacoco/report/internal/html/page/SessionsPage.java
index ca3cca5..434d255 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/SessionsPage.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SessionsPage.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import java.io.IOException;

 import java.text.DateFormat;

@@ -23,7 +23,9 @@
 import org.jacoco.core.data.ExecutionData;

 import org.jacoco.core.data.SessionInfo;

 import org.jacoco.report.ILanguageNames;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.IHTMLReportContext;

 import org.jacoco.report.internal.html.index.ElementIndex;

 import org.jacoco.report.internal.html.resources.Styles;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceFilePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceFilePage.java
new file mode 100644
index 0000000..5c4e7be
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceFilePage.java
@@ -0,0 +1,77 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html.page;

+

+import java.io.IOException;

+import java.io.Reader;

+

+import org.jacoco.core.analysis.ISourceFileCoverage;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.resources.Resources;

+

+/**

+ * Page showing the content of a source file with numbered and highlighted

+ * source lines.

+ */

+public class SourceFilePage extends NodePage<ISourceFileCoverage> {

+

+	private final Reader sourceReader;

+

+	/**

+	 * Creates a new page with given information.

+	 * 

+	 * @param sourceFileNode

+	 * @param sourceReader

+	 * @param parent

+	 * @param folder

+	 * @param context

+	 */

+	public SourceFilePage(final ISourceFileCoverage sourceFileNode,

+			final Reader sourceReader, final ReportPage parent,

+			final ReportOutputFolder folder, final IHTMLReportContext context) {

+		super(sourceFileNode, parent, folder, context);

+		this.sourceReader = sourceReader;

+	}

+

+	@Override

+	protected void content(final HTMLElement body) throws IOException {

+		final SourceHighlighter hl = new SourceHighlighter(context.getLocale());

+		hl.render(body, getNode(), sourceReader);

+		sourceReader.close();

+	}

+

+	@Override

+	protected void headExtra(final HTMLElement head) throws IOException {

+		super.headExtra(head);

+		head.link(

+				"stylesheet",

+				context.getResources().getLink(folder,

+						Resources.PRETTIFY_STYLESHEET), "text/css");

+		head.script(

+				"text/javascript",

+				context.getResources().getLink(folder,

+						Resources.PRETTIFY_SCRIPT));

+	}

+

+	@Override

+	protected String getOnload() {

+		return "prettyPrint()";

+	}

+

+	@Override

+	protected String getFileName() {

+		return getNode().getName() + ".html";

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/SourceHighlighter.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceHighlighter.java
similarity index 97%
rename from org.jacoco.report/src/org/jacoco/report/internal/html/SourceHighlighter.java
rename to org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceHighlighter.java
index 7002b7e..49a9942 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/SourceHighlighter.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/SourceHighlighter.java
@@ -9,7 +9,7 @@
  *    Marc R. Hoffmann - initial API and implementation

  *    

  *******************************************************************************/

-package org.jacoco.report.internal.html;

+package org.jacoco.report.internal.html.page;

 

 import java.io.BufferedReader;

 import java.io.IOException;

@@ -20,6 +20,7 @@
 import org.jacoco.core.analysis.ICounter;

 import org.jacoco.core.analysis.ILine;

 import org.jacoco.core.analysis.ISourceNode;

+import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Styles;

 

 /**

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/page/TablePage.java b/org.jacoco.report/src/org/jacoco/report/internal/html/page/TablePage.java
new file mode 100644
index 0000000..5437a91
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/page/TablePage.java
@@ -0,0 +1,78 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.html.page;

+

+import java.io.IOException;

+import java.util.ArrayList;

+import java.util.List;

+

+import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.report.internal.ReportOutputFolder;

+import org.jacoco.report.internal.html.HTMLElement;

+import org.jacoco.report.internal.html.IHTMLReportContext;

+import org.jacoco.report.internal.html.resources.Resources;

+import org.jacoco.report.internal.html.table.ITableItem;

+

+/**

+ * Report page that contains a table of items linked to other pages.

+ * 

+ * @param <NodeType>

+ *            type of the node represented by this page

+ */

+public abstract class TablePage<NodeType extends ICoverageNode> extends

+		NodePage<NodeType> implements ITableItem {

+

+	private final List<ITableItem> items = new ArrayList<ITableItem>();

+

+	/**

+	 * Creates a new node page.

+	 * 

+	 * @param node

+	 *            corresponding node

+	 * @param parent

+	 *            optional hierarchical parent

+	 * @param folder

+	 *            base folder to create this report in

+	 * @param context

+	 *            settings context

+	 */

+	protected TablePage(final NodeType node, final ReportPage parent,

+			final ReportOutputFolder folder, final IHTMLReportContext context) {

+		super(node, parent, folder, context);

+	}

+

+	/**

+	 * Adds the given item to the table. Method must be called before the page

+	 * is rendered.

+	 * 

+	 * @param item

+	 */

+	public void addItem(final ITableItem item) {

+		items.add(item);

+	}

+

+	@Override

+	protected void headExtra(final HTMLElement head) throws IOException {

+		super.headExtra(head);

+		head.script("text/javascript",

+				context.getResources().getLink(folder, Resources.SORT_SCRIPT));

+	}

+

+	@Override

+	protected void content(final HTMLElement body) throws IOException {

+		context.getTable().render(body, items, getNode(),

+				context.getResources(), folder);

+		// free memory, otherwise we will keep the complete page tree:

+		items.clear();

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/resources/Resources.java b/org.jacoco.report/src/org/jacoco/report/internal/html/resources/Resources.java
index 1ffc3e0..750d7d8 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/resources/Resources.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/resources/Resources.java
@@ -16,7 +16,7 @@
 import java.io.OutputStream;

 

 import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 

 /**

  * Static resource that are included with the coverage report and might be

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/BarColumn.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/BarColumn.java
index 18747cb..60f5b98 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/BarColumn.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/BarColumn.java
@@ -22,7 +22,7 @@
 import org.jacoco.core.analysis.ICounter;

 import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/CounterColumn.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/CounterColumn.java
index fdd04b0..ea73b95 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/CounterColumn.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/CounterColumn.java
@@ -22,7 +22,7 @@
 import org.jacoco.core.analysis.ICounter;

 import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/IColumnRenderer.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/IColumnRenderer.java
index 80fb95c..c251f8e 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/IColumnRenderer.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/IColumnRenderer.java
@@ -16,7 +16,7 @@
 import java.util.List;

 

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/LabelColumn.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/LabelColumn.java
index 92ba6d0..fc2d019 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/LabelColumn.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/LabelColumn.java
@@ -16,7 +16,7 @@
 import java.util.List;

 

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/PercentageColumn.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/PercentageColumn.java
index c1166e4..148cc56 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/PercentageColumn.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/PercentageColumn.java
@@ -22,7 +22,7 @@
 import org.jacoco.core.analysis.ICounter;

 import org.jacoco.core.analysis.ICoverageNode;

 import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/html/table/Table.java b/org.jacoco.report/src/org/jacoco/report/internal/html/table/Table.java
index 2d3ba12..606a3ce 100644
--- a/org.jacoco.report/src/org/jacoco/report/internal/html/table/Table.java
+++ b/org.jacoco.report/src/org/jacoco/report/internal/html/table/Table.java
@@ -18,7 +18,7 @@
 import java.util.List;

 

 import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.report.ReportOutputFolder;

+import org.jacoco.report.internal.ReportOutputFolder;

 import org.jacoco.report.internal.html.HTMLElement;

 import org.jacoco.report.internal.html.resources.Resources;

 import org.jacoco.report.internal.html.resources.Styles;

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLCoverageWriter.java b/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLCoverageWriter.java
new file mode 100644
index 0000000..76ad64f
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLCoverageWriter.java
@@ -0,0 +1,154 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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

+ *    

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

+package org.jacoco.report.internal.xml;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.core.analysis.IClassCoverage;

+import org.jacoco.core.analysis.ICounter;

+import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

+import org.jacoco.core.analysis.ILine;

+import org.jacoco.core.analysis.IMethodCoverage;

+import org.jacoco.core.analysis.IPackageCoverage;

+import org.jacoco.core.analysis.ISourceFileCoverage;

+import org.jacoco.core.analysis.ISourceNode;

+

+/**

+ * Serializes coverage data as XML fragments.

+ */

+public class XMLCoverageWriter {

+

+	/**

+	 * Creates a child element with a name attribute.

+	 * 

+	 * @param parent

+	 *            parent element

+	 * @param tagname

+	 *            name of the child tag

+	 * @param name

+	 *            value of the name attribute

+	 * @return child element

+	 * @throws IOException

+	 */

+	public static XMLElement createChild(final XMLElement parent,

+			final String tagname, final String name) throws IOException {

+		final XMLElement child = parent.element(tagname);

+		child.attr("name", name);

+		return child;

+	}

+

+	/**

+	 * Writes the structure of a given bundle.

+	 * 

+	 * @param bundle

+	 *            bundle coverage data

+	 * @param element

+	 *            container element for the bundle data

+	 * @throws IOException

+	 */

+	public static void writeBundle(final IBundleCoverage bundle,

+			final XMLElement element) throws IOException {

+		for (final IPackageCoverage p : bundle.getPackages()) {

+			writePackage(p, element);

+		}

+		writeCounters(bundle, element);

+	}

+

+	private static void writePackage(final IPackageCoverage p,

+			final XMLElement parent) throws IOException {

+		final XMLElement element = createChild(parent, "package", p.getName());

+		for (final IClassCoverage c : p.getClasses()) {

+			writeClass(c, element);

+		}

+		for (final ISourceFileCoverage s : p.getSourceFiles()) {

+			writeSourceFile(s, element);

+		}

+		writeCounters(p, element);

+	}

+

+	private static void writeClass(final IClassCoverage c,

+			final XMLElement parent) throws IOException {

+		final XMLElement element = createChild(parent, "class", c.getName());

+		for (final IMethodCoverage m : c.getMethods()) {

+			writeMethod(m, element);

+		}

+		writeCounters(c, element);

+	}

+

+	private static void writeMethod(final IMethodCoverage m,

+			final XMLElement parent) throws IOException {

+		final XMLElement element = createChild(parent, "method", m.getName());

+		element.attr("desc", m.getDesc());

+		final int line = m.getFirstLine();

+		if (line != -1) {

+			element.attr("line", line);

+		}

+		writeCounters(m, element);

+	}

+

+	private static void writeSourceFile(final ISourceFileCoverage s,

+			final XMLElement parent) throws IOException {

+		final XMLElement element = createChild(parent, "sourcefile",

+				s.getName());

+		writeLines(s, element);

+		writeCounters(s, element);

+	}

+

+	/**

+	 * Writes all non-zero counters of the given node.

+	 * 

+	 * @param node

+	 *            node to retrieve counters from

+	 * @param parent

+	 *            container for the counter elements

+	 * @throws IOException

+	 */

+	public static void writeCounters(final ICoverageNode node,

+			final XMLElement parent) throws IOException {

+		for (final CounterEntity counterEntity : CounterEntity.values()) {

+			final ICounter counter = node.getCounter(counterEntity);

+			if (counter.getTotalCount() > 0) {

+				final XMLElement counterNode = parent.element("counter");

+				counterNode.attr("type", counterEntity.name());

+				writeCounter(counterNode, "missed", "covered", counter);

+				counterNode.close();

+			}

+		}

+	}

+

+	private static void writeLines(final ISourceNode source,

+			final XMLElement parent) throws IOException {

+		final int last = source.getLastLine();

+		for (int nr = source.getFirstLine(); nr <= last; nr++) {

+			final ILine line = source.getLine(nr);

+			if (line.getStatus() != ICounter.EMPTY) {

+				final XMLElement element = parent.element("line");

+				element.attr("nr", nr);

+				writeCounter(element, "mi", "ci", line.getInstructionCounter());

+				writeCounter(element, "mb", "cb", line.getBranchCounter());

+			}

+		}

+	}

+

+	private static void writeCounter(final XMLElement element,

+			final String missedattr, final String coveredattr,

+			final ICounter counter) throws IOException {

+		element.attr(missedattr, counter.getMissedCount());

+		element.attr(coveredattr, counter.getCoveredCount());

+	}

+

+	private XMLCoverageWriter() {

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLGroupVisitor.java b/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLGroupVisitor.java
new file mode 100644
index 0000000..5cbba87
--- /dev/null
+++ b/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLGroupVisitor.java
@@ -0,0 +1,71 @@
+/*******************************************************************************

+ * Copyright (c) 2009, 2011 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:

+ *    Brock Janiczak - initial API and implementation

+ *    Marc R. Hoffmann - generalized structure, line info

+ *    

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

+package org.jacoco.report.internal.xml;

+

+import java.io.IOException;

+

+import org.jacoco.core.analysis.IBundleCoverage;

+import org.jacoco.report.IReportGroupVisitor;

+import org.jacoco.report.ISourceFileLocator;

+import org.jacoco.report.internal.AbstractGroupVisitor;

+

+/**

+ * {@link IReportGroupVisitor} that transforms the report structure into XML

+ * elements.

+ */

+public class XMLGroupVisitor extends AbstractGroupVisitor {

+

+	/** XML element of this group */

+	protected final XMLElement element;

+

+	/**

+	 * New handler for a group with the given name.

+	 * 

+	 * @param element

+	 *            XML-Element representing this coverage node. The start tag

+	 *            must not be closed yet to allow adding additional attributes.

+	 * @param name

+	 *            name of the group

+	 * @throws IOException

+	 *             in case of problems with the underlying writer

+	 */

+	public XMLGroupVisitor(final XMLElement element, final String name)

+			throws IOException {

+		super(name);

+		this.element = element;

+	}

+

+	@Override

+	protected void handleBundle(final IBundleCoverage bundle,

+			final ISourceFileLocator locator) throws IOException {

+		final XMLElement child = createChild(bundle.getName());

+		XMLCoverageWriter.writeBundle(bundle, child);

+	}

+

+	@Override

+	protected AbstractGroupVisitor handleGroup(final String name)

+			throws IOException {

+		final XMLElement child = createChild(name);

+		return new XMLGroupVisitor(child, name);

+	}

+

+	@Override

+	protected void handleEnd() throws IOException {

+		XMLCoverageWriter.writeCounters(total, element);

+	}

+

+	private XMLElement createChild(final String name) throws IOException {

+		return XMLCoverageWriter.createChild(element, "group", name);

+	}

+

+}

diff --git a/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLReportNodeHandler.java b/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLReportNodeHandler.java
deleted file mode 100644
index 002fdb2..0000000
--- a/org.jacoco.report/src/org/jacoco/report/internal/xml/XMLReportNodeHandler.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*******************************************************************************

- * Copyright (c) 2009, 2011 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:

- *    Brock Janiczak - initial API and implementation

- *    Marc R. Hoffmann - generalized structure, line info

- *    

- *******************************************************************************/

-package org.jacoco.report.internal.xml;

-

-import java.io.IOException;

-

-import org.jacoco.core.analysis.ICounter;

-import org.jacoco.core.analysis.ICoverageNode;

-import org.jacoco.core.analysis.ICoverageNode.CounterEntity;

-import org.jacoco.core.analysis.ICoverageNode.ElementType;

-import org.jacoco.core.analysis.ILine;

-import org.jacoco.core.analysis.IMethodCoverage;

-import org.jacoco.core.analysis.ISourceNode;

-import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISourceFileLocator;

-

-/**

- * Report visitor that transforms the report structure into XML elements.

- */

-public class XMLReportNodeHandler implements IReportVisitor {

-

-	private final XMLElement element;

-

-	private final ICoverageNode node;

-

-	/**

-	 * New handler for the given coverage node.

-	 * 

-	 * @param element

-	 *            XML-Element representing this coverage node. The start tag

-	 *            must not be closed yet to allow adding additional attributes.

-	 * @param node

-	 *            corresponding coverage node

-	 * @throws IOException

-	 *             in case of problems with the underlying writer

-	 */

-	public XMLReportNodeHandler(final XMLElement element,

-			final ICoverageNode node) throws IOException {

-		this.element = element;

-		this.node = node;

-		element.attr("name", node.getName());

-		insertElementsBefore(element);

-	}

-

-	/**

-	 * Hook to add XML elements before the child elements created by default.

-	 * 

-	 * @param element

-	 *            this element

-	 * @throws IOException

-	 */

-	protected void insertElementsBefore(final XMLElement element)

-			throws IOException {

-	}

-

-	/**

-	 * Hook to add XML elements before the child elements created by default.

-	 * 

-	 * @param element

-	 *            this element

-	 * @throws IOException

-	 */

-	void insertElementsAfter(final XMLElement element) throws IOException {

-	}

-

-	public IReportVisitor visitChild(final ICoverageNode node)

-			throws IOException {

-		final ElementType type = node.getElementType();

-		switch (type) {

-		case GROUP:

-		case BUNDLE:

-			return new XMLReportNodeHandler(element.element("group"), node);

-		case PACKAGE:

-			return new XMLReportNodeHandler(element.element("package"), node);

-		case CLASS:

-			return new XMLReportNodeHandler(element.element("class"), node);

-		case METHOD:

-			final XMLElement methodChild = element.element("method");

-			final IMethodCoverage methodNode = (IMethodCoverage) node;

-			methodChild.attr("desc", methodNode.getDesc());

-			final int line = methodNode.getFirstLine();

-			if (line != -1) {

-				methodChild.attr("line", line);

-			}

-			return new XMLReportNodeHandler(methodChild, node);

-		case SOURCEFILE:

-			return new XMLReportNodeHandler(element.element("sourcefile"), node) {

-				@Override

-				protected void insertElementsAfter(final XMLElement element)

-						throws IOException {

-					writeLines((ISourceNode) node, element);

-				}

-			};

-		default:

-			throw new AssertionError(type);

-		}

-	}

-

-	public final void visitEnd(final ISourceFileLocator sourceFileLocator)

-			throws IOException {

-		insertElementsAfter(element);

-		for (final CounterEntity counterEntity : CounterEntity.values()) {

-			createCounterElement(counterEntity);

-		}

-		element.close();

-	}

-

-	private void createCounterElement(final CounterEntity counterEntity)

-			throws IOException {

-		final ICounter counter = node.getCounter(counterEntity);

-		if (counter.getTotalCount() > 0) {

-			final XMLElement counterNode = this.element.element("counter");

-			counterNode.attr("type", counterEntity.name());

-			counterNode.attr("covered", counter.getCoveredCount());

-			counterNode.attr("missed", counter.getMissedCount());

-			counterNode.close();

-		}

-	}

-

-	private static void writeLines(final ISourceNode source,

-			final XMLElement parent) throws IOException {

-		final int last = source.getLastLine();

-		for (int nr = source.getFirstLine(); nr <= last; nr++) {

-			final ILine line = source.getLine(nr);

-			if (line.getStatus() != ICounter.EMPTY) {

-				final XMLElement element = parent.element("line");

-				element.attr("nr", nr);

-				final ICounter insn = line.getInstructionCounter();

-				element.attr("mi", insn.getMissedCount());

-				element.attr("ci", insn.getCoveredCount());

-				final ICounter branches = line.getBranchCounter();

-				element.attr("mb", branches.getMissedCount());

-				element.attr("cb", branches.getCoveredCount());

-			}

-		}

-	}

-

-}

diff --git a/org.jacoco.report/src/org/jacoco/report/xml/XMLFormatter.java b/org.jacoco.report/src/org/jacoco/report/xml/XMLFormatter.java
index 64e7fc1..be51a73 100644
--- a/org.jacoco.report/src/org/jacoco/report/xml/XMLFormatter.java
+++ b/org.jacoco.report/src/org/jacoco/report/xml/XMLFormatter.java
@@ -12,71 +12,32 @@
 package org.jacoco.report.xml;

 

 import java.io.IOException;

+import java.io.OutputStream;

 import java.util.Collection;

 import java.util.List;

 

-import org.jacoco.core.analysis.ICoverageNode;

+import org.jacoco.core.analysis.IBundleCoverage;

 import org.jacoco.core.data.ExecutionData;

 import org.jacoco.core.data.SessionInfo;

-import org.jacoco.report.IReportFormatter;

 import org.jacoco.report.IReportVisitor;

-import org.jacoco.report.ISingleReportOutput;

+import org.jacoco.report.ISourceFileLocator;

+import org.jacoco.report.internal.AbstractGroupVisitor;

+import org.jacoco.report.internal.xml.XMLCoverageWriter;

 import org.jacoco.report.internal.xml.XMLDocument;

 import org.jacoco.report.internal.xml.XMLElement;

-import org.jacoco.report.internal.xml.XMLReportNodeHandler;

+import org.jacoco.report.internal.xml.XMLGroupVisitor;

 

 /**

  * Report formatter that creates a single XML file for a coverage session

  */

-public class XMLFormatter implements IReportFormatter {

+public class XMLFormatter {

 

 	private static final String PUBID = "-//JACOCO//DTD Report 1.0//EN";

 

 	private static final String SYSTEM = "report.dtd";

 

-	private ISingleReportOutput output;

-

 	private String outputEncoding = "UTF-8";

 

-	public IReportVisitor createReportVisitor(final ICoverageNode rootNode,

-			final List<SessionInfo> sessionInfos,

-			final Collection<ExecutionData> executionData) throws IOException {

-

-		if (output == null) {

-			throw new IllegalStateException("No report output set.");

-		}

-		final XMLElement root = new XMLDocument("report", PUBID, SYSTEM,

-				outputEncoding, true, output.createFile());

-		return new XMLReportNodeHandler(root, rootNode) {

-			@Override

-			protected void insertElementsBefore(final XMLElement element)

-					throws IOException {

-				writeSessionInfos(element, sessionInfos);

-			}

-		};

-	}

-

-	private void writeSessionInfos(final XMLElement root,

-			final List<SessionInfo> infos) throws IOException {

-		for (final SessionInfo i : infos) {

-			final XMLElement sessioninfo = root.element("sessioninfo");

-			sessioninfo.attr("id", i.getId());

-			sessioninfo.attr("start", i.getStartTimeStamp());

-			sessioninfo.attr("dump", i.getDumpTimeStamp());

-		}

-	}

-

-	/**

-	 * Sets the report output callback for this report formatter. This is a

-	 * mandatory property.

-	 * 

-	 * @param output

-	 *            report output

-	 */

-	public void setReportOutput(final ISingleReportOutput output) {

-		this.output = output;

-	}

-

 	/**

 	 * Sets the encoding used for generated XML document. Default is UTF-8.

 	 * 

@@ -87,4 +48,64 @@
 		this.outputEncoding = outputEncoding;

 	}

 

+	/**

+	 * Creates a new visitor to write a report to the given stream.

+	 * 

+	 * @param output

+	 *            output stream to write the report to

+	 * @return visitor to emit the report data to

+	 * @throws IOException

+	 *             in case of problems with the output stream

+	 */

+	public IReportVisitor createVisitor(final OutputStream output)

+			throws IOException {

+		final XMLElement root = new XMLDocument("report", PUBID, SYSTEM,

+				outputEncoding, true, output);

+		class RootVisitor extends XMLGroupVisitor implements IReportVisitor {

+

+			RootVisitor(final XMLElement element) throws IOException {

+				super(element, null);

+			}

+

+			private List<SessionInfo> sessionInfos;

+

+			public void visitInfo(final List<SessionInfo> sessionInfos,

+					final Collection<ExecutionData> executionData)

+					throws IOException {

+				this.sessionInfos = sessionInfos;

+			}

+

+			@Override

+			protected void handleBundle(final IBundleCoverage bundle,

+					final ISourceFileLocator locator) throws IOException {

+				writeHeader(bundle.getName());

+				XMLCoverageWriter.writeBundle(bundle, element);

+			}

+

+			@Override

+			protected AbstractGroupVisitor handleGroup(final String name)

+					throws IOException {

+				writeHeader(name);

+				return new XMLGroupVisitor(element, name);

+			}

+

+			private void writeHeader(final String name) throws IOException {

+				element.attr("name", name);

+				for (final SessionInfo i : sessionInfos) {

+					final XMLElement sessioninfo = root.element("sessioninfo");

+					sessioninfo.attr("id", i.getId());

+					sessioninfo.attr("start", i.getStartTimeStamp());

+					sessioninfo.attr("dump", i.getDumpTimeStamp());

+				}

+			}

+

+			@Override

+			protected void handleEnd() throws IOException {

+				super.handleEnd();

+				element.close();

+			}

+		}

+		return new RootVisitor(root);

+	}

+

 }