Table.java

/*******************************************************************************
 * Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
 * This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Marc R. Hoffmann - initial API and implementation
 *
 *******************************************************************************/
package org.jacoco.report.internal.html.table;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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.resources.Resources;
import org.jacoco.report.internal.html.resources.Styles;

/**
 * Renderer for a table of {@link ITableItem}s.
 */
public class Table {

	private final List<Column> columns;

	private Comparator<ITableItem> defaultComparator;

	/**
	 * Create a new table without any columns yet.
	 */
	public Table() {
		this.columns = new ArrayList<Table.Column>();
	}

	/**
	 * Adds a new column with the given properties to the table.
	 *
	 * @param header
	 *            column header caption
	 * @param style
	 *            optional CSS style class name for the td-Elements of this
	 *            column
	 * @param renderer
	 *            callback for column rendering
	 * @param defaultSorting
	 *            If <code>true</code>, this column is the default sorting
	 *            column. Only one column can be selected for default sorting.
	 *
	 */
	public void add(final String header, final String style,
			final IColumnRenderer renderer, final boolean defaultSorting) {
		columns.add(new Column(columns.size(), header, style, renderer,
				defaultSorting));
		if (defaultSorting) {
			if (defaultComparator != null) {
				throw new IllegalStateException(
						"Default sorting only allowed for one column.");
			}
			this.defaultComparator = renderer.getComparator();
		}
	}

	/**
	 * Renders a table for the given icon
	 *
	 * @param parent
	 *            parent element in which the table is created
	 * @param items
	 *            items that will make the table rows
	 * @param total
	 *            the summary of all coverage data items in the table static
	 *            resources that might be referenced
	 * @param resources
	 *            static resources that might be referenced
	 * @param base
	 *            base folder of the table
	 * @throws IOException
	 *             in case of IO problems with the element output
	 */
	public void render(final HTMLElement parent,
			final List<? extends ITableItem> items, final ICoverageNode total,
			final Resources resources, final ReportOutputFolder base)
			throws IOException {
		final List<? extends ITableItem> sortedItems = sort(items);
		final HTMLElement table = parent.table(Styles.COVERAGETABLE);
		table.attr("id", "coveragetable");
		header(table, sortedItems, total);
		footer(table, total, resources, base);
		body(table, sortedItems, resources, base);
	}

	private void header(final HTMLElement table,
			final List<? extends ITableItem> items, final ICoverageNode total)
			throws IOException {
		final HTMLElement tr = table.thead().tr();
		for (final Column c : columns) {
			c.init(tr, items, total);
		}
	}

	private void footer(final HTMLElement table, final ICoverageNode total,
			final Resources resources, final ReportOutputFolder base)
			throws IOException {
		final HTMLElement tr = table.tfoot().tr();
		for (final Column c : columns) {
			c.footer(tr, total, resources, base);
		}
	}

	private void body(final HTMLElement table,
			final List<? extends ITableItem> items, final Resources resources,
			final ReportOutputFolder base) throws IOException {
		final HTMLElement tbody = table.tbody();
		int idx = 0;
		for (final ITableItem item : items) {
			final HTMLElement tr = tbody.tr();
			for (final Column c : columns) {
				c.body(tr, idx, item, resources, base);
			}
			idx++;
		}
	}

	private List<? extends ITableItem> sort(
			final List<? extends ITableItem> items) {
		if (defaultComparator != null) {
			final List<ITableItem> result = new ArrayList<ITableItem>(items);
			Collections.sort(result, defaultComparator);
			return result;
		}
		return items;
	}

	private static class Column {

		private final char idprefix;
		private final String header;
		private final IColumnRenderer renderer;
		private final SortIndex<ITableItem> index;
		private final String style, headerStyle;

		private boolean visible;

		Column(final int idx, final String header, final String style,
				final IColumnRenderer renderer, final boolean defaultSorting) {
			this.idprefix = (char) ('a' + idx);
			this.header = header;
			this.renderer = renderer;
			index = new SortIndex<ITableItem>(renderer.getComparator());
			this.style = style;
			this.headerStyle = Styles.combine(
					defaultSorting ? Styles.DOWN : null, Styles.SORTABLE,
					style);
		}

		void init(final HTMLElement tr, final List<? extends ITableItem> items,
				final ICoverageNode total) throws IOException {
			visible = renderer.init(items, total);
			if (visible) {
				index.init(items);
				final HTMLElement td = tr.td(headerStyle);
				td.attr("id", String.valueOf(idprefix));
				td.attr("onclick", "toggleSort(this)");
				td.text(header);
			}
		}

		void footer(final HTMLElement tr, final ICoverageNode total,
				final Resources resources, final ReportOutputFolder base)
				throws IOException {
			if (visible) {
				renderer.footer(tr.td(style), total, resources, base);
			}
		}

		void body(final HTMLElement tr, final int idx, final ITableItem item,
				final Resources resources, final ReportOutputFolder base)
				throws IOException {
			if (visible) {
				final HTMLElement td = tr.td(style);
				td.attr("id",
						idprefix + String.valueOf(index.getPosition(idx)));
				renderer.item(td, item, resources, base);
			}
		}

	}

}