I am trying to generate an HTML table using Java. I have an object which I am iterating to make an HTML table.
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append("<head>");
sb.append("</head>");
sb.append("<table>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> ClientName");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> SyncCount");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> SyncPercentile");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> SyncAvg");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> SyncMax");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> AsyncCount");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> AsyncPercentile");
sb.append("</th>");
sb.append("<th style = \"background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;\"> AsyncAvg");
sb.append("</th>");
for (DataMetrics metrics : dataMetricsList) {
sb.append("<tr>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getName());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getSyncCall());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getSyncPercent());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getSyncAvg());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getSyncMax());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getAsyncCall());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getAsyncPercent());
sb.append("</td>");
sb.append("<td style = \"padding: 6px; border: 1px solid #ccc; text-align: left;\"> " + metrics.getAsyncAvg());
sb.append("</td>");
sb.append("</tr>");
}
sb.append("</table>");
sb.append("</body>");
sb.append("</html>");
System.out.println(sb.toString());
Somehow, the code looks pretty ugly to me in the way HTML and CSS are being used within StringBuilder
. I am opting for code review to see whether we can improve anything here. Is there any better way of writing this code?
-
\$\begingroup\$ I am sure there are plenty of libraries that do this, but you could do it yourself: Write objects that are abstractions of the HTML-elements you are creating. That makes everything much easier and safer, at the cost of some performance. The first thing a google search found was "Gagawa" for example. \$\endgroup\$ASA– ASA2014年09月19日 13:03:38 +00:00Commented Sep 19, 2014 at 13:03
-
\$\begingroup\$ HtmlFlow (deployed at Maven Central Repository) provides a simple API to write HTML in a fluent style. Check it here: github.com/fmcarvalho/HtmlFlow. \$\endgroup\$Miguel Gamboa– Miguel Gamboa2016年02月01日 17:18:50 +00:00Commented Feb 1, 2016 at 17:18
6 Answers 6
How about extracting the
style=""
s into thehead
?sb.append("<style>" + "td { padding: 6px; border: 1px solid #ccc; text-align: left; }" + "th { background: #333; color: white; font-weight: bold; padding: 6px; border: 1px solid #ccc; text-align: left;}" + "</style>");
You should write helper methods, that do the 'heavy lifting' for you:
void appendTag(StringBuilder sb, String tag, String contents) { sb.append('<').append(tag).append('>'); sb.append(contents); sb.append("</").append(tag).append('>'); } void appendDataCell(StringBuilder sb, String contents) { appendTag(sb, "td", contents); } void appendHeaderCell(StringBuilder sb, String contents) { appendTag(sb, "th", contents); }
Please note, that if you are using StringBuilder
you should not concatenate strings with +
. You want to get rid of string concatenation, not do it.
If you want to get even more expressive you should take a look at existing template engines. They are optimized and mostly easy to use.
Suggesting mustache:
If you are using mustache, you can write a template like this:
<html>
<head>
<title>Table</title>
<style>...</style>
</head>
<body>
<table>
<tr><th>....</th></tr>
{{#metrics}}
<tr>
<td>{{getName}}</td>
...
</tr>
{{/metrics}}
</body>
</html>
and then render it via e.g.
public static void main(String[] args) throws IOException {
HashMap<String, Object> scopes = new HashMap<String, Object>();
scopes.put("metrics", dataMetricsList);
Writer writer = new OutputStreamWriter(System.out);
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile(new StringReader(getTemplateCodeFromAbove()), "example");
mustache.execute(writer, scopes);
writer.flush();
}
It depends on your use case, of course. If all you ever want is to render a single table, you are fine with StringBuilder
. But if you find yourself repeating this, get an external library for it.
-
\$\begingroup\$ Thanks a lot thriqon. I was not able to understand what do you mean by existing template engines. Can you provide an example what do you mean by that and how it will be useful to me? \$\endgroup\$arsenal– arsenal2014年09月19日 06:41:00 +00:00Commented Sep 19, 2014 at 6:41
-
4\$\begingroup\$ A templating language like Mustache allows you to have a pre-written template, with placeholders for information, and then fill that information from Java, and render the result. It's a form of separation between logic and presentation which is invaluable when scaling. \$\endgroup\$Madara's Ghost– Madara's Ghost2014年09月19日 06:57:41 +00:00Commented Sep 19, 2014 at 6:57
-
\$\begingroup\$ Thanks @MadaraUchiha, important clarification, I forgot about that. \$\endgroup\$thriqon– thriqon2014年09月19日 07:24:18 +00:00Commented Sep 19, 2014 at 7:24
-
\$\begingroup\$ Or, failing a templating engine, some library to write HTML in Java - like jmesa. +100 for excellent suggestions to remove duplication and separate concerns. \$\endgroup\$Boris the Spider– Boris the Spider2014年09月19日 13:03:38 +00:00Commented Sep 19, 2014 at 13:03
I already felt that need in the past and I end up developing a java library--HtmlFlow (deployed at Maven Central Repository)--that provides a simple API to write HTML in a fluent style. Check it here: https://github.com/fmcarvalho/HtmlFlow.
You can use HtmlFlow with, or without, data binding, but here I present an example of binding the properties of a Task
object into HTML elements. Consider a Task
Java class with three properties: Title
, Description
and a Priority
and then we can produce an HTML document for a Task
object in the following way:
import htmlflow.HtmlView;
import model.Priority;
import model.Task;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class App {
private static HtmlView<Task> taskDetailsView(){
HtmlView<Task> taskView = new HtmlView<>();
taskView
.head()
.title("Task Details")
.linkCss("https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css");
taskView
.body().classAttr("container")
.heading(1, "Task Details")
.hr()
.div()
.text("Title: ").text(Task::getTitle)
.br()
.text("Description: ").text(Task::getDescription)
.br()
.text("Priority: ").text(Task::getPriority);
return taskView;
}
public static void main(String [] args) throws IOException{
HtmlView<Task> taskView = taskDetailsView();
Task task = new Task("Special dinner", "Have dinner with someone!", Priority.Normal);
try(PrintStream out = new PrintStream(new FileOutputStream("Task.html"))){
taskView.setPrintStream(out).write(task);
Runtime.getRuntime().exec("explorer Task.html");
}
}
}
-
\$\begingroup\$ This library is super slow writing tables! On my system, saving a list with 100k entries takes Java nio less than a second. Using this library around 20. \$\endgroup\$John Smith– John Smith2018年04月14日 08:51:19 +00:00Commented Apr 14, 2018 at 8:51
-
\$\begingroup\$ @JohnSmith maybe the overhead is not in library itself but in writing through a PrintStream to a file. Did you try to save the same records without HtmlFlow with a PrintStream and compare it with your nio approach? Probably you will observe the same slowdown!!!? \$\endgroup\$Miguel Gamboa– Miguel Gamboa2018年04月22日 15:21:09 +00:00Commented Apr 22, 2018 at 15:21
-
\$\begingroup\$ @JohnSmith The problem is in fact on the
PrintStream
use. We can speedup it with anew BufferedOutputStream(out, 1024*128)
interleaved between thePrintStream
and theOutputStream
. But we can increase the performance even more and achieve a performance of less than 1 second for 100k entries, if we avoid thePrintStream
at all and use for example aDataOutputStream
. I will fix the internal printer implementation ofHtmlFlow
to suppress this bottleneck. I will add a comment here when I release a new version fixing this performance problem. Thanks for your feedback. \$\endgroup\$Miguel Gamboa– Miguel Gamboa2018年04月26日 16:03:56 +00:00Commented Apr 26, 2018 at 16:03 -
\$\begingroup\$ Your dedication is admirable. I have moved on long ago and went on with my own implementation for my specific cornercase. Yet I will keep in mind that there's this neat library HtmlFlow and when I face another table printing issue, I will for sure come back! Thanks! \$\endgroup\$John Smith– John Smith2018年08月01日 10:04:38 +00:00Commented Aug 1, 2018 at 10:04
-
\$\begingroup\$ Thank you @JohnSmith. We have just released version 2.1 that includes in
HtmlView
a new method:String render()
, which uses internally aStringBuilder
rather thanPrintStream
and effectively shows much better performance. Now we have some alerts onREADME
about the use ofPrintStream
. \$\endgroup\$Miguel Gamboa– Miguel Gamboa2018年08月06日 12:10:55 +00:00Commented Aug 6, 2018 at 12:10
thriqon's Answer is a good one, you should move your style code somewhere, but the table headers aren't far enough, you should move all your style into a CSS file that would look like this
th {
background: #333;
color: white;
font-weight: bold;
padding: 6px;
border: 1px solid #ccc;
text-align: left;
}
td {
padding: 6px;
border: 1px solid #ccc;
text-align: left;
}
If you have more than one table than you should look at either creating an ID for each Table or classes for the Tables that have the same Style.
By Doing this, you can change the theme of the page without having to go through the code, you just change the Stylesheet.
After the CSS(styling) is all sorted out you can change your code a little bit, cleaning it up by removing lines of code completely.
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append("<head>");
sb.append("</head>");
sb.append("<table>");
sb.append("<th> ClientName </th>");
sb.append("<th> SyncCount </th>");
sb.append("<th> SyncPercentile </th>");
sb.append("<th> SyncAvg </th>");
sb.append("<th> SyncMax </th>");
sb.append("<th> AsyncCount </th>");
sb.append("<th> AsyncPercentile </th>");
sb.append("<th> AsyncAvg </th>");
for (DataMetrics metrics : dataMetricsList) {
sb.append("<tr>");
sb.append("<td> " + metrics.getName() + " </td>");
sb.append("<td> " + metrics.getSyncCall() + " </td>");
sb.append("<td> " + metrics.getSyncPercent() + " </td>");
sb.append("<td> " + metrics.getSyncAvg() + " </td>");
sb.append("<td> " + metrics.getSyncMax() + " </td>");
sb.append("<td> " + metrics.getAsyncCall() + " </td>");
sb.append("<td> " + metrics.getAsyncPercent() + " </td>");
sb.append("<td> " + metrics.getAsyncAvg() + " </td>");
sb.append("</tr>");
}
sb.append("</table>");
sb.append("</body>");
sb.append("</html>");
System.out.println(sb.toString());
You really don't need to have a line just for the end tags (th
and td
) they can share.
This looks a lot cleaner already.
I do agree that a tag helper would be really nice, and the Template Engine looks like a really nice way to go as well.
If you happen to use a recent versions of Java, you can take avantage of text blocks, i.e. """
(which were introduced as a preview feature in Java 13). They greatly increase the readability of your code by getting the double quotes out of the way while being formattable with the formatted
method (similar to String format
) :
String htmlTable = """
<html>
<head></head>
<body>
<table>
<tr>
<th> ClientName </th>
<th> SyncCount </th>
</tr>
%s
</table>
</body>
</html>""".formatted(
metricsList.stream().map(metrics -> """
\n
<tr>
<td>%s</td>
<td>%s</td>
</tr>""".formatted(
metrics.getName(),
metrics.getSyncCall())
).collect(Collectors.joining())
);
In the above code the %s
in the first text block is replaced with the html for the table data rows which in turn is generated by iterating over the object list with a stream
function which map
each object to a string that gets concatenated with the ending .collect(Collectors.joining())
.
-
2\$\begingroup\$ Welcome to Code Review! Your post looks fine, just keep in mind that the original question was posted about 6 years ago and. Also, there's a comment included in French, which obviously not wrong, might be better in English for the majority of readers. Enjoy your stay! \$\endgroup\$ferada– ferada2021年03月16日 00:14:26 +00:00Commented Mar 16, 2021 at 0:14
-
1\$\begingroup\$ Thanks @ferada, I removed the comment. \$\endgroup\$Pierre C– Pierre C2021年03月16日 22:02:06 +00:00Commented Mar 16, 2021 at 22:02
I have solved this problem by creating a few trivial helper classes called TableData, TableRow and TableCell. I then use those to build a table with Java objects. Those objects have javax.xml.bind.annotation.XmlValue and such annotations in them, so that the table can easily be serialized into XML using JAXB. That XML can then be further processed into any kind of HTML by an XSLT transformation, which is surprisingly simple to do in Java.
I've got an open source project at Bitbucket that uses this method. The HtmlFormatter class takes in a TableData object and outputs an HTML string after the process I just described (using the xmlToHtml.xsl XSL file).
You can conditionally generate html and css using wffweb framework. This lets you write strongly-typed code against objects, not error-prone hard-coded strings. It gives better code readability and re-usability. And also the generated html will be in the minified form so it's an optimization in the level of a webpage, you can see the source of google.com is also minified.
For example this code:
public static void main(String[] args) {
final int totalRows = 10;
Html html = new Html(null) {
Body body = new Body(this) {
Table table = new Table(this) {
{
Style column1Style = new Style(AlignContent.CENTER);
Style column2Style = new Style(AlignItems.CENTER,
new BackgroundColor(CssColorName.AQUA.getColorName()));
for (int i = 0; i < totalRows; i++) {
final String cellContent = "td1Id" + i;
new Tr(this) {
{
Td td1 = new Td(this, column1Style) {
{
new NoTag(this, cellContent);
}
};
Td td2 = new Td(this, column2Style) {
{
new NoTag(this, cellContent);
}
};
Td td3 = new Td(this) {
{
new NoTag(this, cellContent);
}
};
}
};
}
}
};
};
};
// html.setPrependDocType(true);
System.out.println(html.toHtmlString());
}
will print below html (without formatted)
<html>
<body>
<table>
<tr>
<td style="align-content:center;">td1Id0</td>
<td style="align-items:center;background-color:Aqua;">td1Id0</td>
<td>td1Id0</td>
</tr>
<tr>
<td style="align-content:center;">td1Id1</td>
<td style="align-items:center;background-color:Aqua;">td1Id1</td>
<td>td1Id1</td>
</tr>
<tr>
<td style="align-content:center;">td1Id2</td>
<td style="align-items:center;background-color:Aqua;">td1Id2</td>
<td>td1Id2</td>
</tr>
<tr>
<td style="align-content:center;">td1Id3</td>
<td style="align-items:center;background-color:Aqua;">td1Id3</td>
<td>td1Id3</td>
</tr>
<tr>
<td style="align-content:center;">td1Id4</td>
<td style="align-items:center;background-color:Aqua;">td1Id4</td>
<td>td1Id4</td>
</tr>
<tr>
<td style="align-content:center;">td1Id5</td>
<td style="align-items:center;background-color:Aqua;">td1Id5</td>
<td>td1Id5</td>
</tr>
<tr>
<td style="align-content:center;">td1Id6</td>
<td style="align-items:center;background-color:Aqua;">td1Id6</td>
<td>td1Id6</td>
</tr>
<tr>
<td style="align-content:center;">td1Id7</td>
<td style="align-items:center;background-color:Aqua;">td1Id7</td>
<td>td1Id7</td>
</tr>
<tr>
<td style="align-content:center;">td1Id8</td>
<td style="align-items:center;background-color:Aqua;">td1Id8</td>
<td>td1Id8</td>
</tr>
<tr>
<td style="align-content:center;">td1Id9</td>
<td style="align-items:center;background-color:Aqua;">td1Id9</td>
<td>td1Id9</td>
</tr>
</table>
</body>
</html>