13
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\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
    Commented 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\$ Commented Feb 1, 2016 at 17:18

6 Answers 6

21
\$\begingroup\$
  1. How about extracting the style=""s into the head?

    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>");
    
  2. 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.

\$\endgroup\$
4
  • \$\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
    Commented 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\$ Commented Sep 19, 2014 at 6:57
  • \$\begingroup\$ Thanks @MadaraUchiha, important clarification, I forgot about that. \$\endgroup\$
    – thriqon
    Commented 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\$ Commented Sep 19, 2014 at 13:03
4
\$\begingroup\$

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");
        }
    }
}
\$\endgroup\$
5
  • \$\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
    Commented 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\$ Commented Apr 22, 2018 at 15:21
  • \$\begingroup\$ @JohnSmith The problem is in fact on the PrintStream use. We can speedup it with a new BufferedOutputStream(out, 1024*128) interleaved between the PrintStream and the OutputStream. But we can increase the performance even more and achieve a performance of less than 1 second for 100k entries, if we avoid the PrintStream at all and use for example a DataOutputStream. I will fix the internal printer implementation of HtmlFlow 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\$ Commented 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
    Commented 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 a StringBuilder rather than PrintStream and effectively shows much better performance. Now we have some alerts on README about the use of PrintStream. \$\endgroup\$ Commented Aug 6, 2018 at 12:10
4
\$\begingroup\$

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.

\$\endgroup\$
0
3
\$\begingroup\$

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()).

\$\endgroup\$
2
  • 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
    Commented Mar 16, 2021 at 0:14
  • 1
    \$\begingroup\$ Thanks @ferada, I removed the comment. \$\endgroup\$
    – Pierre C
    Commented Mar 16, 2021 at 22:02
1
\$\begingroup\$

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).

\$\endgroup\$
1
\$\begingroup\$

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>
\$\endgroup\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.