11
\$\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\$
  • \$\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\$ – Traubenfuchs Sep 19 '14 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 Feb 1 '16 at 17:18
20
\$\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\$
  • \$\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 Sep 19 '14 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 Uchiha Sep 19 '14 at 6:57
  • \$\begingroup\$ Thanks @MadaraUchiha, important clarification, I forgot about that. \$\endgroup\$ – thriqon Sep 19 '14 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 Sep 19 '14 at 13:03
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\$
3
\$\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\$
  • \$\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 Apr 14 '18 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 Apr 22 '18 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\$ – Miguel Gamboa Apr 26 '18 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 Aug 1 '18 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\$ – Miguel Gamboa Aug 6 '18 at 12:10
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\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.