FREEMARKER-144 Proof Of Concept for providing DataFrames (#14)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b610dbf..045191a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ## 0.1.0-SNAPSHOT
 
 ### Added
+* [FREEMARKER-144] Proof Of Concept for providing DataFrames
 * [FREEMARKER-142] Support Transformation Of Directories
 * [FREEMARKER-139] freemarker-cli: Provide GsonTool to align with Maven plugin
 * Environment variables can bes passed as `DataSource`
@@ -38,4 +39,5 @@
 [FREEMARKER-136]: https://issues.apache.org/jira/browse/FREEMARKER-136
 [FREEMARKER-138]: https://issues.apache.org/jira/browse/FREEMARKER-138
 [FREEMARKER-139]: https://issues.apache.org/jira/browse/FREEMARKER-139
-[FREEMARKER-142]: https://issues.apache.org/jira/browse/FREEMARKER-142
\ No newline at end of file
+[FREEMARKER-142]: https://issues.apache.org/jira/browse/FREEMARKER-142
+[FREEMARKER-144]: https://issues.apache.org/jira/browse/FREEMARKER-144
\ No newline at end of file
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/table/Table.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/table/Table.java
new file mode 100644
index 0000000..1abf21b
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/table/Table.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.base.table;
+
+import org.apache.freemarker.generator.base.util.ListUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Simple table model filled from maps or rows representing tabular data.
+ */
+public class Table {
+
+    /** List of column names */
+    private final List<String> columnNames;
+
+    /** Column types derived from tabular data */
+    private final List<Class<?>> columnTypes;
+
+    /** Table data as rows */
+    private final List<List<Object>> values;
+
+    /** Map column names to column index */
+    private final Map<String, Integer> columnMap;
+
+    private Table() {
+        this.columnNames = emptyList();
+        this.columnTypes = emptyList();
+        this.values = emptyList();
+        this.columnMap = new HashMap<>();
+    }
+
+    private Table(Collection<String> columnNames, Collection<Class<?>> columnTypes, List<List<Object>> columnValuesList) {
+        this.columnNames = new ArrayList<>(requireNonNull(columnNames));
+        this.columnTypes = new ArrayList<>(requireNonNull(columnTypes));
+        this.values = ListUtils.transpose(requireNonNull(columnValuesList));
+        this.columnMap = columnMap(this.columnNames);
+    }
+
+    public List<String> getColumnNames() {
+        return columnNames;
+    }
+
+    public List<Class<?>> getColumnTypes() {
+        return columnTypes;
+    }
+
+    public List<List<Object>> getValues() {
+        return values;
+    }
+
+    public int getNrOfColumns() {
+        return columnNames.isEmpty() ? values.isEmpty() ? 0 : values.get(0).size() : columnNames.size();
+    }
+
+    public int size() {
+        return values.size();
+    }
+
+    public boolean isEmpty() {
+        return values.isEmpty();
+    }
+
+    public List<Object> getRow(int row) {
+        return values.get(row);
+    }
+
+    public Object get(int row, int column) {
+        return values.get(row).get(column);
+    }
+
+    public Object get(int row, String column) {
+        return values.get(row).get(columnMap.get(column));
+    }
+
+    public boolean hasColumnHeaderRow() {
+        return !columnNames.isEmpty();
+    }
+
+    /**
+     * Create a table from a list of maps. Non-tabular data is supported,
+     * i.e. not all maps contains all possible keys.
+     *
+     * @param maps list of maps
+     * @return table
+     */
+    public static Table fromMaps(Collection<Map<String, Object>> maps) {
+        if (maps == null || maps.isEmpty()) {
+            return new Table();
+        }
+
+        final List<String> columnNames = columnNames(maps);
+        final List<List<Object>> columnValuesList = columnValuesList(maps, columnNames);
+        final List<Class<?>> columnTypes = columnTypes(columnValuesList);
+
+        return new Table(
+                columnNames,
+                columnTypes,
+                columnValuesList);
+    }
+
+    /**
+     * Create a table from a list of rows representing tabular data.
+     *
+     * @param rows row values
+     * @return table
+     */
+    public static Table fromRows(List<List<Object>> rows) {
+        requireNonNull(rows, "rows is null");
+
+        final List<List<Object>> columnValuesList = ListUtils.transpose(rows);
+        final List<Class<?>> columnTypes = columnTypes(columnValuesList);
+
+        return new Table(
+                new ArrayList<>(),
+                columnTypes,
+                columnValuesList);
+    }
+
+    /**
+     * Create a table from a list of rows representing tabular data
+     * where the first row may consists of column headers.
+     *
+     * @param rows                      row values
+     * @param withFirstRowAsColumnNames column names as first row?
+     * @return table
+     */
+    public static Table fromRows(List<List<Object>> rows, boolean withFirstRowAsColumnNames) {
+        if (ListUtils.isNullOrEmpty(rows) && withFirstRowAsColumnNames) {
+            throw new IllegalArgumentException("Header columns expected but list is empty");
+        }
+
+        if (withFirstRowAsColumnNames) {
+            final List<String> columnNames = columnNames(rows.get(0));
+            final List<List<Object>> table = rows.subList(1, rows.size());
+            return fromRows(columnNames, table);
+        } else {
+            return fromRows(rows);
+        }
+    }
+
+    /**
+     * Create a table from column names and row values.
+     *
+     * @param columnNames list of column names
+     * @param rows        row values as rows
+     * @return table
+     */
+    public static Table fromRows(Collection<String> columnNames, List<List<Object>> rows) {
+        requireNonNull(columnNames, "columnNames is null");
+        requireNonNull(rows, "rows is null");
+
+        final List<List<Object>> columnValuesList = ListUtils.transpose(rows);
+        final List<Class<?>> columnTypes = columnTypes(columnValuesList);
+
+        return new Table(
+                new ArrayList<>(columnNames),
+                columnTypes,
+                columnValuesList);
+    }
+
+    /**
+     * Determine column names based on all available keys of the maps.
+     *
+     * @param maps list of maps
+     * @return column names
+     */
+    private static List<String> columnNames(Collection<Map<String, Object>> maps) {
+        return maps.stream()
+                .map(Map::keySet)
+                .flatMap(Collection::stream)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Determine column names based on a single list.
+     *
+     * @param list list of column names
+     * @return column names
+     */
+    private static List<String> columnNames(List<Object> list) {
+        return list.stream()
+                .map(Object::toString)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Determine all column values.
+     *
+     * @param maps        list of maps
+     * @param columnNames column names
+     * @return list of column values
+     */
+    private static List<List<Object>> columnValuesList(Collection<Map<String, Object>> maps, List<String> columnNames) {
+        return columnNames.stream()
+                .map(columnName -> columnValues(maps, columnName))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Determine the values of a single column.
+     *
+     * @param maps       list of maps
+     * @param columnName column name
+     * @return values of the given column
+     */
+    private static List<Object> columnValues(Collection<Map<String, Object>> maps, String columnName) {
+        return maps.stream()
+                .map(map -> map.getOrDefault(columnName, null))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Determine the column types based on the first non-null values.
+     *
+     * @param columnValuesList list of column values
+     * @return classes of the first non-null value
+     */
+    private static List<Class<?>> columnTypes(List<List<Object>> columnValuesList) {
+        return columnValuesList.stream()
+                .map(Table::columnType)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Determine the column type based on the first non-null value.
+     *
+     * @param columnValues column values
+     * @return class of the first non-null value
+     */
+    private static Class<?> columnType(List<Object> columnValues) {
+        return ListUtils.coalesce(columnValues).getClass();
+    }
+
+    private static Map<String, Integer> columnMap(List<String> columnNames) {
+        final Map<String, Integer> result = new HashMap<>();
+        for (int i = 0; i < columnNames.size(); i++) {
+            result.put(columnNames.get(i), i);
+        }
+        return result;
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
index 37100aa..fe62017 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
@@ -13,7 +13,8 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */package org.apache.freemarker.generator.base.template;
+ */
+package org.apache.freemarker.generator.base.template;
 
 import org.apache.freemarker.generator.base.file.RecursiveFileSupplier;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
@@ -22,8 +23,6 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.ArrayList;
@@ -73,7 +72,7 @@
 
         final List<TemplateTransformation> result = new ArrayList<>();
 
-        if (template != null) {
+        if (hasInteractiveTemplate()) {
             final File outputFile = outputs.isEmpty() ? null : outputs.get(0);
             result.add(resolveInteractiveTemplate(outputFile));
         } else {
@@ -249,18 +248,8 @@
         return excludes.isEmpty() ? null : excludes.get(0);
     }
 
-    private Writer writer(String outputFile, String outputEncoding) {
-        try {
-            if (writer != null) {
-                return writer;
-            } else if (!StringUtils.isEmpty(outputFile)) {
-                return new BufferedWriter(new FileWriter(outputFile));
-            } else {
-                return new BufferedWriter(new OutputStreamWriter(System.out, outputEncoding));
-            }
-        } catch (IOException e) {
-            throw new RuntimeException("Unable to create writer", e);
-        }
+    private boolean hasInteractiveTemplate() {
+        return template != null;
     }
 
     private static File getTemplateOutputFile(File templateDirectory, File templateFile, File outputDirectory) {
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
new file mode 100644
index 0000000..e04d16a
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.base.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class ListUtils {
+
+    public static <T> boolean isNullOrEmpty(final List<T> list) {
+        return list == null || list.isEmpty();
+    }
+
+    /**
+     * Transposes the given tabular data, swapping rows with columns.
+     *
+     * @param <T>   the type of the table
+     * @param table the table
+     * @return the transposed table
+     * @throws NullPointerException if the given table is {@code null}
+     */
+    public static <T> List<List<T>> transpose(final List<List<T>> table) {
+        if (isNullOrEmpty(table)) {
+            return new ArrayList<>();
+        }
+
+        final List<List<T>> result = new ArrayList<>();
+        for (int i = 0; i < table.get(0).size(); i++) {
+            final List<T> col = new ArrayList<>();
+            for (List<T> row : table) {
+                col.add(row.get(i));
+            }
+            result.add(col);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the first non-null value of the list.
+     *
+     * @param list array
+     * @param <T>  the type of the array
+     * @return copied array
+     */
+    public static <T> T coalesce(List<T> list) {
+        return list.stream().filter(Objects::nonNull).findFirst().orElseGet(() -> null);
+    }
+
+    /**
+     * Copy an array to another array while casting to <code>R</code>.
+     *
+     * @param array array to copy
+     * @param <T>   the source type of the array
+     * @param <R>   the target type of the array
+     * @return copied array
+     */
+    @SuppressWarnings("unchecked")
+    public static <T, R> List<R> copy(final List<T> array) {
+        final List<R> result = new ArrayList<>();
+        for (int i = 0; i < array.size(); i++) {
+            result.set(i, (R) array.get(i));
+        }
+        return result;
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
new file mode 100644
index 0000000..60daa80
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.base.util;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class MapBuilder {
+
+    public static Map<String, Object> toLinkedMap(Object... data) {
+
+        final HashMap<String, Object> map = new LinkedHashMap<>();
+
+        if (data.length % 2 != 0) {
+            throw new IllegalArgumentException("Odd number of arguments");
+        }
+
+        String currKey = null;
+        int step = -1;
+
+        for (Object value : data) {
+            step++;
+            switch (step % 2) {
+                case 0:
+                    if (value == null) {
+                        throw new IllegalArgumentException("Null key value");
+                    }
+                    currKey = value.toString();
+                    continue;
+                case 1:
+                    map.put(currKey, value);
+                    break;
+            }
+        }
+
+        return map;
+    }
+}
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/table/TableTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/table/TableTest.java
new file mode 100644
index 0000000..bd44efc
--- /dev/null
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/table/TableTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.table;
+
+import org.apache.freemarker.generator.base.table.Table;
+import org.apache.freemarker.generator.base.util.MapBuilder;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNull;
+
+public class TableTest {
+
+    public final List<Map<String, Object>> booksMaps = Arrays.asList(
+            MapBuilder.toLinkedMap(
+                    "Book ID", "1",
+                    "Book Name", "Computer Architecture",
+                    "Category", "Computers",
+                    "In Stock", true,
+                    "Price", 125.60),
+            MapBuilder.toLinkedMap(
+                    "Book ID", "2",
+                    "Book Name", "Asp.Net 4 Blue Book",
+                    "Category", "Programming",
+                    "In Stock", null,
+                    "Price", 56),
+            MapBuilder.toLinkedMap(
+                    "Book ID", "3",
+                    "Book Name", "Popular Science",
+                    "Category", "Science",
+                    "Price", 210.40)
+    );
+
+    public final List<String> booksHeader = Arrays.asList(
+            "Book ID",
+            "Book Name",
+            "Category",
+            "In Stock",
+            "Price");
+
+
+    public final List<List<Object>> booksList = Arrays.asList(
+            Arrays.asList("1", "Computer Architecture", "Computers", true, 125.60),
+            Arrays.asList("2", "Asp.Net 4 Blue Book", "Programming", null, 56),
+            Arrays.asList("3", "Popular Science", "Science", null, 210.40)
+    );
+
+    public final List<List<Object>> booksListWithHeaders = Arrays.asList(
+            Arrays.asList("Book ID", "Book Name", "Category", "In Stock", "Price"),
+            Arrays.asList("1", "Computer Architecture", "Computers", true, 125.60),
+            Arrays.asList("2", "Asp.Net 4 Blue Book", "Programming", null, 56),
+            Arrays.asList("3", "Popular Science", "Science", null, 210.40)
+    );
+
+    @Test
+    public void shouldConvertFromMaps() {
+        final Table table = Table.fromMaps(booksMaps);
+
+        validateBooks(table);
+        assertEquals(booksHeader, table.getColumnNames());
+    }
+
+    @Test
+    public void shouldConvertFromEmptyMaps() {
+        final Table table = Table.fromMaps(new ArrayList<>());
+
+        assertEquals(0, table.getNrOfColumns());
+        assertEquals(0, table.size());
+    }
+
+    @Test
+    public void shouldConvertFromNullMap() {
+        final Table table = Table.fromMaps(null);
+
+        assertEquals(0, table.getNrOfColumns());
+        assertEquals(0, table.size());
+    }
+
+    @Test
+    public void shouldConvertFromListsWithExplicitHeaders() {
+        final Table table = Table.fromRows(booksHeader, booksList);
+
+        validateBooks(table);
+        assertEquals(booksHeader, table.getColumnNames());
+    }
+
+    @Test
+    public void shouldConvertFromListsWithImplicitHeaders() {
+        final Table table = Table.fromRows(booksListWithHeaders, true);
+
+        validateBooks(table);
+        assertEquals(booksHeader, table.getColumnNames());
+    }
+
+    @Test
+    public void shouldConvertFromListsWithEmptyHeaders() {
+        final Table table = Table.fromRows(booksList);
+
+        validateBooks(table);
+    }
+
+    public void validateBooks(Table table) {
+        assertEquals(5, table.getNrOfColumns());
+        assertEquals(3, table.size());
+        // Book Id
+        assertEquals("1", table.get(0, 0));
+        assertEquals("2", table.get(1, 0));
+        assertEquals("3", table.get(2, 0));
+        // Book Name
+        assertEquals("Computer Architecture", table.get(0, 1));
+        assertEquals("Asp.Net 4 Blue Book", table.get(1, 1));
+        assertEquals("Popular Science", table.get(2, 1));
+        // Category
+        assertEquals("Computers", table.get(0, 2));
+        assertEquals("Programming", table.get(1, 2));
+        assertEquals("Science", table.get(2, 2));
+        // In Stock
+        assertEquals(true, table.get(0, 3));
+        assertNull(table.get(1, 3));
+        assertNull(table.get(2, 3));
+        // Price
+        assertEquals(125.60, table.get(0, 4));
+        assertEquals(56, table.get(1, 4));
+        assertEquals(210.40, table.get(2, 4));
+    }
+}
diff --git a/freemarker-generator-cli/pom.xml b/freemarker-generator-cli/pom.xml
index 8b74a0d..892473d 100644
--- a/freemarker-generator-cli/pom.xml
+++ b/freemarker-generator-cli/pom.xml
@@ -146,7 +146,7 @@
                         <exclude>src/main/resources/patterns/*</exclude>
                         <exclude>site/sample/*/**</exclude>
                         <exclude>src/test/data/encoding/utf8.txt</exclude>
-                        <exclude>src/test/data/json/environments.json</exclude>
+                        <exclude>src/test/data/json/*/**</exclude>
                         <exclude>src/test/data/yaml/environments.yaml</exclude>
                     </excludes>
                 </configuration>
diff --git a/freemarker-generator-cli/run-samples.sh b/freemarker-generator-cli/run-samples.sh
index d760145..ec2733a 100755
--- a/freemarker-generator-cli/run-samples.sh
+++ b/freemarker-generator-cli/run-samples.sh
@@ -51,6 +51,7 @@
 $FREEMARKER_CMD -i '${JsoupTool.parse(DataSources.first).select("a")[0]}' site/sample/html/dependencies.html > target/out/interactive-html.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -i '${GsonTool.toJson(YamlTool.parse(DataSources.get(0)))}' site/sample/yaml/swagger-spec.yaml > target/out/interactive-swagger.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -i '${YamlTool.toYaml(GsonTool.parse(DataSources.get(0)))}' site/sample/json/swagger-spec.json > target/out/interactive-swagger.yaml || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${DataFrameTool.print(DataFrameTool.fromMaps(GsonTool.parse(DataSources.get(0))))}' site/sample/json/github-users.json > target/out/interactive-dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
 # CSV
@@ -101,6 +102,13 @@
 fi
 
 #############################################################################
+# DataFrame
+#############################################################################
+
+echo "templates/dataframe/example.ftl"
+$FREEMARKER_CMD -DCSV_TOOL_DELIMITER=SEMICOLON -DCSV_TOOL_HEADERS=true -t templates/dataframe/example.ftl site/sample/csv/dataframe.csv > target/out/dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # Grok
 #############################################################################
 
@@ -111,6 +119,9 @@
 # Excel
 #############################################################################
 
+echo "templates/excel/dataframe/transform.ftl"
+$FREEMARKER_CMD -t templates/excel/dataframe/transform.ftl site/sample/excel/test.xls > target/out/test.xls.dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
 echo "templates/excel/html/transform.ftl"
 $FREEMARKER_CMD -t templates/excel/html/transform.ftl site/sample/excel/test.xls > target/out/test.xls.html || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -t templates/excel/html/transform.ftl site/sample/excel/test.xlsx > target/out/test.xslx.html || { echo >&2 "Test failed.  Aborting."; exit 1; }
diff --git a/freemarker-generator-cli/site/sample/csv/dataframe.csv b/freemarker-generator-cli/site/sample/csv/dataframe.csv
new file mode 100644
index 0000000..b841ef9
--- /dev/null
+++ b/freemarker-generator-cli/site/sample/csv/dataframe.csv
@@ -0,0 +1,10 @@
+name;age;country
+Schmitt;24;Germany
+Parker;45;USA
+Meier;20;Germany
+Schmitt;30;France
+Peter;44;Germany
+Meier;24;Germany
+Green;33;UK
+Schmitt;30;Germany
+Meier;30;Germany
\ No newline at end of file
diff --git a/freemarker-generator-cli/site/template/application.properties b/freemarker-generator-cli/site/template/application.properties
index d5f2114..fe83a14 100644
--- a/freemarker-generator-cli/site/template/application.properties
+++ b/freemarker-generator-cli/site/template/application.properties
@@ -15,5 +15,5 @@
 under the License.
 -->
 # == application.properties ==================================================
-server.name=${NGINX_HOSTNAME!"somehost"}
+server.name=${NGINX_HOSTNAME!"127.0.0.1"}
 server.logs=${NGINX_LOGS!"/var/log/nginx"}
diff --git a/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl b/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl
index ac57019..7cb9f3d 100644
--- a/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl
+++ b/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl
@@ -14,10 +14,10 @@
   specific language governing permissions and limitations
   under the License.
 -->
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen ${NGINX_PORT!"80"};
-  server_name ${NGINX_HOSTNAME!"somehost"};
+  server_name ${NGINX_HOSTNAME!"127.0.0.1"};
 
   root ${NGINX_WEBROOT!"/usr/share/nginx/www"};
   index index.htm;
diff --git a/freemarker-generator-cli/src/main/config/freemarker-cli.properties b/freemarker-generator-cli/src/main/config/freemarker-cli.properties
index fa08255..bd9c917 100644
--- a/freemarker-generator-cli/src/main/config/freemarker-cli.properties
+++ b/freemarker-generator-cli/src/main/config/freemarker-cli.properties
@@ -25,15 +25,16 @@
 # Configure FreeMarker Tools (name -> implementation class)
 #############################################################################
 freemarker.tools.CSVTool=org.apache.freemarker.generator.tools.commonscsv.CommonsCSVTool
-freemarker.tools.ExecTool=org.apache.freemarker.generator.tools.commonsexec.CommonsExecTool
+freemarker.tools.DataFrameTool=org.apache.freemarker.generator.tools.dataframe.DataFrameTool
 freemarker.tools.ExcelTool=org.apache.freemarker.generator.tools.excel.ExcelTool
+freemarker.tools.ExecTool=org.apache.freemarker.generator.tools.commonsexec.CommonsExecTool
 freemarker.tools.FreeMarkerTool=org.apache.freemarker.generator.tools.freemarker.FreeMarkerTool
 freemarker.tools.GrokTool=org.apache.freemarker.generator.tools.grok.GrokTool
 freemarker.tools.GsonTool=org.apache.freemarker.generator.tools.gson.GsonTool
 freemarker.tools.JsonPathTool=org.apache.freemarker.generator.tools.jsonpath.JsonPathTool
 freemarker.tools.JsoupTool=org.apache.freemarker.generator.tools.jsoup.JsoupTool
 freemarker.tools.PropertiesTool=org.apache.freemarker.generator.tools.properties.PropertiesTool
-freemarker.tools.YamlTool=org.apache.freemarker.generator.tools.snakeyaml.SnakeYamlTool
 freemarker.tools.SystemTool=org.apache.freemarker.generator.tools.system.SystemTool
 freemarker.tools.UUIDTool=org.apache.freemarker.generator.tools.uuid.UUIDTool
 freemarker.tools.XmlTool=org.apache.freemarker.generator.tools.xml.XmlTool
+freemarker.tools.YamlTool=org.apache.freemarker.generator.tools.snakeyaml.SnakeYamlTool
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
index d8e3491..79d94b0 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
@@ -30,13 +30,14 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toMap;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_APPLICATION_JSON;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_PLAIN;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_YAML;
@@ -62,12 +63,12 @@
     public Map<String, Object> get() {
         return sources.stream()
                 .filter(StringUtils::isNotEmpty)
-                .map(this::toDataModel)
+                .map(DataModelSupplier::toDataModel)
                 .flatMap(map -> map.entrySet().stream())
-                .collect(toMap(Entry::getKey, Entry::getValue));
+                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
     }
 
-    protected Map<String, Object> toDataModel(String source) {
+    private static Map<String, Object> toDataModel(String source) {
         final NamedUri namedUri = NamedUriStringParser.parse(source);
         final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
         final boolean isExplodedDataModel = !namedUri.hasName();
@@ -84,19 +85,19 @@
         }
     }
 
-    private Map<String, Object> fromJson(DataSource dataSource, boolean isExplodedDataModel) {
+    private static Map<String, Object> fromJson(DataSource dataSource, boolean isExplodedDataModel) {
         final GsonTool gsonTool = new GsonTool();
-        final Map<String, Object> map = gsonTool.parse(dataSource);
-        return fromMap(dataSource.getName(), map, isExplodedDataModel);
+        final Object json = gsonTool.parse(dataSource);
+        return toMap(dataSource.getName(), json, isExplodedDataModel);
     }
 
-    private Map<String, Object> fromYaml(DataSource dataSource, boolean isExplodedDataModel) {
+    private static Map<String, Object> fromYaml(DataSource dataSource, boolean isExplodedDataModel) {
         final SnakeYamlTool snakeYamlTool = new SnakeYamlTool();
-        final Map<String, Object> map = snakeYamlTool.parse(dataSource);
-        return fromMap(dataSource.getName(), map, isExplodedDataModel);
+        final Object yaml = snakeYamlTool.parse(dataSource);
+        return toMap(dataSource.getName(), yaml, isExplodedDataModel);
     }
 
-    private Map<String, Object> fromProperties(DataSource dataSource, boolean isExplodedDataModel) {
+    private static Map<String, Object> fromProperties(DataSource dataSource, boolean isExplodedDataModel) {
         final Map<String, Object> result = new HashMap<>();
         final URI uri = dataSource.getUri();
 
@@ -114,13 +115,27 @@
         return result;
     }
 
-    private Map<String, Object> fromMap(String name, Map<String, Object> map, boolean isExplodedDataModel) {
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> toMap(String name, Object obj, boolean isExplodedDataModel) {
         final Map<String, Object> result = new HashMap<>();
 
-        if (isExplodedDataModel) {
-            map.forEach(result::put);
-        } else {
-            result.put(name, map);
+        if (obj instanceof Map) {
+            final Map<String, Object> map = (Map<String, Object>) obj;
+            if (isExplodedDataModel) {
+                map.forEach(result::put);
+            } else {
+                result.put(name, map);
+            }
+        } else if (obj instanceof List) {
+            final List<Object> list = (List<Object>) obj;
+            if (isExplodedDataModel) {
+                for (Object entry : list) {
+                    final Map<String, Object> map = (Map<String, Object>) entry;
+                    map.forEach(result::put);
+                }
+            } else {
+                result.put(name, list);
+            }
         }
 
         return result;
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
index 2c220b2..7151600 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
@@ -68,14 +68,14 @@
     }
 
     public static TemplateTransformationsSupplier templateTransformationsSupplier(Settings settings) {
-        return (() -> TemplateTransformationsBuilder.builder()
+        return () -> TemplateTransformationsBuilder.builder()
                 .setTemplate("interactive", settings.getInteractiveTemplate())
                 .addSources(settings.getTemplates())
                 .addInclude(settings.getTemplateFileIncludePattern())
                 .addExclude(settings.getTemplateFileExcludePattern())
                 .addOutput(settings.getOutput())
                 .setWriter(settings.getWriter())
-                .build());
+                .build();
     }
 
     public static PropertiesSupplier propertiesSupplier(String fileName) {
diff --git a/freemarker-generator-cli/src/main/resources/freemarker-cli.properties b/freemarker-generator-cli/src/main/resources/freemarker-cli.properties
index 0a0542a..bd9c917 100644
--- a/freemarker-generator-cli/src/main/resources/freemarker-cli.properties
+++ b/freemarker-generator-cli/src/main/resources/freemarker-cli.properties
@@ -25,6 +25,7 @@
 # Configure FreeMarker Tools (name -> implementation class)
 #############################################################################
 freemarker.tools.CSVTool=org.apache.freemarker.generator.tools.commonscsv.CommonsCSVTool
+freemarker.tools.DataFrameTool=org.apache.freemarker.generator.tools.dataframe.DataFrameTool
 freemarker.tools.ExcelTool=org.apache.freemarker.generator.tools.excel.ExcelTool
 freemarker.tools.ExecTool=org.apache.freemarker.generator.tools.commonsexec.CommonsExecTool
 freemarker.tools.FreeMarkerTool=org.apache.freemarker.generator.tools.freemarker.FreeMarkerTool
diff --git a/freemarker-generator-cli/src/main/scripts/run-samples.sh b/freemarker-generator-cli/src/main/scripts/run-samples.sh
index c005f09..780a702 100755
--- a/freemarker-generator-cli/src/main/scripts/run-samples.sh
+++ b/freemarker-generator-cli/src/main/scripts/run-samples.sh
@@ -51,6 +51,7 @@
 $FREEMARKER_CMD -i '${JsoupTool.parse(DataSources.first).select("a")[0]}' site/sample/html/dependencies.html > target/out/interactive-html.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -i '${GsonTool.toJson(YamlTool.parse(DataSources.get(0)))}' site/sample/yaml/swagger-spec.yaml > target/out/interactive-swagger.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -i '${YamlTool.toYaml(GsonTool.parse(DataSources.get(0)))}' site/sample/json/swagger-spec.json > target/out/interactive-swagger.yaml || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${DataFrameTool.print(DataFrameTool.fromMaps(GsonTool.parse(DataSources.get(0))))}' site/sample/json/github-users.json > target/out/interactive-dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
 # CSV
@@ -101,6 +102,13 @@
 fi
 
 #############################################################################
+# DataFrame
+#############################################################################
+
+echo "templates/dataframe/example.ftl"
+$FREEMARKER_CMD -DCSV_TOOL_DELIMITER=SEMICOLON -DCSV_TOOL_HEADERS=true -t templates/dataframe/example.ftl site/sample/csv/dataframe.csv > target/out/dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # Grok
 #############################################################################
 
@@ -111,6 +119,9 @@
 # Excel
 #############################################################################
 
+echo "templates/excel/dataframe/transform.ftl"
+$FREEMARKER_CMD -t templates/excel/dataframe/transform.ftl site/sample/excel/test.xls > target/out/test.xls.dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
 echo "templates/excel/html/transform.ftl"
 $FREEMARKER_CMD -t templates/excel/html/transform.ftl site/sample/excel/test.xls > target/out/test.xls.html || { echo >&2 "Test failed.  Aborting."; exit 1; }
 $FREEMARKER_CMD -t templates/excel/html/transform.ftl site/sample/excel/test.xlsx > target/out/test.xslx.html || { echo >&2 "Test failed.  Aborting."; exit 1; }
diff --git a/freemarker-generator-cli/src/site/markdown/cli/tools/dataframe.md b/freemarker-generator-cli/src/site/markdown/cli/tools/dataframe.md
new file mode 100644
index 0000000..3523294
--- /dev/null
+++ b/freemarker-generator-cli/src/site/markdown/cli/tools/dataframe.md
@@ -0,0 +1,205 @@
+# DataFrameTool
+
+The `DataFrameTool` uses [nRo/DataFrame](https://github.com/nRo/DataFrame) to convert tabular data into a `DataFrame`.
+
+A `DataFrame` allows declartive filtering and transformation of tabular data, i.e. little code to write.
+
+Currently the following sources are supported
+
+* Apache Commons CSV Parser
+* JSON arrays represented as collection of maps
+* Excel sheets represented as rows
+
+## CSV Examples
+
+[nRo/DataFrame]("https://raw.githubusercontent.com/nRo/DataFrame/master/src/test/resources/users.csv") provides the following CSV file
+
+```
+┌────────────┬────────────┬────────────┐
+│#name       │#age        │#country    │
+├────────────┼────────────┼────────────┤
+│Schmitt     │24          │Germany     │
+├────────────┼────────────┼────────────┤
+│Parker      │45          │USA         │
+├────────────┼────────────┼────────────┤
+│Meier       │20          │Germany     │
+├────────────┼────────────┼────────────┤
+│Schmitt     │30          │France      │
+├────────────┼────────────┼────────────┤
+│Peter       │44          │Germany     │
+├────────────┼────────────┼────────────┤
+│Meier       │24          │Germany     │
+├────────────┼────────────┼────────────┤
+│Green       │33          │UK          │
+├────────────┼────────────┼────────────┤
+│Schmitt     │30          │Germany     │
+├────────────┼────────────┼────────────┤
+│Meier       │30          │Germany     │
+└────────────┴────────────┴────────────┘
+```
+
+and create a `DateFrame` using the following code
+
+```
+<#assign cvsFormat = CSVTool.formats["DEFAULT"].withHeader().withDelimiter(';')>
+<#assign csvParser = CSVTool.parse(DataSources.get(0), cvsFormat)>
+<#assign users = DataFrameTool.toDataFrame(csvParser)>
+```
+
+### Select By Age
+
+```
+${DataFrameTool.print(users.select("(age > 40)"))}
+```
+
+which shows 
+
+```
+┌────────────┬────────────┬────────────┐
+│#name       │#age        │#country    │
+├────────────┼────────────┼────────────┤
+│Parker      │45          │USA         │
+├────────────┼────────────┼────────────┤
+│Peter       │44          │Germany     │
+└────────────┴────────────┴────────────┘
+```
+
+### Complex Select & Sort
+
+Now we want to create a new `DataFrame` by selecting `name` and `country`
+
+```
+<#assign country = "Germany">
+${DataFrameTool.print(users
+    .select("(name == 'Schmitt' || name == 'Meier') && country == '${country}'")
+    .sort("name", DataFrameTool.sortOrder["ASCENDING"]))}
+```
+
+which shows
+
+```
+┌────────────┬────────────┬────────────┐
+│#name       │#age        │#country    │
+├────────────┼────────────┼────────────┤
+│Meier       │20          │Germany     │
+├────────────┼────────────┼────────────┤
+│Meier       │24          │Germany     │
+├────────────┼────────────┼────────────┤
+│Meier       │30          │Germany     │
+├────────────┼────────────┼────────────┤
+│Schmitt     │24          │Germany     │
+├────────────┼────────────┼────────────┤
+│Schmitt     │30          │Germany     │
+└────────────┴────────────┴────────────┘
+```
+
+### Count Column Values
+
+Let's assume we want to count the records for each `country`
+
+```
+${DataFrameTool.print(users.getColumn("country").transform(DataFrameTool.transformer["COUNT"]))}
+```
+
+returns the following `DataFrame`
+
+```
+┌────────────┬────────────┐
+│#country    │#counts     │
+├────────────┼────────────┤
+│Germany     │6           │
+├────────────┼────────────┤
+│USA         │1           │
+├────────────┼────────────┤
+│France      │1           │
+├────────────┼────────────┤
+│UK          │1           │
+└────────────┴────────────┘
+```
+
+### Group By Age And Country
+
+Let's assume that we want to group the `DataFrame` by `age` and `country`
+
+```
+${DataFrameTool.print(users.groupBy("age", "country").sort("age"))}
+``` 
+
+which results in 
+
+```
+┌────────────┬────────────┐
+│#age        │#country    │
+├────────────┼────────────┤
+│20          │Germany     │
+├────────────┼────────────┤
+│24          │Germany     │
+├────────────┼────────────┤
+│30          │France      │
+├────────────┼────────────┤
+│30          │Germany     │
+├────────────┼────────────┤
+│33          │UK          │
+├────────────┼────────────┤
+│44          │Germany     │
+├────────────┼────────────┤
+│45          │USA         │
+└────────────┴────────────┘
+```
+
+## JSON Examples
+
+Here we load a `site/samples/json/github-users.json` which represents a tabular data be 
+being parsed as a list of maps and print the JSOB as dataframe
+
+```
+./bin/freemarker-cli \
+  -i '${DataFrameTool.print(DataFrameTool.fromMaps(GsonTool.parse(DataSources.get(0))))}' \
+  site/sample/json/github-users.json
+
+┌────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
+│#login      │#id         │#avatar_ur  │#gravatar_  │#url        │#html_url   │#followers  │#following  │#gists_url  │#starred_u  │#subscript  │#organizat  │#repos_url  │#events_ur  │#received_  │#type       │#site_admi  │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│mojombo     │1.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │false       │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│defunkt     │2.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │true        │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│pjhyett     │3.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │true        │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│wycats      │4.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │false       │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│ezmobius    │5.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │false       │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│ivey        │6.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │false       │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│evanphx     │7.00000000  │https:/...  │            │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │https:/...  │User        │false       │
+└────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┘
+```
+
+## Excel Examples
+
+Let's transform an Excel Sheet to a `DataFrame` being printed using the following template
+
+```
+<#assign dataSource = DataSources.get(0)>
+<#assign workbook = ExcelTool.parse(dataSource)>
+<#list ExcelTool.getSheets(workbook) as sheet>
+    <#assign table = ExcelTool.toTable(sheet)>
+    <#assign df = DataFrameTool.fromRows(table, true)>
+    ${DataFrameTool.print(df)}<#t>
+</#list>
+```
+
+which is rendered by the following command line invocation
+
+```
+./bin/freemarker-cli -t templates/excel/dataframe/transform.ftl site/sample/excel/test.xls
+
+┌────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
+│#Text       │#Date       │#Number     │#Currency   │#Time       │#Percentag  │#Forumula   │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│Row 1       │01/01/17    │100.00      │€100.00     │10:00       │50.00%      │C2*F2       │
+├────────────┼────────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
+│Row 2       │01/01/17    │100.00      │€100.00     │10:00       │50.00%      │C3*F3       │
+└────────────┴────────────┴────────────┴────────────┴────────────┴────────────┴────────────┘
+```
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md b/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
index 3181907..7a65dc0 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
@@ -5,16 +5,45 @@
 * Transform an input directory recursively into an output directory
 * If a template has a ".ftl" extension this extension will be removed after processing
 * Only a single directory is support
-* Currently no inclusion / exclusion pattern for templates are supported
+* Currently no inclusion / exclusion patterns for templates are supported
+
+The following sample files are used
+
+* template/application.properties
+* template/nginx/nginx.conf.ftl
+
+```
+appassembler> tree site/template/
+site/template/
+|-- application.properties
+`-- nginx
+    `-- nginx.conf.ftl
+
+# == application.properties ==================================================
+server.name=${NGINX_HOSTNAME!"127.0.0.1"}
+server.logs=${NGINX_LOGS!"/var/log/nginx"}
+```
+
+```
+# == nginx-conf ==============================================================
+server {
+  listen ${NGINX_PORT!"80"};
+  server_name ${NGINX_HOSTNAME!"127.0.0.1"};
+
+  root ${NGINX_WEBROOT!"/usr/share/nginx/www"};
+  index index.htm;
+```
 
 ### Transform Template Directory To STDOUT
 
+If no output directory is provided all output is written to `stdout`
+
 ```
 bin/freemarker-cli -t site/template/
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 80;
   server_name 127.0.0.1;
@@ -26,21 +55,30 @@
 
 ### Transform Template Directory To Output Directory
 
+The transformed templates are written to an `out` directory
+
+* `nginx.conf.ftl` was changed to `nginx.conf" during the transformation
+
 ```
-bin/freemarker-cli -t site/template/ -o out; ls -l out
-total 8
--rw-r--r--  1 sgoeschl  staff  128 May 30 20:02 application.properties
-drwxr-xr-x  3 sgoeschl  staff   96 May 30 20:02 nginx
+bin/freemarker-cli -t site/template/ -o out; tree out
+out
+|-- application.properties
+`-- nginx
+    `-- nginx.conf
+
+1 directory, 2 files
 ```
 
-### Pass Parameter On The Command Line
+### Use Command Line Parameters
+
+A user-supplied parameter `NGINX_HOSTNAME` is used to render the templates
 
 ```
 bin/freemarker-cli -t site/template/ -P NGINX_HOSTNAME=localhost
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 80;
   server_name localhost;
@@ -52,13 +90,18 @@
 
 ### Use Environment Variables
 
+All environment variables can be copied to the top-level data model by providing `-m env:///`
+
+* `-m` or `-data-model` creates a data model
+* `env:///` is an URI referencing all environment variables
+
 ```
 export NGINX_PORT=8080
 bin/freemarker-cli -t site/template/ -m env:///
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 8080;
   server_name 127.0.0.1;
@@ -70,13 +113,15 @@
 
 ### Use Environment File
 
+Instead of environment variables an environment file (aka properties file) can be used
+
 ```
 echo "NGINX_PORT=8080" > nginx.env
 bin/freemarker-cli -t site/template/ -m nginx.env 
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 8080;
   server_name 127.0.0.1;
@@ -88,13 +133,15 @@
 
 ### Use JSON File
 
+Another option is passing the information as JSON file
+
 ```
 echo '{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}' > nginx.json
 bin/freemarker-cli -t site/template/ -m nginx.json 
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 8443;
   server_name localhost;
@@ -102,20 +149,42 @@
   root /usr/share/nginx/www;
   index index.htm;
 }
+```
 
+### Use YAML File
+
+Yet another option is using a YAML file
+
+```
+echo -e "- NGINX_PORT": "\"8443\"\n- NGINX_HOSTNAME": "localhost" > nginx.yaml
+bin/freemarker-cli -t site/template/ -m nginx.yaml 
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf ==============================================================
+server {
+  listen 8443;
+  server_name localhost;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
 ```
 
 ### Use Environment Variable With JSON Payload
 
+In the cloud it is common to pass JSON configuration as environment variable
+
+* `env:///NGINX_CONF` selects the `NGINX_CONF` environment variable
+* `#mimetype=application/json` defines that JSON content is parsed
+
 ```
 export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
-echo $NGINX_CONF
-{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}
 bin/freemarker-cli -t site/template/ -m env:///NGINX_CONF#mimetype=application/json
 # == application.properties ==================================================
 server.name=localhost
 server.logs=/var/log/nginx
-# == nginx-conf =============================================================
+# == nginx-conf ==============================================================
 server {
   listen 8443;
   server_name localhost;
@@ -123,4 +192,26 @@
   root /usr/share/nginx/www;
   index index.htm;
 }
-```
\ No newline at end of file
+```
+
+### Overriding Values From The Command Line
+
+For testing purpose it is useful to override certain settings
+
+```
+export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
+bin/freemarker-cli -t site/template/ -PNGINX_HOSTNAME=www.mydomain.com -m env:///NGINX_CONF#mimetype=application/json
+# == application.properties ==================================================
+server.name=www.mydomain.com
+server.logs=/var/log/nginx
+# == nginx-conf ==============================================================
+server {
+  listen 8443;
+  server_name www.mydomain.com;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
+
+Please note that this only works for "top-level" variables, i.e. mimicking enviroment variables or property files. 
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/site/markdown/index.md b/freemarker-generator-cli/src/site/markdown/index.md
index b8d50e1..2eb0dfd 100644
--- a/freemarker-generator-cli/src/site/markdown/index.md
+++ b/freemarker-generator-cli/src/site/markdown/index.md
@@ -8,6 +8,10 @@
 * [User-Supplied Parameters](cli/concepts/user-parameters.html)
 * [Transformation](cli/concepts/transformation.html)
 
+### Tools
+
+* [DataFrameTool](cli/tools/dataframe.html)
+
 ### Usage
 
 * [Transforming Directories](cli/usage/transforming-directories.html)
diff --git a/freemarker-generator-cli/src/test/data/json/list.json b/freemarker-generator-cli/src/test/data/json/list.json
new file mode 100644
index 0000000..40ea060
--- /dev/null
+++ b/freemarker-generator-cli/src/test/data/json/list.json
@@ -0,0 +1 @@
+["first", "second"]
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
index 9b2727b..95c7241 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
@@ -62,6 +62,7 @@
         assertValid(execute("-t templates/excel/md/transform.ftl site/sample/excel/test-multiple-sheets.xlsx"));
         assertValid(execute("-t templates/excel/csv/transform.ftl site/sample/excel/test-multiple-sheets.xlsx"));
         assertValid(execute("-t templates/excel/csv/custom.ftl -Pcsv.format=MYSQL site/sample/excel/test.xls"));
+        assertValid(execute("-t templates/excel/dataframe/transform.ftl site/sample/excel/test.xls"));
     }
 
     @Test
@@ -103,6 +104,11 @@
     }
 
     @Test
+    public void shouldRunDataFrameExamples() throws IOException {
+        assertValid(execute("-DCSV_TOOL_DELIMITER=SEMICOLON -DCSV_TOOL_HEADERS=true -t templates/dataframe/example.ftl site/sample/csv/dataframe.csv"));
+    }
+
+    @Test
     public void shouldRunInteractiveTemplateExamples() throws IOException {
         assertValid(execute("-i ${JsonPathTool.parse(DataSources.first).read(\"$.info.title\")} site/sample/json/swagger-spec.json"));
         assertValid(execute("-i ${XmlTool.parse(DataSources.first)[\"recipients/person[1]/name\"]} site/sample/xml/recipients.xml"));
@@ -111,12 +117,13 @@
         assertValid(execute("-i ${GsonTool.toJson(yaml)} -m yaml=site/sample/yaml/swagger-spec.yaml"));
         assertValid(execute("-i ${YamlTool.toYaml(GsonTool.parse(DataSources.get(0)))} site/sample/json/swagger-spec.json"));
         assertValid(execute("-i ${YamlTool.toYaml(json)} -m json=site/sample/json/swagger-spec.json"));
+        assertValid(execute("-i ${DataFrameTool.print(DataFrameTool.fromMaps(GsonTool.parse(DataSources.get(0))))} site/sample/json/github-users.json"));
     }
 
     @Test
     public void shouldTransformTemplateDirectory() throws IOException {
-        assertTrue(execute("-t site/template").contains("server.name=somehost"));
-        assertTrue(execute("-t site/template -PNGINX_HOSTNAME=localhost").contains("server.name=localhost"));
+        assertTrue(execute("-t site/template").contains("server.name=127.0.0.1"));
+        assertTrue(execute("-t site/template -PNGINX_HOSTNAME=my.domain.com").contains("server.name=my.domain.com"));
     }
 
     @Test
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
index 2dbaf2c..fd2c11c 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
@@ -48,8 +48,7 @@
     // private static final String CMD = "-b ./src/test -t templates/demo.ftl -m env=./site/sample/properties/user_0001/user.properties";
     // private static final String CMD = "-b ./src/test -t templates/demo.ftl -m ./site/sample/properties/user_0001/user.properties";
     // private static final String CMD = "-b ./src/test --data-model post=https://jsonplaceholder.typicode.com/posts/2 -t templates/info.ftl";
-    // private static final String CMD = "-b ./src/test -t templates/info.ftl -P name=value";
-    private static final String CMD = "-P NGINX_PORT=8080 -t ../freemarker-generator-base/src/test/template -t templates/info.ftl";
+    private static final String CMD = "-DCSV_TOOL_DELIMITER=SEMICOLON -DCSV_TOOL_HEADERS=true -b ./src/test -t templates/dataframe/example.ftl https://raw.githubusercontent.com/nRo/DataFrame/master/src/test/resources/users.csv";
 
 
     public static void main(String[] args) {
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/DataModelSupplierTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/DataModelSupplierTest.java
index def6a86..1924ccc 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/DataModelSupplierTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/DataModelSupplierTest.java
@@ -20,6 +20,7 @@
 import org.junit.Test;
 
 import java.nio.file.Paths;
+import java.util.List;
 import java.util.Map;
 
 import static java.util.Collections.singletonList;
@@ -36,7 +37,7 @@
     // === Environment Variables ===
 
     @Test
-    public void shouldResolveAllEnvironmentVariablesToTopLevelDataModel() {
+    public void shouldCopyAllEnvironmentVariablesToTopLevelDataModel() {
         final DataModelSupplier supplier = supplier("env:///");
 
         final Map<String, Object> model = supplier.get();
@@ -46,7 +47,7 @@
     }
 
     @Test
-    public void shouldResolveAllEnvironmentVariablesToDataModelVariable() {
+    public void shouldCopyAllEnvironmentVariablesToDataModelVariable() {
         final DataModelSupplier supplier = supplier("myenv=env:///");
 
         final Map<String, Object> model = supplier.get();
@@ -57,7 +58,7 @@
     }
 
     @Test
-    public void shouldResolveSingleEnvironmentVariablesToTopLevelDataModel() {
+    public void shouldCopySingleEnvironmentVariablesToTopLevelDataModel() {
         final DataModelSupplier supplier = supplier("env:///PWD");
 
         final Map<String, Object> model = supplier.get();
@@ -67,7 +68,7 @@
     }
 
     @Test
-    public void shouldResolveSingleEnvironmentVariableToDataModelVariable() {
+    public void shouldCopySingleEnvironmentVariableToDataModelVariable() {
         final DataModelSupplier supplier = supplier("mypwd=env:///PWD");
 
         final Map<String, Object> model = supplier.get();
@@ -84,7 +85,7 @@
     // === Properties ===
 
     @Test
-    public void shouldResolvePropertiesFileToTopLevelDataModel() {
+    public void shouldCopeyPropertiesFileToTopLevelDataModel() {
         final DataModelSupplier supplier = supplier("./src/test/data/properties/test.properties");
 
         final Map<String, Object> model = supplier.get();
@@ -95,7 +96,7 @@
     }
 
     @Test
-    public void shouldResolvePropertiesFileToDataModelVariable() {
+    public void shouldCopyPropertiesFileToDataModelVariable() {
         final DataModelSupplier supplier = supplier("props=./src/test/data/properties/test.properties");
 
         final Map<String, Object> model = supplier.get();
@@ -106,7 +107,7 @@
     }
 
     @Test
-    public void shouldResolvePropertiesUriToDataModelVariable() {
+    public void shouldCopyPropertiesUriToDataModelVariable() {
         final DataModelSupplier supplier = supplier("props=file:///" + PWD + "/src/test/data/properties/test.properties");
 
         final Map<String, Object> model = supplier.get();
@@ -119,7 +120,7 @@
     // === JSON ===
 
     @Test
-    public void shouldResolveJsonFileToTopLevelDataModel() {
+    public void shouldCopyJsonObjectFileToTopLevelDataModel() {
         final DataModelSupplier supplier = supplier("./src/test/data/json/environments.json");
 
         final Map<String, Object> model = supplier.get();
@@ -130,6 +131,24 @@
     }
 
     @Test
+    public void shouldCopyJsonArrayFileToDataModelVariable() {
+        final DataModelSupplier supplier = supplier("list=./src/test/data/json/list.json");
+
+        final Map<String, Object> model = supplier.get();
+
+        assertEquals(1, model.size());
+        assertEquals("first", ((List)model.get("list")).get(0));
+        assertEquals("second", ((List)model.get("list")).get(1));
+    }
+
+    @Test(expected = Exception.class)
+    public void shouldFailWhenCopyJsonArrayFileToTopLevelDataModel() {
+        supplier("./src/test/data/json/list.json").get();
+    }
+
+    // == YAML ===
+
+    @Test
     public void shouldResolveYamlFileToTopLevelDataModel() {
         final DataModelSupplier supplier = supplier("./src/test/data/yaml/environments.yaml");
 
@@ -145,6 +164,16 @@
     @Test
     @Ignore
     public void shouldResolveUrlToTopLevelDataModel() {
+        final DataModelSupplier supplier = supplier("https://jsonplaceholder.typicode.com/posts/2");
+
+        final Map<String, Object> model = supplier.get();
+
+        assertTrue(model.size() == 4);
+    }
+
+    @Test
+    @Ignore
+    public void shouldResolveUrlToDataModelVariable() {
         final DataModelSupplier supplier = supplier("post=https://jsonplaceholder.typicode.com/posts/2");
 
         final Map<String, Object> model = supplier.get();
@@ -155,16 +184,6 @@
 
     @Test
     @Ignore
-    public void shouldResolveUrlToDataModelVariable() {
-        final DataModelSupplier supplier = supplier("https://jsonplaceholder.typicode.com/posts/2");
-
-        final Map<String, Object> model = supplier.get();
-
-        assertTrue(model.size() == 4);
-    }
-
-    @Test(expected = RuntimeException.class)
-    @Ignore
     public void shouldResolveUrlToDataModelVariables() {
         supplier("https://jsonplaceholder.typicode.com/posts/does-not-exist").get();
     }
diff --git a/freemarker-generator-cli/src/test/templates/manual.ftl b/freemarker-generator-cli/src/test/templates/manual.ftl
index d33edf4..b381b05 100644
--- a/freemarker-generator-cli/src/test/templates/manual.ftl
+++ b/freemarker-generator-cli/src/test/templates/manual.ftl
@@ -17,10 +17,4 @@
 -->
 Manual Test
 ---------------------------------------------------------------------------
-<#assign smallNumber = 3.1415927>
-<#assign largeNumber = 99999.99>
-
-Small Number :  ${smallNumber}
-Large Number :  ${largeNumber}
-Date         :  ${.now?date}
-Time         :  ${.now?time}
+<#assign df=DataFrameTool.fromMaps(GsonTool.parse(DataSources.get(0)))>${DataFrameTool.print(df)}
\ No newline at end of file
diff --git a/freemarker-generator-cli/templates/dataframe/example.ftl b/freemarker-generator-cli/templates/dataframe/example.ftl
new file mode 100644
index 0000000..54d0ac2
--- /dev/null
+++ b/freemarker-generator-cli/templates/dataframe/example.ftl
@@ -0,0 +1,48 @@
+<#ftl output_format="plainText">
+<#--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<#assign csvParser = CSVTool.parse(DataSources.get(0))>
+<#assign users = DataFrameTool.fromCSVParser(csvParser)>
+
+Original Data
+=============================================================================
+${DataFrameTool.print(users)}
+
+Select By Age
+=============================================================================
+${DataFrameTool.print(users.select("(age > 40)"))}
+
+Select By Name & Country
+=============================================================================
+<#assign country = "Germany">
+${DataFrameTool.print(users
+.select("(name == 'Schmitt' || name == 'Meier') && country == '${country}'")
+.sort("name", DataFrameTool.sortOrder["ASCENDING"]))}
+
+Head of Users
+=============================================================================
+${DataFrameTool.print(users.head(2))}
+
+Count Column Values
+=============================================================================
+${DataFrameTool.print(users.getColumn("country").transform(DataFrameTool.transformer["COUNT"]))}
+
+Group By Age & Country
+=============================================================================
+${DataFrameTool.print(users.groupBy("country", "age").sort("country"))}
+
+
diff --git a/freemarker-generator-cli/templates/dataframe/html/print.ftl b/freemarker-generator-cli/templates/dataframe/html/print.ftl
new file mode 100644
index 0000000..e17d3a9
--- /dev/null
+++ b/freemarker-generator-cli/templates/dataframe/html/print.ftl
@@ -0,0 +1,54 @@
+<#ftl output_format="HTML" >
+<#--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<#assign cvsFormat = CSVTool.formats["DEFAULT"].withHeader().withDelimiter(';')>
+<#assign csvParser = CSVTool.parse(DataSources.get(0), cvsFormat)>
+<#assign dataFrame = DataFrameTool.toDataFrame(csvParser)>
+<#--------------------------------------------------------------------------->
+<!DOCTYPE html>
+<html>
+<head>
+    <title>DataFrame</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
+</head>
+<body>
+<div class="container-fluid">
+    <h1>DataFrame</h1>
+    <@writeDataFrame dataFrame/>
+</div>
+</body>
+</html>
+
+<#--------------------------------------------------------------------------->
+<#macro writeDataFrame dataFrame>
+    <table class="table table-striped">
+        <tr>
+            <#list dataFrame.columns as column>
+                <th>${column.name}</th>
+            </#list>
+        </tr>
+        <#list dataFrame.iterator() as row>
+            <tr>
+                <#list 0..row.size()-1 as idx>
+                    <td>${row.getString(idx)}</td>
+                </#list>
+            </tr>
+        </#list>
+    </table>
+</#macro>
diff --git a/freemarker-generator-cli/templates/excel/dataframe/transform.ftl b/freemarker-generator-cli/templates/excel/dataframe/transform.ftl
new file mode 100644
index 0000000..d172bec
--- /dev/null
+++ b/freemarker-generator-cli/templates/excel/dataframe/transform.ftl
@@ -0,0 +1,23 @@
+<#--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+<#assign dataSource = DataSources.get(0)>
+<#assign workbook = ExcelTool.parse(dataSource)>
+<#list ExcelTool.getSheets(workbook) as sheet>
+    <#assign table = ExcelTool.toTable(sheet)>
+    <#assign df = DataFrameTool.fromRows(table, true)>
+    ${DataFrameTool.print(df)}<#t>
+</#list>
diff --git a/freemarker-generator-tools/pom.xml b/freemarker-generator-tools/pom.xml
index 95e7d4c..57c6b59 100644
--- a/freemarker-generator-tools/pom.xml
+++ b/freemarker-generator-tools/pom.xml
@@ -54,6 +54,12 @@
             <artifactId>commons-csv</artifactId>
             <version>1.8</version>
         </dependency>
+        <!-- DataFrame -->
+        <dependency>
+            <groupId>de.unknownreality</groupId>
+            <artifactId>dataframe</artifactId>
+            <version>0.7.6</version>
+        </dependency>
         <!-- ExcelTool -->
         <dependency>
             <groupId>org.apache.poi</groupId>
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVTool.java
index 31c5d7c..296f903 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVTool.java
@@ -37,6 +37,7 @@
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static java.lang.Boolean.parseBoolean;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
@@ -44,8 +45,10 @@
 
 public class CommonsCSVTool {
 
+    private final CSVFormat defaulCSVFormat = csvFormat();
+
     public CSVParser parse(DataSource dataSource) {
-        return parse(dataSource, CSVFormat.DEFAULT);
+        return parse(dataSource, defaulCSVFormat);
     }
 
     public CSVParser parse(DataSource dataSource, CSVFormat format) {
@@ -65,7 +68,7 @@
     }
 
     public CSVParser parse(String csv) {
-        return parse(csv, CSVFormat.DEFAULT);
+        return parse(csv, defaulCSVFormat);
     }
 
     public CSVParser parse(String csv, CSVFormat format) {
@@ -259,6 +262,23 @@
         return result;
     }
 
+    private CSVFormat csvFormat() {
+
+        CSVFormat csvFormat = CSVFormat.valueOf(System.getProperty("CSV_TOOL_FORMAT", "Default"));
+
+        final String delimiter = System.getProperty("CSV_TOOL_DELIMITER");
+        if (StringUtils.isNotEmpty(delimiter)) {
+            csvFormat = csvFormat.withDelimiter(toDelimiter(delimiter));
+        }
+
+        final boolean withHeader = parseBoolean(System.getProperty("CSV_TOOL_HEADERS", Boolean.toString(!csvFormat.getSkipHeaderRecord())));
+        if (withHeader) {
+            csvFormat = csvFormat.withHeader();
+        }
+
+        return csvFormat;
+    }
+
     private static final class ValueResolver implements Function<CSVRecord, String> {
 
         private final Integer index;
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/DataFrameTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/DataFrameTool.java
new file mode 100644
index 0000000..5441861
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/DataFrameTool.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe;
+
+import de.unknownreality.dataframe.DataFrame;
+import de.unknownreality.dataframe.DataFrameWriter;
+import de.unknownreality.dataframe.sort.SortColumn.Direction;
+import de.unknownreality.dataframe.transform.ColumnDataFrameTransform;
+import de.unknownreality.dataframe.transform.CountTransformer;
+import org.apache.commons.csv.CSVParser;
+import org.apache.freemarker.generator.tools.dataframe.converter.CSVConverter;
+import org.apache.freemarker.generator.tools.dataframe.converter.ListConverter;
+import org.apache.freemarker.generator.tools.dataframe.converter.MapConverter;
+
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static de.unknownreality.dataframe.DataFrameWriter.DEFAULT_PRINT_FORMAT;
+
+/**
+ * Create and manipulate data frames (tabular data structure). Data frames allow
+ * easy manipulation and transformation of data, e.g. joining two data frames.
+ * For more information see <a href="https://github.com/nRo/DataFrame">nRo/DataFrame</a>.
+ */
+public class DataFrameTool {
+
+    /**
+     * Create a data frame from Apache Commons CSVParser.
+     *
+     * @param csvParser CSV Parser
+     * @return data frame
+     */
+    public DataFrame fromCSVParser(CSVParser csvParser) {
+        return CSVConverter.toDataFrame(csvParser);
+    }
+
+    /**
+     * Create a data frame from a list of maps.
+     *
+     * @param maps maps to build the data frame
+     * @return data frame
+     */
+    public DataFrame fromMaps(Collection<Map<String, Object>> maps) {
+        return MapConverter.toDataFrame(maps);
+    }
+
+    /**
+     * Create a data frame from a list of rows.
+     *
+     * @param rows                      rows to build the data frame from
+     * @param withFirstRowAsColumnNames column names as first row?
+     * @return data frame
+     */
+    public DataFrame fromRows(List<List<Object>> rows, boolean withFirstRowAsColumnNames) {
+        return ListConverter.toDataFrame(rows, withFirstRowAsColumnNames);
+    }
+
+    /**
+     * Provide a convinience map with predefined sort orders to be used by templates.
+     *
+     * @return available sort orders
+     */
+    public Map<String, Direction> getSortOrder() {
+        final Map<String, Direction> result = new HashMap<>();
+        result.put(Direction.Ascending.name().toUpperCase(), Direction.Ascending);
+        result.put(Direction.Descending.name().toUpperCase(), Direction.Descending);
+        return result;
+    }
+
+    /**
+     * Provide a convinience map with predefined transformers.
+     *
+     * @return available transformers
+     */
+    public Map<String, ColumnDataFrameTransform> getTransformer() {
+        final Map<String, ColumnDataFrameTransform> result = new HashMap<>();
+        result.put("COUNT", countTransformer(false));
+        return result;
+    }
+
+    /**
+     * Print the <code>DataFrame</code> to the FreeMarker writer.
+     *
+     * @param dataFrame data frame
+     */
+    public String print(DataFrame dataFrame) {
+        final StringWriter writer = new StringWriter();
+        DataFrameWriter.write(writer, dataFrame, DEFAULT_PRINT_FORMAT);
+        return writer.toString();
+    }
+
+    @Override
+    public String toString() {
+        return "Bridge to nRo/DataFrame (see https://github.com/nRo/DataFrame)";
+    }
+
+    private static CountTransformer countTransformer(boolean ignoreNA) {
+        return new CountTransformer(ignoreNA);
+    }
+}
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/CSVConverter.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/CSVConverter.java
new file mode 100644
index 0000000..055b508
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/CSVConverter.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe.converter;
+
+import de.unknownreality.dataframe.DataFrame;
+import de.unknownreality.dataframe.DataFrameBuilder;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+
+import java.io.IOException;
+import java.util.List;
+
+public class CSVConverter {
+
+    /**
+     * Create a data frame from  Apache Commons CSV Parser.
+     *
+     * @param csvParser CSV Parser
+     * @return data frame
+     */
+    public static DataFrame toDataFrame(CSVParser csvParser) {
+        try {
+            final List<String> headerNames = csvParser.getHeaderNames();
+            final DataFrameBuilder builder = DataFrameBuilder.create();
+            final List<CSVRecord> records = csvParser.getRecords();
+            final CSVRecord firstRecord = records.get(0);
+
+            //  build dataframe with headers
+            if (headerNames != null && !headerNames.isEmpty()) {
+                headerNames.forEach(builder::addStringColumn);
+            } else {
+                for (int i = 0; i < firstRecord.size(); i++) {
+                    builder.addStringColumn(ConverterUtils.getAlphaColumnName(i + 1));
+                }
+            }
+
+            final DataFrame dataFrame = builder.build();
+
+            // populate rows
+            final String[] currValues = new String[firstRecord.size()];
+            for (CSVRecord csvRecord : records) {
+                for (int i = 0; i < currValues.length; i++) {
+                    currValues[i] = csvRecord.get(i);
+                }
+                dataFrame.append(currValues);
+            }
+
+            return dataFrame;
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create DataFrame", e);
+        }
+    }
+
+}
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ConverterUtils.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ConverterUtils.java
new file mode 100644
index 0000000..5237f52
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ConverterUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe.converter;
+
+import de.unknownreality.dataframe.DataFrame;
+import de.unknownreality.dataframe.DataFrameBuilder;
+import org.apache.freemarker.generator.base.table.Table;
+
+import java.util.List;
+
+public class ConverterUtils {
+
+    static DataFrame toDataFrame(Table table) {
+        final DataFrame dataFrame = create(table);
+        return appendValues(dataFrame, table);
+    }
+
+    static String getAlphaColumnName(int num) {
+        String result = "";
+        while (num > 0) {
+            num--; // 1 => a, not 0 => a
+            final int remainder = num % 26;
+            final char digit = (char) (remainder + 65);
+            result = digit + result;
+            num = (num - remainder) / 26;
+        }
+        return result;
+    }
+
+    private static DataFrameBuilder addColumn(DataFrameBuilder builder, String columnName, Class<?> columnType) {
+        switch (columnType.getName()) {
+            case "java.lang.Boolean":
+                return builder.addBooleanColumn(columnName);
+            case "java.lang.Byte":
+                return builder.addByteColumn(columnName);
+            case "java.lang.Double":
+                return builder.addDoubleColumn(columnName);
+            case "java.lang.Float":
+                return builder.addFloatColumn(columnName);
+            case "java.lang.Integer":
+                return builder.addIntegerColumn(columnName);
+            case "java.lang.Long":
+                return builder.addLongColumn(columnName);
+            case "java.lang.Short":
+                return builder.addShortColumn(columnName);
+            case "java.lang.String":
+                return builder.addStringColumn(columnName);
+            case "java.time.LocalDate":
+                return builder.addStringColumn(columnName);
+            case "java.time.LocalTime":
+                return builder.addStringColumn(columnName);
+            case "java.util.Date":
+                return builder.addStringColumn(columnName);
+            default:
+                throw new RuntimeException("Unable to add colum for the following type: " + columnType.getName());
+        }
+    }
+
+    private static Comparable<?>[] toComparables(List<?> values) {
+        final int size = values.size();
+        final Comparable<?>[] comparables = new Comparable<?>[size];
+        for (int i = 0; i < size; i++) {
+            comparables[i] = (Comparable<?>) values.get(i);
+        }
+        return comparables;
+    }
+
+    /**
+     * Create a <code>DataFrame</code> from a table.
+     *
+     * @param table table
+     * @return data frame
+     */
+    private static DataFrame create(Table table) {
+        final DataFrameBuilder builder = DataFrameBuilder.create();
+
+        if (table.hasColumnHeaderRow()) {
+            for (int i = 0; i < table.getColumnNames().size(); i++) {
+                final String columnName = table.getColumnNames().get(i);
+                final Class<?> columnType = table.getColumnTypes().get(i);
+                addColumn(builder, columnName, columnType);
+            }
+        } else {
+            if (!table.isEmpty()) {
+                final List<Object> firstRecord = table.getRow(0);
+                for (int i = 0; i < firstRecord.size(); i++) {
+                    final String columnName = getAlphaColumnName(i + 1);
+                    final Class<?> columnType = table.getColumnTypes().get(i);
+                    addColumn(builder, columnName, columnType);
+                }
+            }
+        }
+
+        return builder.build();
+    }
+
+    private static DataFrame appendValues(DataFrame dataFrame, Table table) {
+        // TODO the conversion to Comparable[] is ugly
+        for (int i = 0; i < table.size(); i++) {
+            dataFrame.append(ConverterUtils.toComparables(table.getRow(i)));
+        }
+        return dataFrame;
+    }
+}
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ListConverter.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ListConverter.java
new file mode 100644
index 0000000..ecf891a
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/ListConverter.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe.converter;
+
+import de.unknownreality.dataframe.DataFrame;
+import org.apache.freemarker.generator.base.table.Table;
+
+import java.util.List;
+
+public class ListConverter {
+
+    /**
+     * Create a data frame from a list of rows. It is assumed
+     * that the rows represent tabular data.
+     *
+     * @param rows rows to build the data frame
+     * @return <code>DataFrame</code>
+     */
+    public static DataFrame toDataFrame(List<List<Object>> rows, boolean withFirstRowAsColumnNames) {
+        final Table table = Table.fromRows(rows, withFirstRowAsColumnNames);
+        return ConverterUtils.toDataFrame(table);
+    }
+}
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/MapConverter.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/MapConverter.java
new file mode 100644
index 0000000..f6800ce
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/converter/MapConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe.converter;
+
+import de.unknownreality.dataframe.DataFrame;
+import org.apache.freemarker.generator.base.table.Table;
+
+import java.util.Collection;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
+
+public class MapConverter {
+
+    /**
+     * Create a data frame from a list of maps. It is assumed
+     * that the map represent tabular data.
+     *
+     * @param map map to build the data frame
+     * @return <code>DataFrame</code>
+     */
+    public static DataFrame toDataFrame(Map<String, Object> map) {
+        return toDataFrame(singletonList(map));
+    }
+
+    /**
+     * Create a data frame from a list of maps. It is assumed
+     * that the map represent tabular data.
+     *
+     * @param maps list of map to build the data frame
+     * @return <code>DataFrame</code>
+     */
+    public static DataFrame toDataFrame(Collection<Map<String, Object>> maps) {
+        final Table table = Table.fromMaps(maps);
+        return ConverterUtils.toDataFrame(table);
+    }
+}
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
index f03df80..b5ead1d 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/excel/ExcelTool.java
@@ -78,10 +78,10 @@
      * @param sheet Excel sheet
      * @return Table containing formatted cell values as strings
      */
-    public List<List<String>> toTable(Sheet sheet) {
+    public List<List<Object>> toTable(Sheet sheet) {
         final DataFormatter dataFormatter = dataFormatter();
         final Iterator<Row> iterator = sheet.iterator();
-        final List<List<String>> result = new ArrayList<>();
+        final List<List<Object>> result = new ArrayList<>();
 
         while (iterator.hasNext()) {
             final Row row = iterator.next();
@@ -117,8 +117,8 @@
         return "Process Excels files (XLS, XLSX) using Apache POI (see https://poi.apache.org)";
     }
 
-    private static List<String> toColumns(Row row, DataFormatter dataFormatter) {
-        final List<String> columnValues = new ArrayList<>();
+    private static List<Object> toColumns(Row row, DataFormatter dataFormatter) {
+        final List<Object> columnValues = new ArrayList<>();
         for (int columnIndex = 0; columnIndex < row.getLastCellNum(); columnIndex++) {
             final Cell cell = row.getCell(columnIndex, CREATE_NULL_AS_BLANK);
             final String formatedCellValue = dataFormatter.formatCellValue(cell).trim();
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/gson/GsonTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/gson/GsonTool.java
index 6a864eb..dd8128b 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/gson/GsonTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/gson/GsonTool.java
@@ -18,32 +18,49 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import com.google.gson.reflect.TypeToken;
 import com.google.gson.stream.JsonReader;
 import org.apache.freemarker.generator.base.datasource.DataSource;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
-import java.lang.reflect.Type;
-import java.util.Map;
 
+/**
+ * JSON processing using <a href="https://github.com/google/gson">Google GSON</a>
+ */
 public class GsonTool {
 
-    private Gson gson;
-    private Type type;
+    private volatile Gson gson;
 
-    public Map<String, Object> parse(DataSource dataSource) {
+    /**
+     * Parse a data source containing a JSON object.
+     *
+     * @param dataSource data source
+     * @return parsed JSON either as a map or list
+     */
+    public Object parse(DataSource dataSource) {
         try (JsonReader reader = new JsonReader(new InputStreamReader(dataSource.getUnsafeInputStream()))) {
-            return gson().fromJson(reader, type());
+            return gson().fromJson(reader, Object.class);
         } catch (IOException e) {
             throw new RuntimeException("Failed to parse data source:" + dataSource, e);
         }
     }
 
-    public Map<String, Object> parse(String json) {
-        return gson().fromJson(json, type());
+    /**
+     * Parse a JSON object string.
+     *
+     * @param json Json string
+     * @return parsed JSON either as a map or list
+     */
+    public Object parse(String json) {
+        return gson().fromJson(json, Object.class);
     }
 
+    /**
+     * Converts to JSON string.
+     *
+     * @param src source object
+     * @return JSON string
+     */
     public String toJson(Object src) {
         return gson().toJson(src);
     }
@@ -59,11 +76,4 @@
         }
         return gson;
     }
-
-    private synchronized Type type() {
-        if (type == null) {
-            type = new TypeToken<Map<String, Object>>() {}.getType();
-        }
-        return type;
-    }
 }
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/jsoup/JsoupTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/jsoup/JsoupTool.java
index 6a9d23e..df252c3 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/jsoup/JsoupTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/jsoup/JsoupTool.java
@@ -18,13 +18,14 @@
 
 import org.apache.freemarker.generator.base.datasource.DataSource;
 import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
 
 import java.io.IOException;
 import java.io.InputStream;
 
 public class JsoupTool {
 
-    public org.jsoup.nodes.Document parse(DataSource dataSource) {
+    public Document parse(DataSource dataSource) {
         try (InputStream is = dataSource.getUnsafeInputStream()) {
             return Jsoup.parse(is, dataSource.getCharset().name(), "");
         } catch (IOException e) {
@@ -32,7 +33,7 @@
         }
     }
 
-    public org.jsoup.nodes.Document parse(String html) {
+    public Document parse(String html) {
         return Jsoup.parse(html);
     }
 
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlTool.java
index 1058d47..efacaea 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlTool.java
@@ -22,7 +22,6 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Map;
 
 import static org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK;
 
@@ -30,7 +29,7 @@
 
     private Yaml yaml;
 
-    public Map<String, Object> parse(DataSource dataSource) {
+    public Object parse(DataSource dataSource) {
         try (InputStream is = dataSource.getUnsafeInputStream()) {
             return yaml().load(is);
         } catch (IOException e) {
@@ -38,8 +37,8 @@
         }
     }
 
-    public Map<String, Object> parse(String value) {
-        return yaml().load(value);
+    public Object parse(String yaml) {
+        return yaml().load(yaml);
     }
 
     public String toYaml(Object data) {
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/system/SystemTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/system/SystemTool.java
index 183b9da..ae5b66a 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/system/SystemTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/system/SystemTool.java
@@ -34,7 +34,7 @@
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Model.FREEMARKER_WRITER;
 
 /**
- * Provides system related functionality, e.g. accessing environment variable,
+ * Provides system related functionality, e.g. accessing environment variables,
  * system properties, commandl-line arguments, hostname, FreeMarker writer, etc.
  */
 @SuppressWarnings("unchecked")
diff --git a/freemarker-generator-tools/src/test/data/csv/data_join_a.csv b/freemarker-generator-tools/src/test/data/csv/data_join_a.csv
new file mode 100644
index 0000000..cc05775
--- /dev/null
+++ b/freemarker-generator-tools/src/test/data/csv/data_join_a.csv
@@ -0,0 +1,5 @@
+GENE_ID;FPKM;CHR
+A;5;1
+B;4;2
+C;6;3
+D;6;1
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/test/data/csv/data_join_b.csv b/freemarker-generator-tools/src/test/data/csv/data_join_b.csv
new file mode 100644
index 0000000..c84d1a0
--- /dev/null
+++ b/freemarker-generator-tools/src/test/data/csv/data_join_b.csv
@@ -0,0 +1,5 @@
+TRANSCRIPT_ID;GENE_ID;FPKM;TRANSCRIPT_NUMBER
+TA;A;7;1
+TB;A;3;2
+TC;B;6;1
+TD;E;4;1
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVToolTest.java
index f76e252..d8e4b6a 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonscsv/CommonsCSVToolTest.java
@@ -74,7 +74,6 @@
         }
 
         assertEquals(7, keys.size());
-        assertEquals(7, keys.size());
         assertEquals("C71", keys.get(0));
         assertEquals("C72", keys.get(1));
         assertEquals("C73", keys.get(2));
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
new file mode 100644
index 0000000..6965064
--- /dev/null
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.tools.dataframe;
+
+import de.unknownreality.dataframe.DataFrame;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.tools.commonscsv.CommonsCSVTool;
+import org.apache.freemarker.generator.tools.excel.ExcelTool;
+import org.apache.freemarker.generator.tools.gson.GsonTool;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static org.apache.commons.csv.CSVFormat.DEFAULT;
+
+public class DataFrameToolTest {
+
+    private static final String CSV_WITHOUT_HEADER = "A;5;1\n" +
+            "B;4;2\n" +
+            "C;6;3\n" +
+            "D;6;1";
+
+    private static final String CSV_WITH_HEADER = "GENE_ID;FPKM;CHR\n" +
+            CSV_WITHOUT_HEADER;
+
+    private static final String JSON_ARRAY = "[\n" +
+            "    {\n" +
+            "        \"Book ID\": \"1\",\n" +
+            "        \"Book Name\": \"Computer Architecture\",\n" +
+            "        \"Category\": \"Computers\",\n" +
+            "        \"In Stock\": true,\n" +
+            "        \"Price\": 125.60\n" +
+            "    },\n" +
+            "    {\n" +
+            "        \"Book ID\": \"2\",\n" +
+            "        \"Book Name\": \"Asp.Net 4 Blue Book\",\n" +
+            "        \"Category\": \"Programming\",\n" +
+            "        \"In Stock\": null,\n" +
+            "        \"Price\": 56.00\n" +
+            "    },\n" +
+            "    {\n" +
+            "        \"Book ID\": \"3\",\n" +
+            "        \"Book Name\": \"Popular Science\",\n" +
+            "        \"Category\": \"Science\",\n" +
+            "        \"Price\": 210.40\n" +
+            "    }\n" +
+            "]";
+
+    // === CSV ==============================================================
+
+    @Test
+    public void shouldParseCsvFileWithoutHeader() {
+        final CSVParser csvParser = csvParser(CSV_WITHOUT_HEADER, DEFAULT.withDelimiter(';'));
+        final DataFrame dataFrame = dataFrameTool().fromCSVParser(csvParser);
+
+        assertEquals(3, dataFrame.getColumns().size());
+        assertEquals(4, dataFrame.getRows().size());
+        assertEquals("A", dataFrame.getRow(0).get(0));
+        assertEquals("4", dataFrame.getRow(1).get(1));
+        assertEquals("3", dataFrame.getRow(2).get(2));
+    }
+
+    @Test
+    public void shouldParseCsvFileWithHeader() {
+        final CSVParser csvParser = csvParser(CSV_WITH_HEADER, DEFAULT.withHeader().withDelimiter(';'));
+        final DataFrame dataFrame = dataFrameTool().fromCSVParser(csvParser);
+
+        assertEquals(3, dataFrame.getColumns().size());
+        assertEquals(4, dataFrame.getRows().size());
+        assertEquals("A", dataFrame.getColumn("GENE_ID").get(0));
+        assertEquals("4", dataFrame.getColumn("FPKM").get(1));
+        assertEquals("3", dataFrame.getColumn("CHR").get(2));
+    }
+
+    // === JSON =============================================================
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void shouldParseJsonTable() {
+        final String columnName = "Book ID";
+        final List<Map<String, Object>> json = (List<Map<String, Object>>) gsonTool().parse(JSON_ARRAY);
+        final DataFrame dataFrame = dataFrameTool().fromMaps(json);
+
+        assertEquals(5, dataFrame.getColumns().size());
+        assertEquals(3, dataFrame.getRows().size());
+        assertEquals("1", dataFrame.getColumn(columnName).get(0));
+        assertEquals("2", dataFrame.getColumn(columnName).get(1));
+        assertEquals("3", dataFrame.getColumn(columnName).get(2));
+    }
+
+    // === Excel ============================================================
+
+    @Test
+    public void shouldParseExcelSheet() {
+        final ExcelTool excelTool = excelTool();
+        final Workbook workbook = excelTool.parse(DataSourceFactory.create("./src/test/data/excel/test.xls"));
+        final List<List<Object>> sheet = excelTool.toTable(workbook.getSheetAt(0));
+
+        final DataFrame dataFrame = dataFrameTool().fromRows(sheet, true);
+
+        assertEquals(7, dataFrame.getColumns().size());
+        assertEquals(2, dataFrame.getRows().size());
+        assertNotNull(dataFrame.getColumn("Text"));
+        assertNotNull(dataFrame.getColumn("Date"));
+        assertNotNull(dataFrame.getColumn("Number"));
+        assertNotNull(dataFrame.getColumn("Time"));
+        assertNotNull(dataFrame.getColumn("Percentage"));
+        assertNotNull(dataFrame.getColumn("Forumula"));
+        assertEquals("Row 1", dataFrame.getValue(0,0));
+        assertEquals("C3*F3", dataFrame.getColumn("Forumula").get(1));
+
+    }
+
+    private DataFrameTool dataFrameTool() {
+        return new DataFrameTool();
+    }
+
+    private CommonsCSVTool commonsCSVTool() {
+        return new CommonsCSVTool();
+    }
+
+    private GsonTool gsonTool() {
+        return new GsonTool();
+    }
+
+    private ExcelTool excelTool() {
+        return new ExcelTool();
+    }
+
+    private CSVParser csvParser(String csv, CSVFormat csvFormat) {
+        return commonsCSVTool().parse(csv, csvFormat);
+    }
+}
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/excel/ExcelToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/excel/ExcelToolTest.java
index 9c8ef8b..089c2d0 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/excel/ExcelToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/excel/ExcelToolTest.java
@@ -39,7 +39,7 @@
         final Workbook workbook = workbook(TEST_XLS);
 
         final List<Sheet> sheets = excelTool().getSheets(workbook);
-        final List<List<String>> records = excelTool().toTable(sheets.get(0));
+        final List<List<Object>> records = excelTool().toTable(sheets.get(0));
 
         assertEquals(1, sheets.size());
         assertEquals(3, records.size());
@@ -50,7 +50,7 @@
         final Workbook workbook = workbook(TEST_XLSX);
 
         final List<Sheet> sheets = excelTool().getSheets(workbook);
-        final List<List<String>> records = excelTool().toTable(sheets.get(0));
+        final List<List<Object>> records = excelTool().toTable(sheets.get(0));
 
         assertEquals(1, sheets.size());
         assertEquals(3, records.size());
@@ -71,9 +71,9 @@
     public void shouldConvertSheetToTable() {
         final Workbook workbook = workbook(TEST_XLSX);
         final List<Sheet> sheets = excelTool().getSheets(workbook);
-        final List<List<String>> records = excelTool().toTable(sheets.get(0));
+        final List<List<Object>> records = excelTool().toTable(sheets.get(0));
 
-        final List<String> record = records.get(1);
+        final List<Object> record = records.get(1);
 
         assertEquals("Row 1", record.get(0));
         assertEquals("01/31/17", record.get(1));
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/gson/GsonToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/gson/GsonToolTest.java
index 7a5f5cf..601b2b4 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/gson/GsonToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/gson/GsonToolTest.java
@@ -29,13 +29,13 @@
 
 public class GsonToolTest {
 
-    private static final String JSON_OBJECT_STRING = "{\n" +
+    private static final String JSON_OBJECT = "{\n" +
             "  \"id\": 110.0,\n" +
             "  \"language\": \"Python\",\n" +
             "  \"price\": 1900.0\n" +
             "}";
 
-    private static final String JSON_ARRAY_STRING = "{\n" +
+    private static final String JSON_OBJECT_WITH_ARRAY = "{\n" +
             "  \"eBooks\": [\n" +
             "    {\n" +
             "      \"language\": \"Pascal\",\n" +
@@ -59,11 +59,33 @@
             "    \"color\": \"Red\"\n" +
             "}";
 
+    private static final String JSON_ARRAY = "[\n" +
+            "    {\n" +
+            "        \"Book ID\": \"1\",\n" +
+            "        \"Book Name\": \"Computer Architecture\",\n" +
+            "        \"Category\": \"Computers\",\n" +
+            "        \"Price\": \"125.60\"\n" +
+            "    },\n" +
+            "    {\n" +
+            "        \"Book ID\": \"2\",\n" +
+            "        \"Book Name\": \"Asp.Net 4 Blue Book\",\n" +
+            "        \"Category\": \"Programming\",\n" +
+            "        \"Price\": \"56.00\"\n" +
+            "    },\n" +
+            "    {\n" +
+            "        \"Book ID\": \"3\",\n" +
+            "        \"Book Name\": \"Popular Science\",\n" +
+            "        \"Category\": \"Science\",\n" +
+            "        \"Price\": \"210.40\"\n" +
+            "    }\n" +
+            "]";
+
     private final GsonTool gsonTool = gsonTool();
 
     @Test
+    @SuppressWarnings("unchecked")
     public void shouldParseJsonObject() {
-        final Map<String, Object> map = parse(JSON_OBJECT_STRING);
+        final Map<String, Object> map = (Map) gsonTool.parse(JSON_OBJECT);
 
         assertEquals(3, map.size());
         assertEquals("110.0", map.get("id").toString());
@@ -72,39 +94,48 @@
     }
 
     @Test
-    public void shouldParseJsonArray() {
-        final Map<String, Object> map = parse(JSON_ARRAY_STRING);
+    @SuppressWarnings("unchecked")
+    public void shouldParseJsonObjectWithArray() {
+        final Map<String, Object> map = (Map) gsonTool.parse(JSON_OBJECT_WITH_ARRAY);
 
         assertEquals(1, map.size());
         assertEquals(3, ((List) map.get("eBooks")).size());
-
-        return;
     }
 
     @Test
+    @SuppressWarnings("unchecked")
     public void shouldParseJsonWithComemnts() {
-        final Map<String, Object> map = parse(JSON_WITH_COMMENTS);
+        final Map<String, Object> map = (Map) gsonTool.parse(JSON_WITH_COMMENTS);
 
         assertEquals("Apple", map.get("fruit"));
     }
 
     @Test
-    public void shouldConvertToJson() {
-        assertEquals(JSON_OBJECT_STRING, gsonTool.toJson(parse(JSON_OBJECT_STRING)));
-        assertEquals(JSON_ARRAY_STRING, gsonTool.toJson(parse(JSON_ARRAY_STRING)));
+    @SuppressWarnings("unchecked")
+    public void shouldParseJsonArray() {
+        final List<Map<String, Object>> list = (List<Map<String, Object>>) gsonTool.parse(JSON_ARRAY);
+
+        assertEquals(3, list.size());
+        assertEquals("1", list.get(0).get("Book ID"));
+        assertEquals("2", list.get(1).get("Book ID"));
+        assertEquals("3", list.get(2).get("Book ID"));
     }
 
     @Test
-    public void shouldParseComplexJson() throws IOException {
-        final String json = readFileToString(new File("./src/test/data/json/swagger.json"), UTF_8);
-        final Map<String, Object> map = parse(json);
-
-        assertEquals("petstore.swagger.io", map.get("host"));
-        assertEquals(json, gsonTool.toJson(parse(json)));
+    @SuppressWarnings("unchecked")
+    public void shouldConvertToJson() {
+        assertEquals(JSON_OBJECT, gsonTool.toJson(gsonTool.parse(JSON_OBJECT)));
+        assertEquals(JSON_OBJECT_WITH_ARRAY, gsonTool.toJson(gsonTool.parse(JSON_OBJECT_WITH_ARRAY)));
     }
 
-    private Map<String, Object> parse(String json) {
-        return gsonTool.parse(json);
+    @Test
+    @SuppressWarnings("unchecked")
+    public void shouldParseComplexJson() throws IOException {
+        final String json = readFileToString(new File("./src/test/data/json/swagger.json"), UTF_8);
+        final Map<String, Object> map = (Map) gsonTool.parse(json);
+
+        assertEquals("petstore.swagger.io", map.get("host"));
+        assertEquals(json, gsonTool.toJson(gsonTool.parse(json)));
     }
 
     private GsonTool gsonTool() {
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
index d588094..25ee6b1 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
@@ -34,16 +34,48 @@
 
     private static final String ANY_GROUP = "group";
 
-    private static final String ANY_YAML_STRING = "docker:\n" +
+    private static final String NESTED_YAML_MAP = "docker:\n" +
             "    - image: ubuntu:14.04\n" +
             "    - image: mongo:2.6.8\n" +
             "      command: [mongod, --smallfiles]\n" +
             "    - image: postgres:9.4.1";
 
+    private static final String MAP_YAML = "- NGINX_PORT: 8443\n" +
+            "- NGINX_HOSTNAME: localhost";
+
+    private static final String LIST_YAML = "- foo\n" +
+            "- bar";
+
+    @Test
+    public void shallParseSimpleListYamlString() {
+        final List<String> list = (List<String>) snakeYamlTool().parse(LIST_YAML);
+
+        assertEquals(2, list.size());
+        assertEquals("foo", list.get(0));
+        assertEquals("bar", list.get(1));
+    }
+
+    @Test
+    public void shallParseListOfMapYamlString() {
+        final List<Map<String, Object>> list = (List<Map<String, Object>>) snakeYamlTool().parse(MAP_YAML);
+
+        assertEquals(2, list.size());
+        assertEquals(8443, list.get(0).get("NGINX_PORT"));
+        assertEquals("localhost", list.get(1).get("NGINX_HOSTNAME"));
+    }
+
+    @Test
+    public void shallParseNestedYamlString() {
+        final Map<String, Object> map = (Map<String, Object>) snakeYamlTool().parse(NESTED_YAML_MAP);
+
+        assertEquals(1, map.size());
+        assertEquals(3, ((List<?>) map.get("docker")).size());
+    }
+
     @Test
     public void shallParseYamlDataSource() {
-        try (DataSource dataSource = dataSource(ANY_YAML_STRING)) {
-            final Map<String, Object> map = snakeYamlTool().parse(dataSource);
+        try (DataSource dataSource = dataSource(NESTED_YAML_MAP)) {
+            final Map<String, Object> map = (Map<String, Object>) snakeYamlTool().parse(dataSource);
 
             assertEquals(1, map.size());
             assertEquals(3, ((List<?>) map.get("docker")).size());
@@ -51,29 +83,21 @@
     }
 
     @Test
-    public void shallParseYamlString() {
-        final Map<String, Object> map = snakeYamlTool().parse(ANY_YAML_STRING);
+    public void shouldParseComplexYaml() throws IOException {
+        final String yaml = readFileToString(new File("./src/test/data/yaml/swagger.yaml"), UTF_8);
+        final Map<String, Object> map = (Map<String, Object>) snakeYamlTool().parse(yaml);
 
-        assertEquals(1, map.size());
-        assertEquals(3, ((List<?>) map.get("docker")).size());
+        assertEquals("2.0", map.get("swagger"));
+        assertEquals(16956, snakeYamlTool().toYaml(map).length());
     }
 
     @Test
     public void shallConvertToYamlString() {
-        final Map<String, Object> map = snakeYamlTool().parse(ANY_YAML_STRING);
+        final Map<String, Object> map = (Map<String, Object>) snakeYamlTool().parse(NESTED_YAML_MAP);
 
         assertEquals(114, snakeYamlTool().toYaml(map).length());
     }
 
-    @Test
-    public void shouldParseComplexYaml() throws IOException {
-        final String yaml = readFileToString(new File("./src/test/data/yaml/swagger.yaml"), UTF_8);
-        final Map<String, Object> map = snakeYamlTool().parse(yaml);
-
-        assertEquals("2.0", map.get("swagger"));
-        assertEquals(16956, snakeYamlTool().toYaml(map).length());
-    }
-
     private SnakeYamlTool snakeYamlTool() {
         return new SnakeYamlTool();
     }
diff --git a/travis.sh b/travis.sh
index e9c3f62..912d3d7 100755
--- a/travis.sh
+++ b/travis.sh
@@ -1,3 +1,4 @@
+#!/bin/sh
 mvn clean install
 cd ./freemarker-generator-cli
 sh ./run-samples.sh