Repository: jtablesaw/tablesaw
Pull request #500 (2019-03-23 05:18:37)

Add a callback for creating HTML elements
Thanks for contributing.

- [X] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Added the ability for user to override html element creation functionality

## Testing

Moved existing unit test to `HtmlWriterTest` and created a new unit test there for the new functionality


lwhite1: LGTM

Patch:
diff --git a/html/src/main/java/tech/tablesaw/io/html/HtmlWriteOptions.java b/html/src/main/java/tech/tablesaw/io/html/HtmlWriteOptions.java
index 8adbdb40..28d53b2a 100644
--- a/html/src/main/java/tech/tablesaw/io/html/HtmlWriteOptions.java
+++ b/html/src/main/java/tech/tablesaw/io/html/HtmlWriteOptions.java
@@ -1,26 +1,93 @@
 package tech.tablesaw.io.html;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+
+import org.jsoup.nodes.Element;
+
+import tech.tablesaw.columns.Column;
 import tech.tablesaw.io.Destination;
 import tech.tablesaw.io.WriteOptions;
 
 public class HtmlWriteOptions extends WriteOptions {
 
+    private final ElementCreator elementCreator;
+  
     protected HtmlWriteOptions(Builder builder) {
         super(builder);
+        this.elementCreator = builder.elementCreator;
+    }
+
+    public ElementCreator elementCreator() {
+        return elementCreator;
+    }
+
+    public static Builder builder(Destination dest) {
+        return new Builder(dest);
     }
 
-    public static Builder build(Destination dest) {
+    public static Builder builder(OutputStream dest) {
         return new Builder(dest);
     }
 
+    public static Builder builder(Writer dest) {
+        return new Builder(dest);
+    }
+
+    public static Builder builder(File dest) throws IOException {
+        return new Builder(dest);
+    }
+
+    public static Builder builder(String fileName) throws IOException {
+        return builder(new File(fileName));
+    }
+
     public static class Builder extends WriteOptions.Builder {
+        private ElementCreator elementCreator
+            = (elementName, column, row) -> new Element(elementName);
+
         protected Builder(Destination dest) {
             super(dest);
         }
 
+        protected Builder(File file) throws IOException {
+            super(file);
+        }
+
+        protected Builder(Writer writer) {
+            super(writer);
+        }
+
+        protected Builder(OutputStream stream) {
+            super(stream);
+        }
+
+        public Builder elementCreator(ElementCreator elementCreator) {
+            this.elementCreator = elementCreator;
+            return this;
+        }
+        
         public HtmlWriteOptions build() {
           return new HtmlWriteOptions(this);
         }
     }
 
+    public static interface ElementCreator {
+        /**
+         * Called for each element created. Used as a hook to add classes or
+         * other attributes to the element.
+         * @param elementName element type to create. E.g. table, thead, tbody, tr, th, td
+         * @param column the column this table cell corresponds to. null if not a td or th
+         * @param row the row this table cell corresponds to. null if not a td or tr in table body
+         * @return HTML element
+         */
+        Element create(String elementName, Column<?> column, Integer row);
+
+        default Element create(String elementName) {
+            return create(elementName, null, null);
+        }
+    }
+
 }
diff --git a/html/src/main/java/tech/tablesaw/io/html/HtmlWriter.java b/html/src/main/java/tech/tablesaw/io/html/HtmlWriter.java
index 81e0b96f..542f5ae0 100644
--- a/html/src/main/java/tech/tablesaw/io/html/HtmlWriter.java
+++ b/html/src/main/java/tech/tablesaw/io/html/HtmlWriter.java
@@ -21,11 +21,14 @@ import java.io.IOException;
 import java.io.Writer;
 import java.util.List;
 
+import org.jsoup.nodes.Element;
+
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.io.DataWriter;
 import tech.tablesaw.io.Destination;
 import tech.tablesaw.io.WriterRegistry;
+import tech.tablesaw.io.html.HtmlWriteOptions.ElementCreator;
 
 public class HtmlWriter implements DataWriter<HtmlWriteOptions> {
 
@@ -41,63 +44,46 @@ public class HtmlWriter implements DataWriter<HtmlWriteOptions> {
     }
 
     public void write(Table table, HtmlWriteOptions options) throws IOException {
-        StringBuilder builder = new StringBuilder();
-        builder.append("<table>").append(System.lineSeparator());
-        builder.append(header(table.columnNames()));
-        builder.append("<tbody>").append(System.lineSeparator());
+        ElementCreator elements = options.elementCreator();
+        Element html = elements.create("table");
+        html.appendChild(header(table.columns(), elements));
+
+        Element tbody = elements.create("tbody");
+        html.appendChild(tbody);
         for (int row = 0; row < table.rowCount(); row++) {
-            builder.append(row(row, table));
+            tbody.appendChild(row(row, table, elements));
         }
-        builder.append("</tbody>").append(System.lineSeparator());
-        builder.append("</table>");
-        String str = builder.toString();
 
         Writer writer = options.destination().createWriter();
-        writer.write(str);
+        writer.write(html.toString());
         writer.flush();
     }
 
     /**
      * Returns a string containing the html output of one table row
      */
-    private static String row(int row, Table table) {
-        StringBuilder builder = new StringBuilder()
-                .append("<tr>");
-
+    private static Element row(int row, Table table, ElementCreator elements) {
+        Element tr = elements.create("tr", null, row);
         for (Column<?> col : table.columns()) {
-            builder
-                    .append("<td>")
-                    .append(String.valueOf(col.getString(row)))
-                    .append("</td>");
+          tr.appendChild(elements.create("td", col, row)
+              .append(String.valueOf(col.getString(row))));
         }
-        builder
-                .append("</tr>")
-                .append(System.lineSeparator());
-        return builder.toString();
+        return tr;
     }
 
-    private static String header(List<String> columnNames) {
-        StringBuilder builder = new StringBuilder()
-                .append("<thead>")
-                .append(System.lineSeparator())
-                .append("<tr>");
-        for (String name : columnNames) {
-            builder
-                    .append("<th>")
-                    .append(splitCamelCase(splitOnUnderscore(name)))
-                    .append("</th>");
+    private static Element header(List<Column<?>> cols, ElementCreator elements) {
+        Element thead = elements.create("thead");
+        Element tr = elements.create("tr");
+        thead.appendChild(tr);
+        for (Column<?> col : cols) {
+            tr.appendChild(elements.create("th", col, null)
+                .append(splitCamelCase(splitOnUnderscore(col.name()))));
         }
-        builder
-                .append("</tr>")
-                .append(System.lineSeparator())
-                .append("</thead>")
-                .append(System.lineSeparator());
-
-        return builder.toString();
+        return thead;
     }
 
     @Override
     public void write(Table table, Destination dest) throws IOException {
-        write(table, HtmlWriteOptions.build(dest).build());
+        write(table, HtmlWriteOptions.builder(dest).build());
     }
 }
\ No newline at end of file


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/io/DataFrameWriterTest.java b/core/src/test/java/tech/tablesaw/io/DataFrameWriterTest.java
index 1d180e91..78176f92 100644
--- a/core/src/test/java/tech/tablesaw/io/DataFrameWriterTest.java
+++ b/core/src/test/java/tech/tablesaw/io/DataFrameWriterTest.java
@@ -1,22 +1,22 @@
 package tech.tablesaw.io;
 
-import org.junit.jupiter.api.Test;
-import tech.tablesaw.api.DoubleColumn;
-import tech.tablesaw.api.Table;
+import static java.lang.Double.NaN;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.OutputStreamWriter;
 
-import static java.lang.Double.NaN;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.api.Table;
 
 public class DataFrameWriterTest {
 
     private static final String LINE_END = System.lineSeparator();
 
-    private double[] v1 = {1, 2, 3, 4, 5, NaN};
-    private double[] v2 = {1, 2, 3, 4, 5, NaN};
+    private double[] v1 = {1, 2, NaN};
+    private double[] v2 = {1, 2, NaN};
     private Table table = Table.create("t",
             DoubleColumn.create("v", v1),
             DoubleColumn.create("v2", v2)
@@ -30,9 +30,6 @@ public class DataFrameWriterTest {
         assertEquals("v,v2" + LINE_END +
                 "1.0,1.0" + LINE_END +
                 "2.0,2.0" + LINE_END +
-                "3.0,3.0" + LINE_END +
-                "4.0,4.0" + LINE_END +
-                "5.0,5.0" + LINE_END +
                 "," + LINE_END +
                 "", output);
     }
@@ -46,30 +43,7 @@ public class DataFrameWriterTest {
         assertEquals("v,v2" + LINE_END +
                 "1.0,1.0" + LINE_END +
                 "2.0,2.0" + LINE_END +
-                "3.0,3.0" + LINE_END +
-                "4.0,4.0" + LINE_END +
-                "5.0,5.0" + LINE_END +
                 "," + LINE_END +
                 "", output);
     }
-
-    @Test
-    public void html() throws IOException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        table.write().toStream(baos, "html");
-        String output = baos.toString();
-        assertEquals("<table>" + LINE_END +
-        	"<thead>" + LINE_END +
-                "<tr><th>v</th><th>v 2</th></tr>" + LINE_END +
-                "</thead>" + LINE_END +
-                "<tbody>" + LINE_END +
-                "<tr><td>1.0</td><td>1.0</td></tr>" + LINE_END +
-                "<tr><td>2.0</td><td>2.0</td></tr>" + LINE_END +
-                "<tr><td>3.0</td><td>3.0</td></tr>" + LINE_END +
-                "<tr><td>4.0</td><td>4.0</td></tr>" + LINE_END +
-                "<tr><td>5.0</td><td>5.0</td></tr>" + LINE_END +
-                "<tr><td></td><td></td></tr>" + LINE_END +
-                "</tbody>" + LINE_END +
-                "</table>", output);
-    }
 }
\ No newline at end of file
diff --git a/core/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java b/core/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java
index c2004856..3f9fc698 100644
--- a/core/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java
+++ b/core/src/test/java/tech/tablesaw/io/html/HtmlWriterTest.java
@@ -14,34 +14,92 @@
 
 package tech.tablesaw.io.html;
 
+import static java.lang.Double.NaN;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.StringWriter;
+import java.io.OutputStream;
 
-import org.junit.jupiter.api.BeforeEach;
+import org.jsoup.nodes.Element;
 import org.junit.jupiter.api.Test;
 
-import tech.tablesaw.aggregate.AggregateFunctions;
-import tech.tablesaw.api.StringColumn;
+import tech.tablesaw.api.DoubleColumn;
 import tech.tablesaw.api.Table;
-import tech.tablesaw.io.Destination;
-import tech.tablesaw.io.csv.CsvReadOptions;
-import tech.tablesaw.table.StandardTableSliceGroup;
-import tech.tablesaw.table.TableSliceGroup;
+import tech.tablesaw.io.html.HtmlWriteOptions.ElementCreator;
 
 public class HtmlWriterTest {
 
-    private Table table;
+    private static final String LINE_END = System.lineSeparator();
+
+    private double[] v1 = {1, 2, NaN};
+    private double[] v2 = {1, 2, NaN};
+    private Table table = Table.create("t",
+            DoubleColumn.create("v", v1),
+            DoubleColumn.create("v2", v2)
+    );
 
-    @BeforeEach
-    public void setUp() throws Exception {
-        table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+    @Test
+    public void basic() {
+        String output = table.write().toString("html");
+        assertEquals("<table>" + LINE_END +
+                " <thead>" + LINE_END +
+                "  <tr>" + LINE_END +
+                "   <th>v</th>" + LINE_END +
+                "   <th>v 2</th>" + LINE_END +
+                "  </tr>" + LINE_END +
+                " </thead>" + LINE_END +
+                " <tbody>" + LINE_END +
+                "  <tr>" + LINE_END +
+                "   <td>1.0</td>" + LINE_END +
+                "   <td>1.0</td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                "  <tr>" + LINE_END +
+                "   <td>2.0</td>" + LINE_END +
+                "   <td>2.0</td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                "  <tr>" + LINE_END +
+                "   <td></td>" + LINE_END +
+                "   <td></td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                " </tbody>" + LINE_END +
+                "</table>", output);
     }
 
     @Test
-    public void testWrite() throws IOException {
-        StringColumn byColumn = table.stringColumn("who");
-        TableSliceGroup group = StandardTableSliceGroup.create(table, byColumn);
-        Table result = group.aggregate("approval", AggregateFunctions.mean);
-        new HtmlWriter().write(result, new Destination(new StringWriter()));
+    public void alternatingRows() throws IOException {
+        OutputStream baos = new ByteArrayOutputStream();
+        ElementCreator elementCreator = (elementName, column, row) -> {
+            Element element = new Element(elementName);
+            if (elementName.equals("tr") && row != null) {
+                return element.addClass(row % 2 == 0 ? "even" : "odd");
+            }
+            return element;
+        };
+        table.write().usingOptions(HtmlWriteOptions.builder(baos).elementCreator(elementCreator).build());
+        String output = baos.toString();
+        assertEquals("<table>" + LINE_END +
+                " <thead>" + LINE_END +
+                "  <tr>" + LINE_END +
+                "   <th>v</th>" + LINE_END +
+                "   <th>v 2</th>" + LINE_END +
+                "  </tr>" + LINE_END +
+                " </thead>" + LINE_END +
+                " <tbody>" + LINE_END +
+                "  <tr class=\"even\">" + LINE_END +
+                "   <td>1.0</td>" + LINE_END +
+                "   <td>1.0</td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                "  <tr class=\"odd\">" + LINE_END +
+                "   <td>2.0</td>" + LINE_END +
+                "   <td>2.0</td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                "  <tr class=\"even\">" + LINE_END +
+                "   <td></td>" + LINE_END +
+                "   <td></td>" + LINE_END +
+                "  </tr>" + LINE_END +
+                " </tbody>" + LINE_END +
+                "</table>", output);
     }
+
 }