blob: 3f11974f03c41378b6b423e3c401b657dd1f173b [file] [log] [blame]
/*
* Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.javadoc.internal.doclets.formats.html.markup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import javax.lang.model.element.Element;
import jdk.javadoc.internal.doclets.formats.html.Contents;
import jdk.javadoc.internal.doclets.toolkit.Content;
import jdk.javadoc.internal.doclets.toolkit.util.DocletConstants;
/**
* A builder for HTML tables, such as the summary tables for various
* types of element.
*
* <p>The table should be used in three phases:
* <ol>
* <li>Configuration: the overall characteristics of the table should be specified
* <li>Population: the content for the cells in each row should be added
* <li>Generation: the HTML content and any associated JavaScript can be accessed
* </ol>
*
* Many methods return the current object, to facilitate fluent builder-style usage.
*
* <p><b>This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interfaces are subject to change or
* deletion without notice.</b>
*/
public class Table {
private final HtmlVersion version;
private final HtmlStyle tableStyle;
private String summary;
private Content caption;
private Map<String, Predicate<Element>> tabMap;
private String defaultTab;
private Set<String> tabs;
private HtmlStyle activeTabStyle = HtmlStyle.activeTableTab;
private HtmlStyle tabStyle = HtmlStyle.tableTab;
private HtmlStyle tabEnd = HtmlStyle.tabEnd;
private IntFunction<String> tabScript;
private Function<Integer, String> tabId = (i -> "t" + i);
private TableHeader header;
private List<HtmlStyle> columnStyles;
private int rowScopeColumnIndex;
private List<HtmlStyle> stripedStyles = Arrays.asList(HtmlStyle.altColor, HtmlStyle.rowColor);
private final List<Content> bodyRows;
private final List<Integer> bodyRowMasks;
private String rowIdPrefix = "i";
// compatibility flags
private boolean putIdFirst = false;
private boolean useTBody = true;
/**
* Creates a builder for an HTML table.
*
* @param version the version of HTML, used to determine is a {@code summary}
* attribute is needed
* @param style the style class for the {@code <table>} tag
*/
public Table(HtmlVersion version, HtmlStyle style) {
this.version = version;
this.tableStyle = style;
bodyRows = new ArrayList<>();
bodyRowMasks = new ArrayList<>();
}
/**
* Sets the summary for the table.
* This is ignored if the HTML version for the table is not {@link HtmlVersion#HTML4}.
*
* @param summary the summary
* @return this object
*/
public Table setSummary(String summary) {
if (version == HtmlVersion.HTML4) {
this.summary = summary;
}
return this;
}
/**
* Sets the caption for the table.
* This is ignored if the table is configured to provide tabs to select
* different subsets of rows within the table.
* The caption should be suitable for use as the content of a {@code <caption>}
* element.
*
* <b>For compatibility, the code currently accepts a {@code <caption>} element
* as well. This should be removed when all clients rely on using the {@code <caption>}
* element being generated by this class.</b>
*
* @param captionContent the caption
* @return this object
*/
public Table setCaption(Content captionContent) {
if (captionContent instanceof HtmlTree
&& ((HtmlTree) captionContent).htmlTag == HtmlTag.CAPTION) {
caption = captionContent;
} else {
caption = getCaption(captionContent);
}
return this;
}
/**
* Adds a tab to the table.
* Tabs provide a way to display subsets of rows, as determined by a
* predicate for the tab, and an element associated with each row.
* Tabs will appear left-to-right in the order they are added.
*
* @param name the name of the tab
* @param predicate the predicate
* @return this object
*/
public Table addTab(String name, Predicate<Element> predicate) {
if (tabMap == null) {
tabMap = new LinkedHashMap<>(); // preserves order that tabs are added
tabs = new HashSet<>(); // order not significant
}
tabMap.put(name, predicate);
return this;
}
/**
* Sets the name for the default tab, which displays all the rows in the table.
* This tab will appear first in the left-to-right list of displayed tabs.
*
* @param name the name
* @return this object
*/
public Table setDefaultTab(String name) {
defaultTab = name;
return this;
}
/**
* Sets the function used to generate the JavaScript to be used when a tab is selected.
* When the function is invoked, the argument will be an integer value containing
* the bit mask identifying the rows to be selected.
*
* @param f the function
* @return this object
*/
public Table setTabScript(IntFunction<String> f) {
tabScript = f;
return this;
}
/**
* Sets the name of the styles used to display the tabs.
*
* @param activeTabStyle the style for the active tab
* @param tabStyle the style for other tabs
* @param tabEnd the style for the padding that appears within each tab
* @return this object
*/
public Table setTabStyles(HtmlStyle activeTabStyle, HtmlStyle tabStyle, HtmlStyle tabEnd) {
this.activeTabStyle = activeTabStyle;
this.tabStyle = tabStyle;
this.tabEnd = tabEnd;
return this;
}
/**
* Sets the JavaScript function used to generate the {@code id} attribute for each tag.
* The default is to use <code>t</code><i>N</i> where <i>N</i> is the index of the tab,
* counting from 0 (for the default tab), and then from 1 upwards for additional tabs.
*
* @param f the function
* @return this object
*/
public Table setTabId(Function<Integer,String> f) {
tabId = f;
return this;
}
/**
* Sets the header for the table.
*
* <p>Notes:
* <ul>
* <li>This currently does not use a {@code <thead>} tag, but probably should, eventually
* <li>The column styles are not currently applied to the header, but probably should, eventually
* </ul>
*
* @param header the header
* @return this object
*/
public Table setHeader(TableHeader header) {
this.header = header;
return this;
}
/**
* Sets the styles used for {@code <tr>} tags, to give a "striped" appearance.
* The defaults are currently {@code rowColor} and {@code altColor}.
*
* @param evenRowStyle the style to use for even-numbered rows
* @param oddRowStyle the style to use for odd-numbered rows
* @return
*/
public Table setStripedStyles(HtmlStyle evenRowStyle, HtmlStyle oddRowStyle) {
stripedStyles = Arrays.asList(evenRowStyle, oddRowStyle);
return this;
}
/**
* Sets the column used to indicate which cell in a row should be declared
* as a header cell with the {@code scope} attribute set to {@code row}.
*
* @param columnIndex the column index
* @return this object
*/
public Table setRowScopeColumn(int columnIndex) {
rowScopeColumnIndex = columnIndex;
return this;
}
/**
* Sets the styles for be used for the cells in each row.
*
* <p>Note:
* <ul>
* <li>The column styles are not currently applied to the header, but probably should, eventually
* </ul>
*
* @param styles the styles
* @return this object
*/
public Table setColumnStyles(HtmlStyle... styles) {
return setColumnStyles(Arrays.asList(styles));
}
/**
* Sets the styles for be used for the cells in each row.
*
* <p>Note:
* <ul>
* <li>The column styles are not currently applied to the header, but probably should, eventually
* </ul>
*
* @param styles the styles
* @return this object
*/
public Table setColumnStyles(List<HtmlStyle> styles) {
columnStyles = styles;
return this;
}
/**
* Sets the prefix used for the {@code id} attribute for each row in the table.
* The default is "i".
*
* <p>Note:
* <ul>
* <li>The prefix should probably be a value such that the generated ids cannot
* clash with any other id, such as those that might be created for fields within
* a class.
* </ul>
*
* @param prefix the prefix
* @return this object
*/
public Table setRowIdPrefix(String prefix) {
rowIdPrefix = prefix;
return this;
}
/**
* Sets whether the {@code id} attribute should appear first in a {@code <tr>} tag.
* The default is {@code false}.
*
* <b>This is a compatibility feature that should be removed when all tables use a
* consistent policy.</b>
*
* @param first whether to put {@code id} attributes first
* @return this object
*/
public Table setPutIdFirst(boolean first) {
this.putIdFirst = first;
return this;
}
/**
* Sets whether or not to use an explicit {@code <tbody>} element to enclose the rows
* of a table.
* The default is {@code true}.
*
* <b>This is a compatibility feature that should be removed when all tables use a
* consistent policy.</b>
*
* @param use whether o use a {@code <tbody> element
* @return this object
*/
public Table setUseTBody(boolean use) {
this.useTBody = use;
return this;
}
/**
* Add a row of data to the table.
* Each item of content should be suitable for use as the content of a
* {@code <th>} or {@code <td>} cell.
* This method should not be used when the table has tabs: use a method
* that takes an {@code Element} parameter instead.
*
* @param contents the contents for the row
*/
public void addRow(Content... contents) {
addRow(null, Arrays.asList(contents));
}
/**
* Add a row of data to the table.
* Each item of content should be suitable for use as the content of a
* {@code <th>} or {@code <td> cell}.
* This method should not be used when the table has tabs: use a method
* that takes an {@code element} parameter instead.
*
* @param contents the contents for the row
*/
public void addRow(List<Content> contents) {
addRow(null, contents);
}
/**
* Add a row of data to the table.
* Each item of content should be suitable for use as the content of a
* {@code <th>} or {@code <td>} cell.
*
* If tabs have been added to the table, the specified element will be used
* to determine whether the row should be displayed when any particular tab
* is selected, using the predicate specified when the tab was
* {@link #add(String,Predicate) added}.
*
* @param element the element
* @param contents the contents for the row
* @throws NullPointerException if tabs have previously been added to the table
* and {@code element} is null
*/
public void addRow(Element element, Content... contents) {
addRow(element, Arrays.asList(contents));
}
/**
* Add a row of data to the table.
* Each item of content should be suitable for use as the content of a
* {@code <th>} or {@code <td>} cell.
*
* If tabs have been added to the table, the specified element will be used
* to determine whether the row should be displayed when any particular tab
* is selected, using the predicate specified when the tab was
* {@link #add(String,Predicate) added}.
*
* @param element the element
* @param contents the contents for the row
* @throws NullPointerException if tabs have previously been added to the table
* and {@code element} is null
*/
public void addRow(Element element, List<Content> contents) {
if (tabMap != null && element == null) {
throw new NullPointerException();
}
HtmlTree row = new HtmlTree(HtmlTag.TR);
if (putIdFirst && tabMap != null) {
int index = bodyRows.size();
row.addAttr(HtmlAttr.ID, (rowIdPrefix + index));
}
if (stripedStyles != null) {
int rowIndex = bodyRows.size();
row.addAttr(HtmlAttr.CLASS, stripedStyles.get(rowIndex % 2).name());
}
int colIndex = 0;
for (Content c : contents) {
HtmlStyle cellStyle = (columnStyles == null || colIndex > columnStyles.size())
? null
: columnStyles.get(colIndex);
HtmlTree cell = (colIndex == rowScopeColumnIndex)
? HtmlTree.TH(cellStyle, "row", c)
: HtmlTree.TD(cellStyle, c);
row.addContent(cell);
colIndex++;
}
bodyRows.add(row);
if (tabMap != null) {
if (!putIdFirst) {
int index = bodyRows.size() - 1;
row.addAttr(HtmlAttr.ID, (rowIdPrefix + index));
}
int mask = 0;
int maskBit = 1;
for (Map.Entry<String, Predicate<Element>> e : tabMap.entrySet()) {
String name = e.getKey();
Predicate<Element> predicate = e.getValue();
if (predicate.test(element)) {
tabs.add(name);
mask |= maskBit;
}
maskBit = (maskBit << 1);
}
bodyRowMasks.add(mask);
}
}
/**
* Returns whether or not the table is empty.
* The table is empty if it has no (body) rows.
*
* @return true if the table has no rows
*/
public boolean isEmpty() {
return bodyRows.isEmpty();
}
/**
* Returns the HTML for the table.
*
* @return the HTML
*/
public Content toContent() {
HtmlTree table = new HtmlTree(HtmlTag.TABLE);
table.setStyle(tableStyle);
if (summary != null) {
table.addAttr(HtmlAttr.SUMMARY, summary);
}
if (tabMap != null) {
if (tabs.size() == 1) {
String tabName = tabs.iterator().next();
table.addContent(getCaption(new StringContent(tabName)));
} else {
ContentBuilder cb = new ContentBuilder();
int tabIndex = 0;
HtmlTree defaultTabSpan = new HtmlTree(HtmlTag.SPAN,
HtmlTree.SPAN(new StringContent(defaultTab)),
HtmlTree.SPAN(tabEnd, Contents.SPACE))
.addAttr(HtmlAttr.ID, tabId.apply(tabIndex))
.setStyle(activeTabStyle);
cb.addContent(defaultTabSpan);
for (String tabName : tabMap.keySet()) {
tabIndex++;
if (tabs.contains(tabName)) {
String script = "javascript:" + tabScript.apply(1 << (tabIndex - 1));
HtmlTree link = HtmlTree.A(script, new StringContent(tabName));
HtmlTree tabSpan = new HtmlTree(HtmlTag.SPAN,
HtmlTree.SPAN(link), HtmlTree.SPAN(tabEnd, Contents.SPACE))
.addAttr(HtmlAttr.ID, tabId.apply(tabIndex))
.setStyle(tabStyle);
cb.addContent(tabSpan);
}
}
table.addContent(HtmlTree.CAPTION(cb));
}
} else {
table.addContent(caption);
}
table.addContent(header.toContent());
if (useTBody) {
Content tbody = new HtmlTree(HtmlTag.TBODY);
bodyRows.forEach(row -> tbody.addContent(row));
table.addContent(tbody);
} else {
bodyRows.forEach(row -> table.addContent(row));
}
return table;
}
/**
* Returns whether or not the table needs JavaScript support.
* It requires such support if tabs have been added.
*
* @return true if JavaScript is required
*/
public boolean needsScript() {
return (tabs != null) && (tabs.size() > 1);
}
/**
* Returns the script to be used in conjunction with the table.
*
* @return the script
*/
public String getScript() {
if (tabMap == null)
throw new IllegalStateException();
StringBuilder sb = new StringBuilder();
// Add the variable defining the bitmask for each row
sb.append("var data").append(" = {");
int rowIndex = 0;
for (int mask : bodyRowMasks) {
if (rowIndex > 0) {
sb.append(",");
}
sb.append("\"").append(rowIdPrefix).append(rowIndex).append("\":").append(mask);
rowIndex++;
}
sb.append("};\n");
// Add the variable defining the tabs
sb.append("var tabs = {");
appendTabInfo(sb, 65535, tabId.apply(0), defaultTab);
int tabIndex = 1;
int maskBit = 1;
for (String tabName: tabMap.keySet()) {
if (tabs.contains(tabName)) {
sb.append(",");
appendTabInfo(sb, maskBit, tabId.apply(tabIndex), tabName);
}
tabIndex++;
maskBit = (maskBit << 1);
}
sb.append("};\n");
// Add the variables defining the stylenames
appendStyleInfo(sb,
stripedStyles.get(0), stripedStyles.get(1), tabStyle, activeTabStyle);
return sb.toString();
}
private void appendTabInfo(StringBuilder sb, int value, String id, String name) {
sb.append(value)
.append(":[")
.append(Script.stringLiteral(id))
.append(",")
.append(Script.stringLiteral(name))
.append("]");
}
private void appendStyleInfo(StringBuilder sb, HtmlStyle... styles) {
for (HtmlStyle style : styles) {
sb.append("var ").append(style).append(" = \"").append(style).append("\";\n");
}
}
private HtmlTree getCaption(Content title) {
return new HtmlTree(HtmlTag.CAPTION,
HtmlTree.SPAN(title),
HtmlTree.SPAN(tabEnd, Contents.SPACE));
}
}