Merge remote-tracking branch 'origin/master' into FREEMARKER-154. Also updated website module and DocBook content to be compatible with it.
diff --git a/freemarker-generator-base/pom.xml b/freemarker-generator-base/pom.xml
index deb18fb..512158d 100644
--- a/freemarker-generator-base/pom.xml
+++ b/freemarker-generator-base/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>freemarker-generator-base</artifactId>
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
index 86a69ab..f4e19bd 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
@@ -93,6 +93,15 @@
         public static final String FREEMARKER_USER_PARAMETERS = "freemarker.user.parameters";
     }
 
+    public static class SeedType {
+
+        private SeedType() {
+        }
+
+        public static final String TEMPLATE = "template";
+        public static final String DATASOURCE = "datasource";
+    }
+
     public static class SystemProperties {
 
         private SystemProperties() {
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/CachingUrlDataSource.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/CachingUrlDataSource.java
index 6616598..81b41c4 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/CachingUrlDataSource.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/CachingUrlDataSource.java
@@ -26,6 +26,7 @@
  */
 public class CachingUrlDataSource extends URLDataSource {
 
+    /** Cached content type. */
     private String contentType;
 
     public CachingUrlDataSource(URL url) {
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
index 66557d1..de2b295 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
@@ -59,24 +59,29 @@
  */
 public class DataSource implements Closeable, javax.activation.DataSource {
 
-    public static final String METADATA_BASE_NAME = "basename";
+    public static final String METADATA_BASE_NAME = "baseName";
+    public static final String METADATA_CHARSET = "charset";
     public static final String METADATA_EXTENSION = "extension";
-    public static final String METADATA_FILE_NAME = "filename";
-    public static final String METADATA_FILE_PATH = "filepath";
+    public static final String METADATA_FILE_NAME = "fileName";
+    public static final String METADATA_FILE_PATH = "filePath";
     public static final String METADATA_GROUP = "group";
+    public static final String METADATA_MIME_TYPE = "mimeType";
     public static final String METADATA_NAME = "name";
+    public static final String METADATA_RELATIVE_FILE_PATH = "relativeFilePath";
     public static final String METADATA_URI = "uri";
-    public static final String METADATA_MIME_TYPE = "mimetype";
 
+    /** List of metadata keys */
     public static final List<String> METADATA_KEYS = Arrays.asList(
             METADATA_BASE_NAME,
+            METADATA_CHARSET,
             METADATA_EXTENSION,
             METADATA_FILE_NAME,
             METADATA_FILE_PATH,
             METADATA_GROUP,
+            METADATA_MIME_TYPE,
             METADATA_NAME,
-            METADATA_URI,
-            METADATA_MIME_TYPE
+            METADATA_RELATIVE_FILE_PATH,
+            METADATA_URI
     );
 
     /** Human-readable name of the data source */
@@ -91,6 +96,9 @@
     /** The underlying "javax.activation.DataSource" */
     private final javax.activation.DataSource dataSource;
 
+    /** The relative path for a file-based data source */
+    private final String relativeFilePath;
+
     /** Content type of data source either provided by the caller or fetched directly from the data source */
     private final String contentType;
 
@@ -106,19 +114,21 @@
     /**
      * Constructor.
      *
-     * @param name        Human-readable name of the data source
-     * @param group       Optional group of data source
-     * @param uri         source URI of the data source
-     * @param dataSource  JAF data source being wrapped
-     * @param contentType content type of data source either provided by the caller or fetched directly from the data source
-     * @param charset     option charset for directly accessing text-based content
-     * @param properties  optional name/value pairs
+     * @param name             Human-readable name of the data source
+     * @param group            Optional group of data source
+     * @param uri              source URI of the data source
+     * @param dataSource       JAF data source being wrapped
+     * @param relativeFilePath relative path regarding a source directory
+     * @param contentType      content type of data source either provided by the caller or fetched directly from the data source
+     * @param charset          option charset for directly accessing text-based content
+     * @param properties       optional name/value pairs
      */
     public DataSource(
             String name,
             String group,
             URI uri,
             javax.activation.DataSource dataSource,
+            String relativeFilePath,
             String contentType,
             Charset charset,
             Map<String, String> properties) {
@@ -126,12 +136,17 @@
         this.group = StringUtils.emptyToNull(group);
         this.uri = requireNonNull(uri);
         this.dataSource = requireNonNull(dataSource);
+        this.relativeFilePath = relativeFilePath != null ? relativeFilePath : "";
         this.contentType = contentType;
         this.charset = charset;
         this.properties = properties != null ? new HashMap<>(properties) : new HashMap<>();
         this.closeables = new CloseableReaper();
     }
 
+    public static DataSourceBuilder builder() {
+        return DataSourceBuilder.builder();
+    }
+
     @Override
     public String getName() {
         return name;
@@ -182,6 +197,16 @@
     }
 
     /**
+     * Get the path from the underlying "FileDataSource". All
+     * other data sources will return an empty string.
+     *
+     * @return file name or empty string
+     */
+    public String getFilePath() {
+        return isFileDataSource() ? FilenameUtils.getFullPathNoEndSeparator(uri.getPath()) : "";
+    }
+
+    /**
      * Get the base name from the underlying "FileDataSource". All
      * other data sources will return an empty string.
      *
@@ -202,6 +227,16 @@
     }
 
     /**
+     * Get the relative path for a file-based data source starting from
+     * the data source directory.
+     *
+     * @return relative path
+     */
+    public String getRelativeFilePath() {
+        return relativeFilePath;
+    }
+
+    /**
      * Get the charset. If no charset can be detected UTF-8 is assumed.
      *
      * @return charset
@@ -341,12 +376,16 @@
         switch (key) {
             case METADATA_BASE_NAME:
                 return getBaseName();
+            case METADATA_CHARSET:
+                return getCharset().name();
             case METADATA_EXTENSION:
                 return getExtension();
             case METADATA_FILE_NAME:
                 return getFileName();
             case METADATA_FILE_PATH:
-                return FilenameUtils.getFullPathNoEndSeparator(uri.getPath());
+                return getFilePath();
+            case METADATA_RELATIVE_FILE_PATH:
+                return getRelativeFilePath();
             case METADATA_GROUP:
                 return getGroup();
             case METADATA_NAME:
@@ -371,7 +410,8 @@
     }
 
     /**
-     * Matches a metadata key with a wildcard expression.
+     * Matches a metadata key with a wildcard expression. If the wildcard is prefixed
+     * with a "!" than the match will be negated.
      *
      * @param key      metadata key, e.g. "name", "fileName", "baseName", "extension", "uri", "group"
      * @param wildcard the wildcard string to match against
@@ -380,7 +420,11 @@
      */
     public boolean match(String key, String wildcard) {
         final String value = getMetadata(key);
-        return FilenameUtils.wildcardMatch(value, wildcard);
+        if (wildcard != null && wildcard.startsWith("!")) {
+            return !FilenameUtils.wildcardMatch(value, wildcard.substring(1));
+        } else {
+            return FilenameUtils.wildcardMatch(value, wildcard);
+        }
     }
 
     /**
@@ -432,4 +476,66 @@
     private boolean isByteArrayDataSource() {
         return dataSource instanceof ByteArrayDataSource;
     }
+
+    public static final class DataSourceBuilder {
+        private String name;
+        private String group;
+        private URI uri;
+        private javax.activation.DataSource dataSource;
+        private String relativeFilePath;
+        private String contentType;
+        private Charset charset;
+        private Map<String, String> properties;
+
+        private DataSourceBuilder() {
+        }
+
+        static DataSourceBuilder builder() {
+            return new DataSourceBuilder();
+        }
+
+        public DataSourceBuilder name(String name) {
+            this.name = name;
+            return this;
+        }
+
+        public DataSourceBuilder group(String group) {
+            this.group = group;
+            return this;
+        }
+
+        public DataSourceBuilder uri(URI uri) {
+            this.uri = uri;
+            return this;
+        }
+
+        public DataSourceBuilder dataSource(javax.activation.DataSource dataSource) {
+            this.dataSource = dataSource;
+            return this;
+        }
+
+        public DataSourceBuilder relativeFilePath(String relativeFilePath) {
+            this.relativeFilePath = relativeFilePath;
+            return this;
+        }
+
+        public DataSourceBuilder contentType(String contentType) {
+            this.contentType = contentType;
+            return this;
+        }
+
+        public DataSourceBuilder charset(Charset charset) {
+            this.charset = charset;
+            return this;
+        }
+
+        public DataSourceBuilder properties(Map<String, String> properties) {
+            this.properties = properties;
+            return this;
+        }
+
+        public DataSource build() {
+            return new DataSource(name, group, uri, dataSource, relativeFilePath, contentType, charset, properties);
+        }
+    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
index e6f37f5..d1ee551 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
@@ -62,7 +62,13 @@
             URI uri,
             javax.activation.DataSource dataSource,
             Map<String, String> properties) {
-        return new DataSource(name, group, uri, dataSource, null, null, properties);
+        return DataSource.builder()
+                .name(name)
+                .group(group)
+                .uri(uri)
+                .dataSource(dataSource)
+                .properties(properties)
+                .build();
     }
 
     public static DataSource create(
@@ -73,7 +79,15 @@
             String contentType,
             Charset charset,
             Map<String, String> properties) {
-        return new DataSource(name, group, uri, dataSource, contentType, charset, properties);
+        return DataSource.builder()
+                .name(name)
+                .group(group)
+                .uri(uri)
+                .dataSource(dataSource)
+                .contentType(contentType)
+                .charset(charset)
+                .properties(properties)
+                .build();
     }
 
     // == URL ===============================================================
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
index 4a22137..d2d80c2 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
@@ -22,7 +22,7 @@
      * Check if the data source can be loaded by this instance.
      *
      * @param source source to be loaded from
-     * @return true if the instance wold be able to load a data source
+     * @return true if the instance would be able to load a data source
      */
     boolean accept(String source);
 
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java
index b7c4a3e..2e061bb 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java
@@ -24,21 +24,16 @@
 import java.util.Arrays;
 
 /**
- * Creates a FreeMarker data source from various sources.
+ * Factory method to create a <code>DataSourceLoader</code>.
  */
 public abstract class DataSourceLoaderFactory {
 
-    private static DataSourceLoader dataSourceLoader;
-
-    public static synchronized DataSourceLoader create() {
-        if (dataSourceLoader == null) {
-            dataSourceLoader = new DefaultDataSourceLoader(
-                    Arrays.asList(
-                            new FileDataSourceLoader(),
-                            new HttpDataSourceLoader(),
-                            new EnvironmentDataSourceLoader()
-                    ));
-        }
-        return dataSourceLoader;
+    public static DataSourceLoader create() {
+        return new DefaultDataSourceLoader(
+                Arrays.asList(
+                        new FileDataSourceLoader(),
+                        new HttpDataSourceLoader(),
+                        new EnvironmentDataSourceLoader()
+                ));
     }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
index fcb3432..7762c41 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
@@ -23,9 +23,12 @@
 import java.io.Closeable;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import static java.util.function.Function.identity;
@@ -39,24 +42,27 @@
     /** The underlying list of data sources */
     private final List<DataSource> dataSources;
 
+    /** Map of named data sources */
+    private final Map<String, DataSource> dataSourcesMap;
+
     public DataSources(Collection<DataSource> dataSources) {
-        Validate.notNull(dataSources, "No data sources provided");
-        this.dataSources = new ArrayList<>(dataSources);
+        Validate.notNull(dataSources, "dataSources must not be null");
+
+        this.dataSources = Collections.unmodifiableList(new ArrayList<>(dataSources));
+        this.dataSourcesMap = Collections.unmodifiableMap(dataSourcesMap(dataSources));
     }
 
     /**
      * Get the names of all data sources.
      *
-     * @return data source names
+     * @return list of data source names
      */
     public List<String> getNames() {
-        return dataSources.stream()
-                .map(DataSource::getName)
-                .collect(Collectors.toList());
+        return new ArrayList<>(dataSourcesMap.keySet());
     }
 
     /**
-     * Get the requested metadata value for all data sources.
+     * Get a list of distinct metadata values for all data sources.
      *
      * @param key key of the metadata part
      * @return list of metadata values
@@ -64,13 +70,15 @@
     public List<String> getMetadata(String key) {
         return dataSources.stream()
                 .map(ds -> ds.getMetadata(key))
+                .filter(StringUtils::isNotEmpty)
+                .distinct()
                 .collect(Collectors.toList());
     }
 
     /**
-     * Get a list of unique groups of all data sources.
+     * Get a list of distinct group names of all data sources.
      *
-     * @return list of groups
+     * @return list of group names
      */
     public List<String> getGroups() {
         return dataSources.stream()
@@ -80,27 +88,46 @@
                 .collect(Collectors.toList());
     }
 
+    /**
+     * Returns the number of elements in this list.
+     *
+     * @return the number of elements in this list
+     */
     public int size() {
         return dataSources.size();
     }
 
+    /**
+     * Returns <tt>true</tt> if this list contains no elements.
+     *
+     * @return <tt>true</tt> if this list contains no elements
+     */
     public boolean isEmpty() {
         return dataSources.isEmpty();
     }
 
     /**
+     * Get an array representation of the underlying data sources.
+     *
+     * @return array of data sources
+     */
+    public DataSource[] toArray() {
+        return dataSources.toArray(new DataSource[0]);
+    }
+
+    /**
      * Get a list representation of the underlying data sources.
      *
      * @return list of data sources
      */
     public List<DataSource> toList() {
-        return new ArrayList<>(dataSources);
+        return dataSources;
     }
 
     /**
      * Get a map representation of the underlying data sources.
      * In <code>freemarker-cli</code> the map is also used to
-     * iterate over data source so we need to return a
+     * iterate over data source, so we need to return a
      * <code>LinkedHashMap</code>.
      * <p>
      * The implementation also throws as <code>IllegalStateException</code>
@@ -109,15 +136,15 @@
      * @return linked hasp map of data sources
      */
     public Map<String, DataSource> toMap() {
-        return dataSources.stream().collect(Collectors.toMap(
-                DataSource::getName,
-                identity(),
-                (ds1, ds2) -> {
-                    throw new IllegalStateException("Duplicate key detected when generating map: " + ds1 + ", " + ds2);
-                },
-                LinkedHashMap::new));
+        return dataSourcesMap;
     }
 
+    /**
+     * Returns the element at the specified position in this list.
+     *
+     * @param index index of the element to return
+     * @return the element at the specified position in this list
+     */
     public DataSource get(int index) {
         return dataSources.get(index);
     }
@@ -130,7 +157,7 @@
      * @return data source
      */
     public DataSource get(String name) {
-        final List<DataSource> list = find(name);
+        final List<DataSource> list = findByName(name);
 
         if (list.isEmpty()) {
             throw new IllegalArgumentException("Data source not found : " + name);
@@ -144,20 +171,18 @@
     }
 
     /**
-     * Find data sources based on their name using a wildcard string..
+     * Find data sources based on their name using a wildcard string.
      *
      * @param wildcard the wildcard string to match against
      * @return list of matching data sources
      * @see <a href="https://commons.apache.org/proper/commons-io/javadocs/api-2.7/org/apache/commons/io/FilenameUtils.html#wildcardMatch-java.lang.String-java.lang.String-">Apache Commons IO</a>
      */
-    public List<DataSource> find(String wildcard) {
-        return dataSources.stream()
-                .filter(dataSource -> dataSource.match("name", wildcard))
-                .collect(Collectors.toList());
+    public List<DataSource> findByName(String wildcard) {
+        return find(DataSource.METADATA_NAME, wildcard);
     }
 
     /**
-     * Find data sources based on their metadata key and wildcard string.
+     * Find <code>DataSources</code> based on their metadata key and wildcard string.
      *
      * @param key      metadata key to match
      * @param wildcard the wildcard string to match against
@@ -170,6 +195,32 @@
                 .collect(Collectors.toList());
     }
 
+    /**
+     * Create a new <code>DataSources</code> instance consisting of
+     * data sources matching the filter.
+     *
+     * @param key      metadata key to match
+     * @param wildcard the wildcard string to match against
+     * @return list of matching data sources
+     * @see <a href="https://commons.apache.org/proper/commons-io/javadocs/api-2.7/org/apache/commons/io/FilenameUtils.html#wildcardMatch-java.lang.String-java.lang.String-">Apache Commons IO</a>
+     */
+    public DataSources filter(String key, String wildcard) {
+        return new DataSources(find(key, wildcard));
+    }
+
+    /**
+     * Group the <code>DataSources</code> by a metadata value.
+     * @param key metadata key to group by
+     * @return groups of <code>DataSources</code>
+     */
+    public Map<String, DataSources> groupingBy(String key) {
+        final Function<DataSource, String> metadataFunction = dataSource -> dataSource.getMetadata(key);
+        return dataSources.stream()
+                .collect(Collectors.groupingBy(metadataFunction))
+                .entrySet().stream()
+                .collect(Collectors.toMap(Entry::getKey, p -> new DataSources(p.getValue())));
+    }
+
     @Override
     public void close() {
         dataSources.forEach(ClosableUtils::closeQuietly);
@@ -181,4 +232,21 @@
                 "dataSources=" + dataSources +
                 '}';
     }
+
+    private static List<DataSource> getNamedDataSources(Collection<DataSource> dataSources) {
+        return dataSources.stream()
+                .filter(dataSource -> StringUtils.isNotEmpty(dataSource.getName()))
+                .collect(Collectors.toList());
+    }
+
+    private Map<String, DataSource> dataSourcesMap(Collection<DataSource> dataSources) {
+        final List<DataSource> namedDataSources = getNamedDataSources(dataSources);
+        return namedDataSources.stream().collect(Collectors.toMap(
+                DataSource::getName,
+                identity(),
+                (ds1, ds2) -> {
+                    throw new IllegalStateException("Duplicate names detected when generating data source map: " + ds1 + ", " + ds2);
+                },
+                LinkedHashMap::new));
+    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
index cc919fc..7d84af5 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
@@ -17,11 +17,13 @@
 package org.apache.freemarker.generator.base.datasource;
 
 import org.apache.freemarker.generator.base.file.RecursiveFileSupplier;
+import org.apache.freemarker.generator.base.mime.MimetypesFileTypeMapFactory;
 import org.apache.freemarker.generator.base.uri.NamedUri;
 import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
-import org.apache.freemarker.generator.base.util.UriUtils;
+import org.apache.freemarker.generator.base.util.FileUtils;
 import org.apache.freemarker.generator.base.util.Validate;
 
+import javax.activation.FileDataSource;
 import java.io.File;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -34,10 +36,9 @@
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
-import static org.apache.freemarker.generator.base.datasource.DataSourceFactory.fromFile;
 
 /**
- * Create a list of <code>DataSource</code> based on a list URIs, directories and files.
+ * Supply a list of <code>DataSource</code> based on a list URIs, directories and files.
  */
 public class DataSourcesSupplier implements Supplier<List<DataSource>> {
 
@@ -110,16 +111,43 @@
     }
 
     private static List<DataSource> resolveFileOrDirectory(String source, String include, String exclude, Charset charset) {
-        final NamedUri namedUri = NamedUriStringParser.parse(source);
-        final String path = namedUri.getFile().getPath();
-        final String group = namedUri.getGroupOrDefault(DEFAULT_GROUP);
-        final Charset currCharset = getCharsetOrDefault(namedUri, charset);
-        final Map<String, String> parameters = namedUri.getParameters();
+        final NamedUri sourceUri = NamedUriStringParser.parse(source);
+        final String path = sourceUri.getFile().getPath();
+        final String group = sourceUri.getGroupOrDefault(DEFAULT_GROUP);
+        final Charset currCharset = getCharsetOrDefault(sourceUri, charset);
+        final Map<String, String> parameters = sourceUri.getParameters();
         return fileSupplier(path, include, exclude).get().stream()
-                .map(file -> fromFile(getDataSourceName(namedUri, file), group, file, currCharset, parameters))
+                .map(file -> fromFile(sourceUri, getDataSourceName(sourceUri, file), group, file, currCharset, parameters))
                 .collect(toList());
     }
 
+    private static DataSource fromFile(
+            NamedUri sourceUri,
+            String name,
+            String group,
+            File file,
+            Charset charset,
+            Map<String, String> properties) {
+        Validate.isTrue(file.exists(), "File not found: " + file);
+
+        final FileDataSource dataSource = new FileDataSource(file);
+        // content type is determined from file extension
+        dataSource.setFileTypeMap(MimetypesFileTypeMapFactory.create());
+        final String relativePath = FileUtils.getRelativePath(sourceUri.getFile(), file);
+        final String contentType = dataSource.getContentType();
+
+        return DataSource.builder()
+                .name(name)
+                .group(group)
+                .uri(file.toURI())
+                .dataSource(dataSource)
+                .relativeFilePath(relativePath)
+                .contentType(contentType)
+                .charset(charset)
+                .properties(properties)
+                .build();
+    }
+
     private static RecursiveFileSupplier fileSupplier(String source, String include, String exclude) {
         return new RecursiveFileSupplier(singletonList(source), singletonList(include), singletonList(exclude));
     }
@@ -140,7 +168,7 @@
         if (namedUri.hasName()) {
             return namedUri.getName();
         } else {
-            return UriUtils.toStringWithoutFragment(file.toURI());
+            return file.getAbsolutePath();
         }
     }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
index bc89df3..fb4b9b7 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/file/RecursiveFileSupplier.java
@@ -16,11 +16,14 @@
  */
 package org.apache.freemarker.generator.base.file;
 
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.filefilter.AndFileFilter;
+import org.apache.commons.io.filefilter.HiddenFileFilter;
 import org.apache.commons.io.filefilter.IOFileFilter;
 import org.apache.commons.io.filefilter.NotFileFilter;
 import org.apache.commons.io.filefilter.OrFileFilter;
 import org.apache.commons.io.filefilter.WildcardFileFilter;
+import org.apache.freemarker.generator.base.util.StringUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -32,10 +35,6 @@
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static java.util.stream.Collectors.toList;
-import static org.apache.commons.io.FileUtils.listFiles;
-import static org.apache.commons.io.filefilter.HiddenFileFilter.HIDDEN;
-import static org.apache.commons.io.filefilter.HiddenFileFilter.VISIBLE;
-import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 
 /**
  * Resolve a list of files or directories recursively.
@@ -94,7 +93,7 @@
     }
 
     private List<File> resolveDirectory(File directory) {
-        return new ArrayList<>(listFiles(directory, fileFilter, directoryFilter));
+        return new ArrayList<>(FileUtils.listFiles(directory, fileFilter, directoryFilter));
     }
 
     private static IOFileFilter fileFilter(Collection<String> includes, Collection<String> excludes) {
@@ -113,7 +112,10 @@
     }
 
     private static IOFileFilter includeFilter(String include) {
-        return isEmpty(include) ? VISIBLE : new AndFileFilter(new WildcardFileFilter(include), VISIBLE);
+        return StringUtils.isEmpty(include) ?
+                HiddenFileFilter.VISIBLE :
+                new AndFileFilter(
+                        new WildcardFileFilter(include), HiddenFileFilter.VISIBLE);
     }
 
     private static List<IOFileFilter> excludeFilters(Collection<String> excludes) {
@@ -125,12 +127,14 @@
     }
 
     private static IOFileFilter excludeFilter(String exclude) {
-        return isEmpty(exclude) ?
-                VISIBLE :
-                new NotFileFilter(new OrFileFilter(new WildcardFileFilter(exclude), HIDDEN));
+        return StringUtils.isEmpty(exclude) ?
+                HiddenFileFilter.VISIBLE :
+                new NotFileFilter(
+                        new OrFileFilter(
+                                new WildcardFileFilter(exclude), HiddenFileFilter.HIDDEN));
     }
 
     private static IOFileFilter directoryFilter() {
-        return VISIBLE;
+        return HiddenFileFilter.VISIBLE;
     }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/Mimetypes.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/Mimetypes.java
index c8bb76c..6f70b69 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/Mimetypes.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/Mimetypes.java
@@ -31,6 +31,9 @@
     public static final String MIME_TEXT_TSV = "text/tab-separated-values";
     public static final String MIME_TEXT_YAML = "text/yaml";
 
+    public static final String MIME_TYPE_ZIP = "application/zip";
+    public static final String MIME_TYPE_GZIP = "application/gzip";
+
     public static final String MIME_VENDOR_MS_EXCEL = "application/vnd.ms-excel xls XLS";
     public static final String MIME_VENDOR_OPEN_XML_SPREADSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx XLSX\"";
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
index f8bbf2a..9445fe9 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
@@ -29,10 +29,12 @@
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_RTF;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_TSV;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_YAML;
+import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TYPE_GZIP;
+import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TYPE_ZIP;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_VENDOR_MS_EXCEL;
 import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_VENDOR_OPEN_XML_SPREADSHEET;
 
-public class MimetypesFileTypeMapFactory {
+public abstract class MimetypesFileTypeMapFactory {
 
     private static MimetypesFileTypeMap mimeTypes;
 
@@ -52,6 +54,8 @@
             mimeTypes.addMimeTypes(MIME_TEXT_RTF + " rtf RTF");
             mimeTypes.addMimeTypes(MIME_TEXT_TSV + " tsv TSV");
             mimeTypes.addMimeTypes(MIME_TEXT_YAML + " yml YML yaml YAML");
+            mimeTypes.addMimeTypes(MIME_TYPE_GZIP + " gzip GZIP");
+            mimeTypes.addMimeTypes(MIME_TYPE_ZIP + " zip ZIP");
         }
 
         return mimeTypes;
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/output/OutputGenerator.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/output/OutputGenerator.java
index 5ce4cf4..ebdd409 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/output/OutputGenerator.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/output/OutputGenerator.java
@@ -26,10 +26,16 @@
 import static java.util.Objects.requireNonNull;
 
 /**
- * Information about loading templates and writing their output.
+ * Information about loading templates and writing their output. An instance
+ * describes a transformation of 0..N data sources to an output file.
  */
 public class OutputGenerator {
 
+    public enum SeedType {
+        DATASOURCE,
+        TEMPLATE
+    }
+
     /** Source of template */
     private final TemplateSource templateSource;
 
@@ -42,15 +48,20 @@
     /** Variables (as a map) used for the transformation */
     private final Map<String, Object> variables;
 
+    /** Seed type of "DATASOURCE* only takes a single data soure */
+    private final SeedType seedType;
+
     public OutputGenerator(
             TemplateSource templateSource,
             TemplateOutput templateOutput,
             List<DataSource> dataSources,
-            Map<String, Object> variables) {
+            Map<String, Object> variables,
+            SeedType seedType) {
         this.templateSource = requireNonNull(templateSource);
         this.templateOutput = requireNonNull(templateOutput);
         this.dataSources = requireNonNull(dataSources);
         this.variables = requireNonNull(variables);
+        this.seedType = requireNonNull(seedType);
     }
 
     public TemplateSource getTemplateSource() {
@@ -69,6 +80,10 @@
         return variables;
     }
 
+    public SeedType getSeedType() {
+        return seedType;
+    }
+
     @Override
     public String toString() {
         return "OutputGenerator{" +
@@ -76,6 +91,7 @@
                 ", templateOutput=" + templateOutput +
                 ", dataSources=" + dataSources +
                 ", variables=" + variables +
+                ", seedType=" + seedType +
                 '}';
     }
 }
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
index 1abf21b..b1710b1 100644
--- 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
@@ -141,7 +141,7 @@
 
     /**
      * Create a table from a list of rows representing tabular data
-     * where the first row may consists of column headers.
+     * where the first row may consist of column headers.
      *
      * @param rows                      row values
      * @param withFirstRowAsColumnNames column names as first row?
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/FileUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/FileUtils.java
new file mode 100644
index 0000000..c850d1e
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/FileUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileUtils {
+
+    /**
+     * Determines the relative path between a directory and a file within the directory (excluding the file name).
+     *
+     * @param directory the directory
+     * @param file      the file
+     * @return relative path or an empty string if no relative path exists
+     */
+    public static String getRelativePath(File directory, File file) {
+        Validate.notNull(directory, "directory is null");
+        Validate.notNull(file, "file is null");
+        Validate.isTrue(directory.exists(), "directory does not exist");
+
+        final Path filePath = Paths.get(file.toURI()).normalize();
+        final Path directoryPath = Paths.get(directory.toURI()).normalize();
+        final String relativePath = directoryPath.relativize(filePath).normalize().toString();
+
+        // strip last path segment
+        if (relativePath.lastIndexOf(File.separatorChar) >= 0) {
+            return relativePath.substring(0, relativePath.lastIndexOf(File.separatorChar));
+        } else {
+            return "";
+        }
+    }
+}
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
index 4ec6fbc..08ccd5b 100644
--- 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
@@ -17,8 +17,11 @@
 package org.apache.freemarker.generator.base.util;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class ListUtils {
 
@@ -57,10 +60,24 @@
      * @param <T>  the type of the array
      * @return copied array
      */
-    public static <T> T coalesce(List<T> list) {
+    public static <T> T coalesce(Collection<T> list) {
         return list.stream()
                 .filter(Objects::nonNull)
                 .findFirst()
                 .orElse(null);
     }
+
+    /**
+     * Concatenate an array of lists.
+     *
+     * @param lists array of lists
+     * @param <T>   the type of the array
+     * @return concatenated list
+     */
+    @SafeVarargs
+    public static <T> List<T> concatenate(Collection<T>... lists) {
+        return Stream.of(lists)
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
+    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/NonClosableWriterWrapper.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/NonClosableWriterWrapper.java
index e2637b7..cd48b5b 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/NonClosableWriterWrapper.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/NonClosableWriterWrapper.java
@@ -22,7 +22,7 @@
 import static java.util.Objects.requireNonNull;
 
 /**
- * Wraps a writer (usually the internal FreeMarker's writer instance
+ * Wraps a writer (usually the internal FreeMarker's writer instance)
  * and avoids closing it since this would crash FreeMarker. E.g. the
  * Commons CSV integration uses the FreeMarker writer directly but
  * some implementation could call "CSVPrinter#close"
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
index 180b7ac..02158cb 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
@@ -19,7 +19,7 @@
 import java.util.Locale;
 
 /**
- * Helper class to detect the operting system (mostly Windows).
+ * Helper class to detect the operating system.
  */
 public class OperatingSystem {
     private static final String OS = System.getProperty("os.name", "unknown").toLowerCase(Locale.ROOT);
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
index 90b2f6a..97ed173 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
@@ -24,7 +24,8 @@
 public class PropertiesTransformer {
 
     /**
-     * Create a new <code>java.util.Properties</code> instance having only key with the prefix.
+     * Create a new <code>java.util.Properties</code> instance having only key with the
+     * given prefix.
      *
      * @param properties the properties
      * @param prefix     prefix
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
index c3eabf1..c41fefa 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
@@ -30,6 +30,10 @@
         return value != null && value.trim().isEmpty() ? null : value;
     }
 
+    public static String nullToEmpty(String value) {
+        return isEmpty(value) ? "" : value;
+    }
+
     public static String firstNonEmpty(final String... values) {
         if (values != null) {
             for (final String value : values) {
@@ -41,6 +45,10 @@
         return null;
     }
 
+    public static String trim(String value) {
+        return value != null ? value.trim() : null;
+    }
+
     public static int count(final String s, final char c) {
         final char[] chars = s.toCharArray();
         int count = 0;
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
index e1a8f80..cb7b252 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
@@ -54,6 +54,7 @@
             assertEquals("", dataSource.getBaseName());
             assertEquals("", dataSource.getExtension());
             assertTrue(dataSource.getUri().toString().startsWith("string:///"));
+            assertEquals("", dataSource.getRelativeFilePath());
             assertEquals(UTF_8, dataSource.getCharset());
             assertEquals("text/plain", dataSource.getContentType());
             assertTrue(dataSource.getLength() > 0);
@@ -72,13 +73,14 @@
             assertEquals("xml", dataSource.getExtension());
             assertEquals(ANY_FILE.toURI().toString(), dataSource.getUri().toString());
             assertEquals(ANY_CHAR_SET.name(), dataSource.getCharset().name());
+            assertEquals("", dataSource.getRelativeFilePath());
             assertEquals("application/xml", dataSource.getContentType());
             assertTrue(dataSource.getLength() > 0);
             assertFalse(dataSource.getText().isEmpty());
             assertTrue(dataSource.match("name", "*" + ANY_FILE_NAME));
             assertTrue(dataSource.match("uri", "file:/*/pom.xml"));
             assertTrue(dataSource.match("extension", "xml"));
-            assertTrue(dataSource.match("basename", "pom"));
+            assertTrue(dataSource.match("baseName", "pom"));
         }
     }
 
@@ -91,6 +93,7 @@
             assertEquals("", dataSource.getBaseName());
             assertEquals("", dataSource.getExtension());
             assertEquals("https://www.google.com/?foo=bar", dataSource.getUri().toString());
+            assertEquals("", dataSource.getRelativeFilePath());
             assertEquals("text/html; charset=ISO-8859-1", dataSource.getContentType());
             assertEquals(MIME_TEXT_HTML, dataSource.getMimeType());
             assertEquals("ISO-8859-1", dataSource.getCharset().name());
@@ -128,15 +131,18 @@
         try (DataSource dataSource = stringDataSource()) {
             final Map<String, String> metadata = dataSource.getMetadata();
 
-            assertEquals(8, metadata.size());
-            assertEquals("", metadata.get("basename"));
+            assertEquals(10, metadata.size());
+            assertEquals("", metadata.get("baseName"));
             assertEquals("", metadata.get("extension"));
-            assertEquals("", metadata.get("filename"));
-            assertEquals("/", metadata.get("filepath"));
+            assertEquals("", metadata.get("fileName"));
+            assertEquals("", metadata.get("filePath"));
+            assertEquals("", metadata.get("relativeFilePath"));
+
             assertEquals("default", metadata.get("group"));
             assertEquals("stdin", metadata.get("name"));
             assertTrue(metadata.get("uri").startsWith("string://"));
-            assertEquals("text/plain", metadata.get("mimetype"));
+            assertEquals("text/plain", metadata.get("mimeType"));
+            assertEquals("UTF-8", metadata.get("charset"));
         }
     }
 
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
index 85bdedc..9885a81 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
@@ -25,6 +25,7 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.HashMap;
+import java.util.Map;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
@@ -45,28 +46,32 @@
     private static final String GROUP_PART = "group";
 
     @Test
-    public void shouldFindByName() {
+    public void shouldFindDataSourcesByName() {
         try (DataSources dataSources = dataSources()) {
-            assertEquals(0, dataSources.find(null).size());
-            assertEquals(0, dataSources.find("").size());
-            assertEquals(0, dataSources.find("*.bar").size());
-            assertEquals(0, dataSources.find("foo.*").size());
-            assertEquals(0, dataSources.find("foo.bar").size());
+            assertEquals(0, dataSources.findByName(null).size());
+            assertEquals(0, dataSources.findByName("").size());
+            assertEquals(0, dataSources.findByName("*.bar").size());
+            assertEquals(0, dataSources.findByName("foo.*").size());
+            assertEquals(0, dataSources.findByName("foo.bar").size());
 
-            assertEquals(2, dataSources.find("*.*").size());
-            assertEquals(1, dataSources.find("*." + ANY_FILE_EXTENSION).size());
-            assertEquals(1, dataSources.find("*.???").size());
-            assertEquals(1, dataSources.find("*om*").size());
-            assertEquals(1, dataSources.find("*o*.xml").size());
+            assertEquals(2, dataSources.findByName("*.*").size());
+            assertEquals(1, dataSources.findByName("*." + ANY_FILE_EXTENSION).size());
+            assertEquals(1, dataSources.findByName("*.???").size());
+            assertEquals(1, dataSources.findByName("*om*").size());
+            assertEquals(1, dataSources.findByName("*o*.xml").size());
 
-            assertEquals(3, dataSources.find("*").size());
+            assertEquals(3, dataSources.findByName("*").size());
+
+            assertEquals(2, dataSources.findByName("!pom.xml").size());
+            assertEquals(3, dataSources.findByName("!").size());
+            assertEquals(0, dataSources.findByName("!*").size());
+            assertEquals(1, dataSources.findByName("!*.*").size());
         }
     }
 
     @Test
-    public void shouldFindByGroupPart() {
+    public void shouldFindDataSourcesByMetadata() {
         try (DataSources dataSources = dataSources()) {
-
             assertEquals(0, dataSources.find(GROUP_PART, null).size());
             assertEquals(0, dataSources.find(GROUP_PART, "").size());
 
@@ -76,6 +81,10 @@
             assertEquals(3, dataSources.find(GROUP_PART, "default").size());
             assertEquals(3, dataSources.find(GROUP_PART, "d*").size());
             assertEquals(3, dataSources.find(GROUP_PART, "d??????").size());
+
+            assertEquals(0, dataSources.find(GROUP_PART, "!*").size());
+            assertEquals(3, dataSources.find(GROUP_PART, "!unknown").size());
+            assertEquals(0, dataSources.find(GROUP_PART, "!default").size());
         }
     }
 
@@ -87,10 +96,10 @@
     @Test
     public void shouldGetAllDataSource() {
         try (DataSources dataSources = dataSources()) {
-
             assertEquals("unknown", dataSources.get(0).getName());
             assertEquals("pom.xml", dataSources.get(1).getName());
             assertEquals("server.invalid?foo=bar", dataSources.get(2).getName());
+            assertEquals(3, dataSources.toArray().length);
             assertEquals(3, dataSources.toList().size());
             assertEquals(3, dataSources.toMap().size());
             assertEquals(3, dataSources.size());
@@ -100,9 +109,9 @@
 
     @Test
     public void shouldGetMetadataParts() {
-        assertEquals(asList("", "pom.xml", ""), dataSources().getMetadata("filename"));
-        assertEquals(asList("default", "default", "default"), dataSources().getMetadata("group"));
-        assertEquals(asList("", "xml", ""), dataSources().getMetadata("extension"));
+        assertEquals(asList("pom.xml"), dataSources().getMetadata("fileName"));
+        assertEquals(asList("default"), dataSources().getMetadata("group"));
+        assertEquals(asList("xml"), dataSources().getMetadata("extension"));
         assertEquals(asList("unknown", "pom.xml", "server.invalid?foo=bar"), dataSources().getMetadata("name"));
     }
 
@@ -111,6 +120,36 @@
         assertEquals(singletonList(DEFAULT_GROUP), dataSources().getGroups());
     }
 
+    @Test
+    public void shouldSupportGroupingByMetadata() {
+        try (DataSources dataSources = dataSources()) {
+            final Map<String, DataSources> map = dataSources.groupingBy("mimeType");
+
+            assertEquals(2, map.size());
+            assertEquals(1, map.get("application/xml").size());
+            assertEquals(2, map.get("text/plain").size());
+        }
+    }
+
+    @Test
+    public void shouldSupportFilteringByMetadata() {
+        try (DataSources dataSources = dataSources().filter("mimeType", "text/plain")) {
+            assertEquals(2, dataSources.size());
+            assertEquals("text/plain", dataSources.get(0).getMimeType());
+            assertEquals("text/plain", dataSources.get(1).getMimeType());
+        }
+
+        try (DataSources dataSources = dataSources().filter("mimeType", "application/xml")) {
+            assertEquals(1, dataSources.size());
+            assertEquals("application/xml", dataSources.get(0).getMimeType());
+        }
+
+        try (DataSources dataSources = dataSources().filter("mimeType", "!text/plain")) {
+            assertEquals(1, dataSources.size());
+            assertEquals("application/xml", dataSources.get(0).getMimeType());
+        }
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void shouldThrowExceptionWhenGetDoesNotFindDataSource() {
         dataSources().get("file-does-not-exist");
@@ -134,7 +173,7 @@
     }
 
     private static DataSource urlDataSource() {
-        return DataSourceFactory.fromUrl("server.invalid?foo=bar", "default", toUrl(ANY_URL), "plain/text", UTF_8, new HashMap<>());
+        return DataSourceFactory.fromUrl("server.invalid?foo=bar", "default", toUrl(ANY_URL), "text/plain", UTF_8, new HashMap<>());
     }
 
     private static URL toUrl(String value) {
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/FileUtilsTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/FileUtilsTest.java
new file mode 100644
index 0000000..f799c6b
--- /dev/null
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/FileUtilsTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.util;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.freemarker.generator.base.util.FileUtils;
+import org.apache.freemarker.generator.base.util.OperatingSystem;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.apache.freemarker.generator.base.util.FileUtils.getRelativePath;
+import static org.junit.Assert.assertEquals;
+
+public class FileUtilsTest {
+
+    @Test
+    public void shouldGetRelativePathForSameDirectory() {
+        assertEquals("", getRelativePath(new File("/"), new File("/pom.xml")));
+        assertEquals("", getRelativePath(new File("."), new File("pom.xml")));
+        assertEquals("", getRelativePath(new File("."), new File("./pom.xml")));
+        assertEquals("", getRelativePath(new File("."), new File("./pom.xml")));
+        assertEquals("", getRelativePath(new File("../freemarker-generator-base"), new File("pom.xml")));
+        assertEquals("", getRelativePath(new File("../freemarker-generator-base"), new File("./pom.xml")));
+        assertEquals("", getRelativePath(new File("../freemarker-generator-base"), new File("../freemarker-generator-base/pom.xml")));
+    }
+
+    @Test
+    public void shouldGetRelativePathForNestedDirectory() {
+        assertEquals(fixSeparators("src/test/data/env"), getRelativePath(new File("."), new File("src/test/data/env/nginx.env")));
+        assertEquals(fixSeparators("src/test/data/env"), getRelativePath(new File("."), new File("./src/test/data/env/nginx.env")));
+    }
+
+    @Test
+    public void shouldGetRelativePathForDisjunctDirectories() {
+        assertEquals(fixSeparators("../test/data/env"), getRelativePath(new File("./src/site"), new File("src/test/data/env/nginx.env")));
+    }
+
+    @Test
+    public void shouldHandleInvalidArgumentsGracefully() {
+        assertEquals("", getRelativePath(new File("."), new File(".")));
+        assertEquals("", getRelativePath(new File("pom.xml"), new File("pom.xml")));
+        assertEquals("", getRelativePath(new File("."), new File("does-not-exist.xml")));
+        assertEquals("foo", getRelativePath(new File("."), new File("foo/does-not-exist.xml")));
+        assertEquals("foo", getRelativePath(new File("."), new File("foo/./does-not-exist.xml")));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldThrowIllegalArgumentExceptionForNonExistingDirectoy() {
+        getRelativePath(new File("does-not-exit"), new File("pom.xml"));
+    }
+
+    private static String fixSeparators(String str) {
+        if (OperatingSystem.isWindows()) {
+            return FilenameUtils.separatorsToWindows(str);
+        } else {
+            return str;
+        }
+    }
+}
diff --git a/freemarker-generator-cli/CHANGELOG.md b/freemarker-generator-cli/CHANGELOG.md
index 993890c..c440886 100644
--- a/freemarker-generator-cli/CHANGELOG.md
+++ b/freemarker-generator-cli/CHANGELOG.md
@@ -2,9 +2,19 @@
 
 All notable changes to this project will be documented in this file. We try to adhere to https://github.com/olivierlacan/keep-a-changelog.
 
-## 0.1.0-SNAPSHOT
+## 0.2.0-SNAPSHOT
 
 ### Added
+* [FREEMARKER-199] Add [utah-parser-tool](https://github.com/sonalake/utah-parser) to parse semi-structured text.
+
+### Changed
+* [FREEMARKER-195] Improve exposure of DataSources using TemplateHashModelEx2
+
+## 0.1.0-SNAPSHOT (unreleased)
+
+### Added
+* Use `-Xverify:none -XX:TieredStopAtLevel=1` to improve startup time of CLI
+* [FREEMARKER-188] Support an output "generation" mode to create an output for each `DataSource`
 * [FREEMARKER-181] Support custom pattern definitions for Grok tool
 * Parse a list of `DataSources` for the various tools
 * [FREEMARKER-161] Allow multiple transformations on the CLI
@@ -20,7 +30,6 @@
 * [FREEMARKER-129] Migrate `freemarker-cli` into `freemarker-generator` project (see [https://github.com/sgoeschl/freemarker-cli](https://github.com/sgoeschl/freemarker-cli))
 * [FREEMARKER-129] Provide a `toString()` method for all tools
 
-### Changed
 * [FREEMARKER-182] Upgrade to Apache FreeMarker 2.3.31
 * [FREEMARKER-175] Use latest FreeMarker version
 * [FREEMARKER-173] Allow to pass arbitrary key/value pairs to DataSource when using NamedURIs
@@ -82,4 +91,7 @@
 [FREEMARKER-176]: https://issues.apache.org/jira/browse/FREEMARKER-176
 [FREEMARKER-181]: https://issues.apache.org/jira/browse/FREEMARKER-181
 [FREEMARKER-182]: https://issues.apache.org/jira/browse/FREEMARKER-182
+[FREEMARKER-188]: https://issues.apache.org/jira/browse/FREEMARKER-188
+[FREEMARKER-195]: https://issues.apache.org/jira/browse/FREEMARKER-195
+[FREEMARKER-199]: https://issues.apache.org/jira/browse/FREEMARKER-199
 
diff --git a/freemarker-generator-cli/pom.xml b/freemarker-generator-cli/pom.xml
index 4984b36..05497dc 100644
--- a/freemarker-generator-cli/pom.xml
+++ b/freemarker-generator-cli/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>freemarker-generator-cli</artifactId>
@@ -46,7 +46,13 @@
                 <configuration>
                     <repositoryLayout>flat</repositoryLayout>
                     <repositoryName>lib</repositoryName>
-                    <extraJvmArguments>-Xms64m -Xmx512m</extraJvmArguments>
+                    <!--
+                        See https://ryanharrison.co.uk/2018/04/29/faster-java-startup-time.html
+                        "-Xverify:none -XX:TieredStopAtLevel=1" improves startup time by 50%
+                        See https://www.eclipse.org/openj9/docs/xverify/
+                        "-Xverifiy:none" is deprecated in Java 13
+                     -->
+                    <extraJvmArguments>-Xms64m -Xmx512m -XX:TieredStopAtLevel=1</extraJvmArguments>
                     <endorsedDir>endorsed</endorsedDir>
                     <useWildcardClassPath>true</useWildcardClassPath>
                     <configurationDirectory>config</configurationDirectory>
diff --git a/freemarker-generator-cli/src/app/config/freemarker-generator.properties b/freemarker-generator-cli/src/app/config/freemarker-generator.properties
index 25b7029..d65937b 100644
--- a/freemarker-generator-cli/src/app/config/freemarker-generator.properties
+++ b/freemarker-generator-cli/src/app/config/freemarker-generator.properties
@@ -34,6 +34,7 @@
 freemarker.tools.jsoup=org.apache.freemarker.generator.tools.jsoup.JsoupTool
 freemarker.tools.properties=org.apache.freemarker.generator.tools.properties.PropertiesTool
 freemarker.tools.system=org.apache.freemarker.generator.tools.system.SystemTool
+freemarker.tools.utahparser=org.apache.freemarker.generator.tools.utahparser.UtahParserTool
 freemarker.tools.uuid=org.apache.freemarker.generator.tools.uuid.UUIDTool
 freemarker.tools.xml=org.apache.freemarker.generator.tools.xml.XmlTool
 freemarker.tools.yaml=org.apache.freemarker.generator.tools.snakeyaml.SnakeYamlTool
diff --git a/freemarker-generator-cli/src/app/examples/data/ftl/nginx/nginx.conf.ftl b/freemarker-generator-cli/src/app/examples/data/ftl/nginx/nginx.conf.ftl
index 3cbf310..d74b193 100644
--- a/freemarker-generator-cli/src/app/examples/data/ftl/nginx/nginx.conf.ftl
+++ b/freemarker-generator-cli/src/app/examples/data/ftl/nginx/nginx.conf.ftl
@@ -1,4 +1,4 @@
-<#assign env = tools.properties.parse(dataSources?values[0])>
+<#assign env = tools.properties.parse(dataSources[0])>
 
 server {
   listen 80;
diff --git a/freemarker-generator-cli/src/app/examples/data/nginx/test1-api.nginx b/freemarker-generator-cli/src/app/examples/data/nginx/test1-api.nginx
new file mode 100644
index 0000000..2c2486f
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/data/nginx/test1-api.nginx
@@ -0,0 +1,30 @@
+server {
+  listen               443 ssl;
+  server_name          test1-api.company.org;
+
+  access_log           /var/log/nginx/test1-api.access.log;
+  error_log            /var/log/nginx/test1-api.error.log;
+
+  ssl_certificate      /etc/nginx/ssl/wildcard.company.org-public.crt;
+  ssl_certificate_key  /etc/nginx/ssl/wildcard.company.org-private.rsa;
+
+  include              /etc/nginx/includes/FLKPMM.nginx;
+
+  proxy_buffers        16 64k;
+  proxy_buffer_size    128k;
+  proxy_next_upstream  error timeout invalid_header http_500 http_502 http_503 http_504;
+  proxy_redirect       off;
+  proxy_set_header     Host              $host;
+  proxy_set_header     X-Real-IP         $remote_addr;
+  proxy_set_header     X-Forwarded-For   $proxy_add_x_forwarded_for;
+  proxy_set_header     X-Forwarded-Proto https;
+  proxy_set_header     X-TPP-QWAC        $ssl_client_cert;
+
+  ssl_verify_client    optional_no_ca;
+
+
+  location / {
+    return 403;
+  }
+
+}
diff --git a/freemarker-generator-cli/src/app/examples/data/nginx/test1-application.nginx b/freemarker-generator-cli/src/app/examples/data/nginx/test1-application.nginx
new file mode 100644
index 0000000..b618991
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/data/nginx/test1-application.nginx
@@ -0,0 +1,27 @@
+server {
+  listen 443 ssl;
+  server_name test1-application.company.org;
+
+  access_log /var/log/nginx/test1-application.access.log;
+  error_log /var/log/nginx/test1-application.error.log;
+
+  ssl_certificate /etc/nginx/ssl/wildcard.company.org-public.crt;
+  ssl_certificate_key /etc/nginx/ssl/wildcard.company.org-private.rsa;
+
+  proxy_buffers 16 64k;
+  proxy_buffer_size 128k;
+
+  location / {
+    proxy_pass https://osroutercpssl/;
+    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
+    proxy_redirect off;
+
+    proxy_set_header    Host              $host;
+    proxy_set_header    X-Real-IP         $remote_addr;
+    proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
+    proxy_set_header    X-Forwarded-Proto https;
+  }
+
+}
+
+
diff --git a/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_example.txt b/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_example.txt
new file mode 100644
index 0000000..c4a209e
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_example.txt
@@ -0,0 +1,12 @@
+Groups: 3 Peers: 3 Down peers: 0
+Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
+inet.0               947        310          0          0          0          0
+inet6.0              849        807          0          0          0          0
+Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Damped...
+10.247.68.182         65550     131725   28179233       0      11     6w3d17h Establ
+  inet.0: 4/5/1
+  inet6.0: 0/0/0
+10.254.166.246        65550     136159   29104942       0       0      6w5d6h Establ
+  inet.0: 0/0/0
+  inet6.0: 7/8/1
+192.0.2.100           65551    1269381    1363320       0       1      9w5d6h 1/2/3 4/5/6
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_template.xml b/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_template.xml
new file mode 100644
index 0000000..76084aa
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/data/text/utahparser/juniper_bgp_summary_template.xml
@@ -0,0 +1,83 @@
+<!--
+   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.
+-->
+<config>
+    <searches>
+
+        <!-- in this case, we have a CSV (space delimited file) so we define the line once, and then reuse it over
+        and again for each value -->
+        <search id="QUERY-LINE"><![CDATA[\s*{ipAddress}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbersThenText}]]></search>
+        <search id="inetInline"><![CDATA[{inet} {inet}]]></search>
+        <search id="inet4"><![CDATA[inet.0:\s*{inet}]]></search>
+        <search id="inet6"><![CDATA[inet6.0:\s*{inet}]]></search>
+        <search id="inet"><![CDATA[{numbers}/{numbers}/{numbers}]]></search>
+
+        <!-- Some rules for finding text, to make the values a little easier below -->
+        <search id="numbers"><![CDATA[(\d+)]]></search>
+        <search id="numbersThenText"><![CDATA[(\d+\S+)]]></search>
+        <search id="string"><![CDATA[(\S+?)]]></search>
+        <search id="ipAddress"><![CDATA[(\d+(\.\d+){3})]]></search>
+        <search id="EOL"><![CDATA[[\n\r]]]></search>
+    </searches>
+
+    <!-- the record starts with a line with an ip address and ends with either an inet6 line, or where the ids are at
+    the end of the line-->
+    <delim retain="true">{ipAddress}.*(\/\d+)\s*{EOL}</delim>
+    <delim>\s*({inet6})</delim>
+
+    <!--
+    This is the last line of the header
+     -->
+    <header-delim><![CDATA[Peer\s+AS\s+InPkt]]></header-delim>
+
+    <!--
+    Files look like this:
+
+    10.247.68.182         65550     131725   28179233       0      11     6w3d17h Establ
+      inet.0: 4/5/1
+      inet6.0: 0/0/0
+
+      or
+
+    192.0.2.100           65551    1269381    1363320       0       1      9w5d6h 2/3/0 0/0/0
+  -->
+    <values>
+        <!-- here we reuse the line pattern, only we pull out different group values -->
+        <value id="remoteIp" group="1"><![CDATA[{QUERY-LINE}]]></value>
+        <value id="uptime" group="8"><![CDATA[{QUERY-LINE}]]></value>
+
+        <!-- here we check for values in the inet* lines and use these -->
+        <value id="activeV4" group="1"><![CDATA[{inet4}]]></value>
+        <value id="receivedV4" group="2"><![CDATA[{inet4}]]></value>
+        <value id="accepted_V4" group="3"><![CDATA[{inet4}]]></value>
+
+        <value id="activeV6" group="1"><![CDATA[{inet6}]]></value>
+        <value id="receivedV6" group="2"><![CDATA[{inet6}]]></value>
+        <value id="accepted_V6" group="3"><![CDATA[{inet6}]]></value>
+
+        <!--
+        here we check for values at the end of the query line, and use these
+         NOTE: since we only set non-null values, these will not overwrite any values set above
+        -->
+        <value id="activeV4" group="9"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="receivedV4" group="10"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="accepted_V4" group="11"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="activeV6" group="12"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="receivedV6" group="13"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="accepted_V6" group="14"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+
+    </values>
+</config>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/templates/accesslog/combined-access.ftl b/freemarker-generator-cli/src/app/examples/templates/accesslog/combined-access.ftl
index 944a826..28f7f71 100644
--- a/freemarker-generator-cli/src/app/examples/templates/accesslog/combined-access.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/accesslog/combined-access.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#assign grok = tools.grok.create("%{COMBINEDAPACHELOG}")>
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign lines = dataSource.getLineIterator()>
 
 <#compress>
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/csv/gatling-user-credentials.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/csv/gatling-user-credentials.ftl
index aae83f4..46d505b 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/csv/gatling-user-credentials.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/csv/gatling-user-credentials.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withDelimiter(';')>
 <#assign csvParser = tools.csv.parse(dataSource, cvsFormat)>
 <#assign csvRecords = csvParser.records>
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/fo/transactions.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/fo/transactions.ftl
index 9a513ca..089790f 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/fo/transactions.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/fo/transactions.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withDelimiter('\t').withHeader()>
 <#assign csvParser = tools.csv.parse(dataSource, cvsFormat)>
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/fo/transform.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/fo/transform.ftl
index 7e77af2..8f49b72 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/fo/transform.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/fo/transform.ftl
@@ -17,7 +17,7 @@
 -->
 <#assign csvFormatName = CVS_IN_FORMAT!"DEFAULT">
 <#assign cvsFormat = tools.csv.formats[csvFormatName].withHeader()>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign csvHeaders = csvParser.getHeaderMap()?keys>
 <#assign csvRecords = csvParser.records>
 <#--------------------------------------------------------------------------->
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/html/transactions.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/html/transactions.ftl
index 030cd82..c2eaa05 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/html/transactions.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/html/transactions.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withDelimiter('\t').withHeader()>
 <#assign csvParser = tools.csv.parse(dataSource, cvsFormat)>
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/md/filter.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/md/filter.ftl
index 49beb57..acec8b3 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/md/filter.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/md/filter.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign parser = parser(dataSource)>
 <#assign headers = parser.getHeaderNames()>
 <#assign column = tools.system.getParameter("column")>
diff --git a/freemarker-generator-cli/src/app/examples/templates/csv/shell/curl.ftl b/freemarker-generator-cli/src/app/examples/templates/csv/shell/curl.ftl
index 114dd4f..4a18ecf 100644
--- a/freemarker-generator-cli/src/app/examples/templates/csv/shell/curl.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/csv/shell/curl.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withHeader()>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign records = csvParser.records>
 <#assign csvMap = tools.csv.toMap(records, "disposer")>
 <#--------------------------------------------------------------------------->
diff --git a/freemarker-generator-cli/src/app/examples/templates/dataframe/example.ftl b/freemarker-generator-cli/src/app/examples/templates/dataframe/example.ftl
index 3b154ea..7cbf309 100644
--- a/freemarker-generator-cli/src/app/examples/templates/dataframe/example.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/dataframe/example.ftl
@@ -14,7 +14,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign csvParser = tools.csv.parse(dataSource, tools.csv.formats.DATAFRAME)>
 <#assign users = tools.dataframe.fromCSVParser(csvParser)>
 
diff --git a/freemarker-generator-cli/src/app/examples/templates/dataframe/html/print.ftl b/freemarker-generator-cli/src/app/examples/templates/dataframe/html/print.ftl
index 3ae73c8..82343b8 100644
--- a/freemarker-generator-cli/src/app/examples/templates/dataframe/html/print.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/dataframe/html/print.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withHeader().withDelimiter(';')>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign dataFrame = tools.dataframe.toDataFrame(csvParser)>
 <#--------------------------------------------------------------------------->
 <!DOCTYPE html>
diff --git a/freemarker-generator-cli/src/app/examples/templates/datasources.ftl b/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
index 84e8bb3..e00e76a 100644
--- a/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
@@ -1,79 +1,112 @@
-<#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.
+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.
 -->
-Support FreeMarker Directives
+Support Of FreeMarker Directives
 ==============================================================================
-has_content: ${dataSources?has_content?c}
-size: ${dataSources?size}
+dataSources?has_content: ${dataSources?has_content?c}
+dataSources?size: ${dataSources?size}
 
-Use FTL Array-style Access
+Iterate Over DataSources Using Array-style Access
 ==============================================================================
-${dataSources?values[0].name}
-${dataSources?values?first.name}
+<#if dataSources?has_content>
+<#list 0..<dataSources?size as i>
+- dataSource[${i}] ==> ${dataSources[i].name}
+</#list>
+<#else>
+No data sources provided ...
+</#if>
 
-Get Document Names As Keys
+Iterate Over DataSources Using Sequence
 ==============================================================================
-<#list dataSources?keys as name>
-    ${name}<#lt>
+<#list dataSources as dataSource>
+- dataSource[${dataSource?index}] => ${dataSource.name}
+<#else>
+No data sources provided ...
 </#list>
 
-Iterate Over Names & DataSources
+Iterate Over DataSources Using Key & Values
 ==============================================================================
 <#list dataSources as name, dataSource>
-    ${name} => ${dataSource.uri}<#lt>
-</#list>
-
-<#list dataSources?values>
-    <#items as dataSource>
-        <@writeDataSource dataSource/>
-    </#items>
+- dataSource["${name}"] => ${dataSource.name}<#lt>
 <#else>
-    No data sources found ...
+No data sources provided ...
 </#list>
 
-<#macro writeDataSource dataSource>
+Iterate Over DataSources Using Values
+==============================================================================
+<#list dataSources?values as dataSource>
+- dataSource[${dataSource?index}] => ${dataSource.name}
+<#else>
+No data sources provided ...
+</#list>
 
-${dataSource.name}
+Iterate Over DataSources Using Hash Map Keys
+==============================================================================
+<#list dataSources?keys as key>
+- dataSource["${key}"] => ${dataSources[key].name}
+<#else>
+No data sources provided ...
+</#list>
+
+Iterate Over DataSources Using Local Lambda Expression
+==============================================================================
+<#list dataSources?filter(ds -> ds.match("group", "default")) as dataSource>
+- Group "default" => ${dataSource.name}
+<#else>
+No data sources provided ...
+</#list>
+
+Iterate Over DataSources Using Wildcard Search
+==============================================================================
+<#list dataSources?api.findByName("*") as dataSource>
+- ${dataSource.name}
+<#else>
+No data sources provided ...
+</#list>
+
+Access Underlying DataSources API
+==============================================================================
+DataSources.getNames(): ${dataSources?api.names?size}
+DataSources.getGroups(): ${dataSources?api.getGroups()?size}
+DataSources.find("name", "*"): ${dataSources?api.find("name", "*")?size}
+DataSources.find("name", "!readme"): ${dataSources?api.find("name", "!readme")?size}
+DataSources.find("uri", "*.md"): ${dataSources?api.find("uri", "*.md")?size}
+DataSources.find("extension", "md"): ${dataSources?api.find("extension", "md")?size}
+
+<#if dataSources?has_content>
+<#list dataSources as dataSource>
+[#${dataSource?counter}] - ${dataSource.name}
 ==============================================================================
 
 Invoke Arbitrary Methods On DataSource
 ---------------------------------------------------------------------------
-<#assign dataSource=dataSources?values?first>
-Name            : ${dataSource.name}
-Group           : ${dataSource.group}
-Nr of lines     : ${dataSource.lines?size}
-ContentType     : ${dataSource.contentType}
-MimeType        : ${dataSource.mimeType}
-Charset         : ${dataSource.charset}
-Extension       : ${dataSource.extension}
-Nr of chars     : ${dataSource.text?length}
-Nr of bytes     : ${dataSource.bytes?size}
-File name       : ${dataSource.fileName}
-URI schema      : ${dataSource.uri.scheme}
+<#assign dataSource=dataSources?first>
+Name                : ${dataSource.name}
+Nr of lines         : ${dataSource.lines?size}
+Content Type        : ${dataSource.contentType}
+Charset             : ${dataSource.charset}
+Extension           : ${dataSource.extension}
+Nr of chars         : ${dataSource.text?length}
+Nr of bytes         : ${dataSource.bytes?size}
 
 Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
 <#list dataSource.metadata as name, value>
-${name?right_pad(15)} : ${value}
+${name?right_pad(19)} : ${value}
 </#list>
 
-Iterating Over Properties Of A Datasource
----------------------------------------------------------------------------
-<#list dataSource.properties as name, value>
-${name?right_pad(15)} : ${value}
 </#list>
-</#macro>
+</#if>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/templates/demo.ftl b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
index d929fe2..2217e2b 100644
--- a/freemarker-generator-cli/src/app/examples/templates/demo.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
@@ -49,7 +49,7 @@
 6) Display list of data sources
 ---------------------------------------------------------------------------
 List all data sources:
-<#list dataSources?values as dataSource>
+<#list dataSources as dataSource>
 - Document: name=${dataSource.name} uri=${dataSource.uri} length=${dataSource.length} charset=${dataSource.charset}
 </#list>
 
@@ -107,22 +107,22 @@
 - ${dataSources?size}
 <#if dataSources?has_content>
 Get the first data source:
-- ${dataSources?values[0].name!"No data sources provided"}
+- ${dataSources[0].name!"No data sources provided"}
 </#if>
 Get all documents as map:
 <#list dataSources as name, ds>
 - ${name} => ${ds.mimeType}
 </#list>
 List all data sources containing "test" in the name
-<#list dataSources?values?filter(ds -> ds.match("name", "*test*")) as ds>
+<#list dataSources?filter(ds -> ds.match("name", "*test*")) as ds>
 - ${ds.name}
 </#list>
 List all data sources having "json" extension
-<#list dataSources?values?filter(ds -> ds.match("extension", "json")) as ds>
+<#list dataSources?filter(ds -> ds.match("extension", "json")) as ds>
 - ${ds.name}
 </#list>
 List all data sources having "src/test/data/properties" in their file path
-<#list dataSources?values?filter(ds -> ds.match("filepath", "*/src/test/data/properties")) as ds>
+<#list dataSources?filter(ds -> ds.match("filePath", "*/src/test/data/properties")) as ds>
 - ${ds.name}
 </#list>
 
diff --git a/freemarker-generator-cli/src/app/examples/templates/excel/csv/custom.ftl b/freemarker-generator-cli/src/app/examples/templates/excel/csv/custom.ftl
index 1e2bd06..59ca407 100644
--- a/freemarker-generator-cli/src/app/examples/templates/excel/csv/custom.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/excel/csv/custom.ftl
@@ -18,7 +18,7 @@
 <#assign format = CSV_TARGET_FORMAT!"DEFAULT">
 <#assign salt = tools.system.parameters.salt!"salt">
 <#-- Parse the first data source & sheet of the Excel document -->
-<#assign workbook = tools.excel.parse(dataSources?values[0])>
+<#assign workbook = tools.excel.parse(dataSources[0])>
 <#assign sheet = tools.excel.getSheets(workbook)[0]>
 <#assign records = tools.excel.toTable(sheet)>
 <#-- Setup CSVPrinter  -->
diff --git a/freemarker-generator-cli/src/app/examples/templates/excel/dataframe/transform.ftl b/freemarker-generator-cli/src/app/examples/templates/excel/dataframe/transform.ftl
index 5b24903..bd5252c 100644
--- a/freemarker-generator-cli/src/app/examples/templates/excel/dataframe/transform.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/excel/dataframe/transform.ftl
@@ -14,7 +14,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign workbook = tools.excel.parse(dataSource)>
 <#list tools.excel.getSheets(workbook) as sheet>
     <#assign table = tools.excel.toTable(sheet)>
diff --git a/freemarker-generator-cli/src/app/examples/templates/html/csv/dependencies.ftl b/freemarker-generator-cli/src/app/examples/templates/html/csv/dependencies.ftl
index 51fe17e..4c2e2de 100644
--- a/freemarker-generator-cli/src/app/examples/templates/html/csv/dependencies.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/html/csv/dependencies.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign html = tools.jsoup.parse(dataSource)>
 
diff --git a/freemarker-generator-cli/src/app/examples/templates/html/txt/licence.ftl b/freemarker-generator-cli/src/app/examples/templates/html/txt/licence.ftl
index 2807493..d503011 100644
--- a/freemarker-generator-cli/src/app/examples/templates/html/txt/licence.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/html/txt/licence.ftl
@@ -17,7 +17,7 @@
 <#--
 FreeMarker template to create a LICENCE file.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign html = tools.jsoup.parse(dataSource)>
 <#assign dataframe = tools.dataframe.create()
     .addStringColumn("GroupId")
diff --git a/freemarker-generator-cli/src/app/examples/templates/javafaker/csv/testdata.ftl b/freemarker-generator-cli/src/app/examples/templates/javafaker/csv/testdata.ftl
index 1976644..0c6418e 100644
--- a/freemarker-generator-cli/src/app/examples/templates/javafaker/csv/testdata.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/javafaker/csv/testdata.ftl
@@ -16,7 +16,7 @@
 -->
 <#-- Get a localized JavaFaker instance -->
 <#assign faker = tools.javafaker.getFaker("de_DE")>
-<#assign nrOfRecords = tools.system.getString("NR_OF_RECORDS","100")>
+<#assign nrOfRecords = tools.system.getProperty("NR_OF_RECORDS","100")>
 <#assign days = tools.javafaker.timeUnits["DAYS"]>
 <#assign csvTargetFormat = tools.csv.formats["DEFAULT"].withFirstRecordAsHeader()>
 <#assign csvPrinter = tools.csv.printer(csvTargetFormat)>
diff --git a/freemarker-generator-cli/src/app/examples/templates/json/csv/swagger-endpoints.ftl b/freemarker-generator-cli/src/app/examples/templates/json/csv/swagger-endpoints.ftl
index 6ab4ce2..a70286c 100644
--- a/freemarker-generator-cli/src/app/examples/templates/json/csv/swagger-endpoints.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/json/csv/swagger-endpoints.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign map = tools.gson.parse(dataSources?values[0])>
+<#assign map = tools.gson.parse(dataSources[0])>
 <#assign basePath = map.basePath!"/">
 <#assign paths = map.paths!{}>
 
diff --git a/freemarker-generator-cli/src/app/examples/templates/json/dataframe/github-users.ftl b/freemarker-generator-cli/src/app/examples/templates/json/dataframe/github-users.ftl
index 480f59a..49c21c2 100644
--- a/freemarker-generator-cli/src/app/examples/templates/json/dataframe/github-users.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/json/dataframe/github-users.ftl
@@ -15,6 +15,6 @@
 specific language governing permissions and limitations
 under the License.
 -->
-<#assign json = tools.gson.parse(dataSources?values[0])>
+<#assign json = tools.gson.parse(dataSources[0])>
 <#assign dataframe = tools.dataframe.fromMaps(json)>
 ${tools.dataframe.print(dataframe)}
diff --git a/freemarker-generator-cli/src/app/examples/templates/json/md/github-users.ftl b/freemarker-generator-cli/src/app/examples/templates/json/md/github-users.ftl
index a83d07b..39b37a2 100644
--- a/freemarker-generator-cli/src/app/examples/templates/json/md/github-users.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/json/md/github-users.ftl
@@ -15,7 +15,7 @@
 specific language governing permissions and limitations
 under the License.
 -->
-<#assign json = tools.jsonpath.parse(dataSources?values[0])>
+<#assign json = tools.jsonpath.parse(dataSources[0])>
 <#assign users = json.read("$[*]")>
 <#--------------------------------------------------------------------------->
 # GitHub Users
diff --git a/freemarker-generator-cli/src/app/examples/templates/logs/csv/serverlog-to-csv.ftl b/freemarker-generator-cli/src/app/examples/templates/logs/csv/serverlog-to-csv.ftl
index 544975f..d53e24f 100644
--- a/freemarker-generator-cli/src/app/examples/templates/logs/csv/serverlog-to-csv.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/logs/csv/serverlog-to-csv.ftl
@@ -35,7 +35,7 @@
 <#compress>
     TIMESTAMP;MILLIS
     <#if dataSources?has_content>
-        <#list dataSources?values as dataSource>
+        <#list dataSources as dataSource>
             <#list dataSource.getLineIterator() as line>
                 <#assign parts = grok.match(line).capture()>
                 <#if parts?has_content>
diff --git a/freemarker-generator-cli/src/app/examples/templates/nginx/confluence/nginx-config-parser.ftl b/freemarker-generator-cli/src/app/examples/templates/nginx/confluence/nginx-config-parser.ftl
new file mode 100644
index 0000000..79331a5
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/templates/nginx/confluence/nginx-config-parser.ftl
@@ -0,0 +1,34 @@
+<#compress>
+    || FILE || SERVER || ACCESSLOG || SPLUNK ||
+    <#list dataSources as dataSource>
+        <#assign fileName = dataSource.fileName>
+        <#assign serverName = "N.A">
+        <#-- Transform to a single line to avoid matching OS-specific line endings -->
+        <#assign text = dataSource.getText()?replace("\r", "")?replace("\n", " ")>
+        <#assign accessLog = getAccessLog(text)>
+        <#assign serverName = getServerName(text)>
+        | ${fileName} | ${serverName} | ${accessLog} | [${splunkSearchUrl(accessLog)}] |
+    </#list>
+</#compress>
+<#--------------------------------------------------------------------------->
+<#function splunkSearchUrl accessLog>
+    <#return "https://splunk.p.santanderconsumer.at/en-US/app/scbdevteam/search?q=search%20source%3D%22${accessLog?url}%22">
+</#function>
+<#--------------------------------------------------------------------------->
+<#function getAccessLog text>
+    <#assign matches = text?matches(r".*access_log\s*([\w\.\-\/\\]*);.*")>
+    <#if matches>
+        <#return matches?groups[1]?trim>
+    <#else>
+        <#return "N.A.">
+    </#if>
+</#function>
+<#--------------------------------------------------------------------------->
+<#function getServerName text>
+    <#assign matches = text?matches(r".*server_name\s*([\w\.\-\\]*);.*")>
+    <#if matches>
+        <#return matches?groups[1]?trim>
+    <#else>
+        <#return "N.A.">
+    </#if>
+</#function>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/templates/properties/csv/locker-test-users.ftl b/freemarker-generator-cli/src/app/examples/templates/properties/csv/locker-test-users.ftl
index 8e01dac..32dd5d6 100644
--- a/freemarker-generator-cli/src/app/examples/templates/properties/csv/locker-test-users.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/properties/csv/locker-test-users.ftl
@@ -17,7 +17,7 @@
 -->
 <#compress>
     TENANT,SITE,USER_ID,DISPOSER_ID,PASSWORD,SMS_OTP,NAME,DESCRIPTION
-    <#list dataSources?values as dataSource>
+    <#list dataSources as dataSource>
         <#assign properties = tools.properties.parse(dataSource)>
         <#assign environments = properties.ENVIRONMENTS!"">
         <#assign tenant = extractTenant(environments)>
diff --git a/freemarker-generator-cli/src/app/examples/templates/tsv/fo/transactions.ftl b/freemarker-generator-cli/src/app/examples/templates/tsv/fo/transactions.ftl
index 5beefc3..46a89de 100644
--- a/freemarker-generator-cli/src/app/examples/templates/tsv/fo/transactions.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/tsv/fo/transactions.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#assign cvsFormat = tools.csv.formats.TDF.withHeader()>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign csvHeaders = csvParser.getHeaderMap()?keys>
 <#assign csvRecords = csvParser.records>
 <#--------------------------------------------------------------------------->
diff --git a/freemarker-generator-cli/src/app/examples/templates/utahparser/csv/transform.ftl b/freemarker-generator-cli/src/app/examples/templates/utahparser/csv/transform.ftl
new file mode 100644
index 0000000..f67729c
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/templates/utahparser/csv/transform.ftl
@@ -0,0 +1,32 @@
+<#--
+  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.
+-->
+<#-- Setup Utah-Parser and parse all records to determine the headers -->
+<#assign conf =  tools.utahparser.getConfig(dataSources[0])>
+<#assign parser = tools.utahparser.getParser(conf, dataSources[1])>
+<#assign records = parser.toList()>
+<#assign headers = tools.utahparser.getHeaders(records)>
+<#-- Setup CSVPrinter  -->
+<#assign defaultCsvformat = tools.csv.formats[CSV_TARGET_FORMAT!"DEFAULT"]>
+<#assign csvDelimiter = tools.csv.toDelimiter(CSV_TARGET_DELIMITER!defaultCsvformat.getDelimiter())>
+<#assign cvsFormat = defaultCsvformat.withHeader(headers).withDelimiter(csvDelimiter)>
+<#assign csvPrinter = tools.csv.printer(cvsFormat)>
+<#-- Print records as CSV  -->
+<#compress>
+    <#list records as record>
+        ${csvPrinter.printRecord(record, headers)}
+    </#list>
+</#compress>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/templates/utahparser/json/transform.ftl b/freemarker-generator-cli/src/app/examples/templates/utahparser/json/transform.ftl
new file mode 100644
index 0000000..4c2a53e
--- /dev/null
+++ b/freemarker-generator-cli/src/app/examples/templates/utahparser/json/transform.ftl
@@ -0,0 +1,19 @@
+<#--
+  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 conf =  tools.utahparser.getConfig(dataSources[0])>
+<#assign parser = tools.utahparser.getParser(conf, dataSources[1])>
+${tools.gson.toJson(parser.toList())}
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/examples/templates/xml/txt/recipients.ftl b/freemarker-generator-cli/src/app/examples/templates/xml/txt/recipients.ftl
index b67e8e7..83bf4e5 100644
--- a/freemarker-generator-cli/src/app/examples/templates/xml/txt/recipients.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/xml/txt/recipients.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign xml = tools.xml.parse(dataSources?values[0])>
+<#assign xml = tools.xml.parse(dataSources[0])>
 <#list xml.recipients.person as recipient>
 To: ${recipient.name}
 ${recipient.address}
diff --git a/freemarker-generator-cli/src/app/examples/templates/yaml/txt/transform.ftl b/freemarker-generator-cli/src/app/examples/templates/yaml/txt/transform.ftl
index ebcf525..1e13504 100644
--- a/freemarker-generator-cli/src/app/examples/templates/yaml/txt/transform.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/yaml/txt/transform.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign map = tools.yaml.parse(dataSources?values[0])>
+<#assign map = tools.yaml.parse(dataSources[0])>
 <#--------------------------------------------------------------------------->
 <#compress>
 <@print map 1/>
diff --git a/freemarker-generator-cli/src/app/scripts/run-examples.bat b/freemarker-generator-cli/src/app/scripts/run-examples.bat
index 5467382..a55395d 100644
--- a/freemarker-generator-cli/src/app/scripts/run-examples.bat
+++ b/freemarker-generator-cli/src/app/scripts/run-examples.bat
@@ -51,17 +51,20 @@
 REM Interactive Mode
 REM =========================================================================
 
-%FREEMARKER_CMD% -i '${tools.jsonpath.parse(dataSources?values[0]).read("""$.info.title""")}' examples\data\json\swagger-spec.json > target\out\interactive-json.txt
-%FREEMARKER_CMD% -i '${tools.xml.parse(dataSources?values[0])["""recipients/person[1]/name"""]}' examples\data\xml\recipients.xml > target\out\interactive-xml.txt
-%FREEMARKER_CMD% -i '${tools.jsoup.parse(dataSources?values[0]).select("""a""")[0]}' examples\data\html\dependencies.html > target\out\interactive-html.txt
-%FREEMARKER_CMD% -i '${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))}' examples\data\yaml\swagger-spec.yaml > target\out\interactive-swagger.json
-%FREEMARKER_CMD% -i '${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))}' examples\data\json\swagger-spec.json > target\out\interactive-swagger.yaml
-%FREEMARKER_CMD% -i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources?values[0])))}' examples\data\json\github-users.json > target\out\interactive-dataframe.txt
+%FREEMARKER_CMD% -i '${tools.jsonpath.parse(dataSources[0]).read("""$.info.title""")}' examples\data\json\swagger-spec.json > target\out\interactive-json.txt
+%FREEMARKER_CMD% -i '${tools.xml.parse(dataSources[0])["""recipients/person[1]/name"""]}' examples\data\xml\recipients.xml > target\out\interactive-xml.txt
+%FREEMARKER_CMD% -i '${tools.jsoup.parse(dataSources[0]).select("""a""")[0]}' examples\data\html\dependencies.html > target\out\interactive-html.txt
+%FREEMARKER_CMD% -i '${tools.gson.toJson(tools.yaml.parse(dataSources[0]))}' examples\data\yaml\swagger-spec.yaml > target\out\interactive-swagger.json
+%FREEMARKER_CMD% -i '${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))}' examples\data\json\swagger-spec.json > target\out\interactive-swagger.yaml
+%FREEMARKER_CMD% -i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources[0])))}' examples\data\json\github-users.json > target\out\interactive-dataframe.txt
 
 REM =========================================================================
 REM CSV
 REM =========================================================================
 
+echo "templates\freemarker-generator\csv\confluence\transform.ftl"
+%FREEMARKER_CMD% -t freemarker-generator\csv\confluence\transform.ftl examples\data\csv\contract.csv > target\out\contract.txt
+
 echo "templates\freemarker-generator\csv\html\transform.ftl"
 %FREEMARKER_CMD% -t freemarker-generator\csv\html\transform.ftl examples\data\csv\contract.csv > target\out\contract.html
 
@@ -159,6 +162,13 @@
 %FREEMARKER_CMD% -t examples\templates\json\md\github-users.ftl examples\data\json\github-users.json > target\out\github-users.md
 
 REM =========================================================================
+REM NGINX
+REM =========================================================================
+
+echo "examples\templates\nginx\confluence\nginx-config-parser.ftl"
+%FREEMARKER_CMD% -t examples\templates\nginx\confluence\nginx-config-parser.ftl -s examples\data\nginx > target\out\nginx-config.txt
+
+REM =========================================================================
 REM Properties
 REM =========================================================================
 
@@ -173,6 +183,16 @@
 %FREEMARKER_CMD% -t examples\data\template -PNGINX_HOSTNAME=localhost -o target\out\template
 
 REM =========================================================================
+REM Utah Parser
+REM =========================================================================
+
+echo "examples\templates\utahparser\csv\transform.ftl"
+%FREEMARKER_CMD% -PCSV_TARGET_FORMAT=EXCEL -PCSV_TARGET_DELIMITER=SEMICOLON -t examples\templates\utahparser\csv\transform.ftl examples\data\text\utahparser\juniper_bgp_summary_template.xml examples\data\text\utahparser\juniper_bgp_summary_example.txt -o target\out\utahparserjuniper_bgp_summary_example.csv
+
+echo "examples\templates\utahparser\json\transform.ftl"
+%FREEMARKER_CMD% -t examples\templates\utahparser\json\transform.ftl examples\data\text\utahparser\juniper_bgp_summary_template.xml examples\data\text\utahparser\juniper_bgp_summary_example.txt -o target\out\utahparser\uniper_bgp_summary_example.json
+
+REM =========================================================================
 REM XML
 REM =========================================================================
 
@@ -189,5 +209,15 @@
 echo "templates\freemarker-generator\yaml\json\transform.ftl"
 %FREEMARKER_CMD% -t freemarker-generator\yaml\json\transform.ftl examples\data\yaml\swagger-spec.yaml > target\out\swagger-spec.json
 
+REM =========================================================================
+REM DataSource Seeding
+REM =========================================================================
+
+echo "examples\data\csv"
+%FREEMARKER_CMD% --seed=datasource --template freemarker-generator\csv\html\transform.ftl --output target\out\datasource\csv --output-mapper="*.html" examples\data\csv
+
+echo "examples\data\csv"
+%FREEMARKER_CMD% --seed=datasource --template freemarker-generator\csv\html\transform.ftl --data-source . --data-source-include="*.csv" --output target\out\datasource\csv --output-mapper="*.html"
+
 echo "Created the following sample files in .\target\out"
 dir .\target\out
diff --git a/freemarker-generator-cli/src/app/scripts/run-examples.sh b/freemarker-generator-cli/src/app/scripts/run-examples.sh
index 1fce8b7..9bc5d81 100755
--- a/freemarker-generator-cli/src/app/scripts/run-examples.sh
+++ b/freemarker-generator-cli/src/app/scripts/run-examples.sh
@@ -56,17 +56,20 @@
 # Interactive Mode
 #############################################################################
 
-$FREEMARKER_CMD -i '${tools.jsonpath.parse(dataSources?values[0]).read("$.info.title")}' examples/data/json/swagger-spec.json > target/out/interactive-json.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
-$FREEMARKER_CMD -i '${tools.xml.parse(dataSources?values[0])["recipients/person[1]/name"]}' examples/data/xml/recipients.xml > target/out/interactive-xml.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
-$FREEMARKER_CMD -i '${tools.jsoup.parse(dataSources?values[0]).select("a")[0]}' examples/data/html/dependencies.html > target/out/interactive-html.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
-$FREEMARKER_CMD -i '${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))}' examples/data/yaml/swagger-spec.yaml > target/out/interactive-swagger.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
-$FREEMARKER_CMD -i '${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))}' examples/data/json/swagger-spec.json > target/out/interactive-swagger.yaml || { echo >&2 "Test failed.  Aborting."; exit 1; }
-$FREEMARKER_CMD -i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources?values[0])))}' examples/data/json/github-users.json > target/out/interactive-dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.jsonpath.parse(dataSources[0]).read("$.info.title")}' examples/data/json/swagger-spec.json > target/out/interactive-json.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.xml.parse(dataSources[0])["recipients/person[1]/name"]}' examples/data/xml/recipients.xml > target/out/interactive-xml.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.jsoup.parse(dataSources[0]).select("a")[0]}' examples/data/html/dependencies.html > target/out/interactive-html.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.gson.toJson(tools.yaml.parse(dataSources[0]))}' examples/data/yaml/swagger-spec.yaml > target/out/interactive-swagger.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))}' examples/data/json/swagger-spec.json > target/out/interactive-swagger.yaml || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources[0])))}' examples/data/json/github-users.json > target/out/interactive-dataframe.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
 # CSV
 #############################################################################
 
+echo "templates/freemarker-generator/csv/confluence/transform.ftl"
+$FREEMARKER_CMD -t freemarker-generator/csv/confluence/transform.ftl examples/data/csv/contract.csv > target/out/contract.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
 echo "templates/freemarker-generator/csv/html/transform.ftl"
 $FREEMARKER_CMD -t freemarker-generator/csv/html/transform.ftl examples/data/csv/contract.csv > target/out/contract.html || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
@@ -180,6 +183,13 @@
 $FREEMARKER_CMD -t examples/templates/json/md/github-users.ftl examples/data/json/github-users.json > target/out/github-users.md || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
+# NGINX
+#############################################################################
+
+echo "examples/templates/nginx/confluence/nginx-config-parser.ftl"
+$FREEMARKER_CMD -t examples/templates/nginx/confluence/nginx-config-parser.ftl -s examples/data/nginx > target/out/nginx-config.txt || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # Properties
 #############################################################################
 
@@ -194,6 +204,16 @@
 $FREEMARKER_CMD -t examples/data/template -PNGINX_HOSTNAME=localhost -o target/out/template  || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
+# Utah Parser
+#############################################################################
+
+echo "examples/templates/utahparser/csv/transform.ftl"
+$FREEMARKER_CMD -PCSV_TARGET_FORMAT=EXCEL -PCSV_TARGET_DELIMITER=SEMICOLON -t examples/templates/utahparser/csv/transform.ftl examples/data/text/utahparser/juniper_bgp_summary_template.xml examples/data/text/utahparser/juniper_bgp_summary_example.txt -o target/out/utahparserjuniper_bgp_summary_example.csv || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+echo "examples/templates/utahparser/json/transform.ftl"
+$FREEMARKER_CMD -t examples/templates/utahparser/json/transform.ftl examples/data/text/utahparser/juniper_bgp_summary_template.xml examples/data/text/utahparser/juniper_bgp_summary_example.txt -o target/out/utahparser/juniper_bgp_summary_example.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # XML
 #############################################################################
 
@@ -210,5 +230,15 @@
 echo "templates/freemarker-generator/yaml/json/transform.ftl"
 $FREEMARKER_CMD -t freemarker-generator/yaml/json/transform.ftl examples/data/yaml/swagger-spec.yaml > target/out/swagger-spec.json || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
+#############################################################################
+# DataSource Seeding
+#############################################################################
+
+echo "examples/data/csv"
+$FREEMARKER_CMD --seed=datasource --template freemarker-generator/csv/html/transform.ftl --output target/out/datasource/csv --output-mapper="*.html" examples/data/csv
+
+echo "examples/data/csv"
+$FREEMARKER_CMD --seed=datasource --template freemarker-generator/csv/html/transform.ftl --data-source . --data-source-include="*.csv" --output target/out/datasource/csv --output-mapper="*.html"
+
 echo "Created the following sample files in ./target/out"
 ls -l ./target/out
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/cat.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/cat.ftl
index cdbe0fa..f4f82e8 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/cat.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/cat.ftl
@@ -14,7 +14,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#list dataSources?values as dataSource>
+<#list dataSources as dataSource>
 <#list dataSource.lineIterator as line>
 ${line}
 </#list>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/confluence/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/confluence/transform.ftl
new file mode 100644
index 0000000..35857db
--- /dev/null
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/confluence/transform.ftl
@@ -0,0 +1,40 @@
+<#--
+  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.
+-->
+<#import "/freemarker-generator/lib/commons-csv.ftl" as csv />
+<#assign dataSource = dataSources[0]>
+<#assign csvParser = tools.csv.parse(dataSource, csv.sourceFormat())>
+<#assign headers = (csvParser.getHeaderMap()!{})?keys>
+<#assign records = csvParser.records>
+<#--------------------------------------------------------------------------->
+<#compress>
+    <@writeHeaders headers/>
+    <@writeColums records/>
+</#compress>
+<#--------------------------------------------------------------------------->
+<#macro writeHeaders headers>
+    <#if headers?has_content>
+        || ${headers?join(" || ", "")} ||
+    </#if>
+</#macro>
+<#--------------------------------------------------------------------------->
+<#macro writeColums columns>
+    <#if columns?has_content>
+        <#list columns as column>
+            | ${column.iterator()?join(" | ", "")} |
+        </#list>
+    </#if>
+</#macro>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/csv/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/csv/transform.ftl
index 3941c4c..c999235 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/csv/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/csv/transform.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#import "/freemarker-generator/lib/commons-csv.ftl" as csv />
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign csvParser = tools.csv.parse(dataSource, csv.sourceFormat())>
 <#assign csvTargetFormat = csv.targetFormat()>
 <#assign csvPrinter = tools.csv.printer(csvTargetFormat)>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/html/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/html/transform.ftl
index d027672..e39a76d 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/html/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/html/transform.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 <#import "/freemarker-generator/lib/commons-csv.ftl" as csv />
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign csvParser = tools.csv.parse(dataSource, csv.sourceFormat())>
 <#assign csvHeaders = csvParser.getHeaderNames()>
 <#--------------------------------------------------------------------------->
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/md/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/md/transform.ftl
index 6929b31..edd8b3e 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/md/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/csv/md/transform.ftl
@@ -15,7 +15,7 @@
   under the License.
 -->
 <#import "/freemarker-generator/lib/commons-csv.ftl" as csv />
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign csvParser = tools.csv.parse(dataSource, csv.sourceFormat())>
 <#assign headers = (csvParser.getHeaderMap()!{})?keys>
 <#assign records = csvParser.records>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/csv/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/csv/transform.ftl
index 18179f4..22dec59 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/csv/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/csv/transform.ftl
@@ -17,7 +17,7 @@
 -->
 <#-- Parse the first data source & sheet of the Excel document -->
 <#import "/freemarker-generator/lib/commons-csv.ftl" as csv />
-<#assign workbook = tools.excel.parse(dataSources?values[0])>
+<#assign workbook = tools.excel.parse(dataSources[0])>
 <#assign sheet = tools.excel.getSheets(workbook)[0]>
 <#assign records = tools.excel.toTable(sheet)>
 <#-- Setup CSVPrinter  -->
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/html/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/html/transform.ftl
index 7564751..e1e9409 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/html/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/html/transform.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign workbook = tools.excel.parse(dataSource)>
 <#assign date = .now?iso_utc>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/md/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/md/transform.ftl
index e3fb970..a0e13b7 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/md/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/excel/md/transform.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign workbook = tools.excel.parse(dataSource)>
 <#assign date = .now?iso_utc>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
index c165be3..0a3f0ce 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
@@ -1,4 +1,3 @@
-<#ftl output_format="plainText" strip_whitespace=true>
 <#--
   Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
@@ -16,40 +15,66 @@
   under the License.
 -->
 FreeMarker Generator Information
-------------------------------------------------------------------------------
+==============================================================================
+
 FreeMarker version     : ${.version}
 Template name          : ${.current_template_name}
 Language               : ${.lang}
 Locale                 : ${.locale}
 Timestamp              : ${.now}
+Time zone              : ${.now?string["z"]}
 Output encoding        : ${.output_encoding}
 Output format          : ${.output_format}
 
+FreeMarker Command-line Parameters
+==============================================================================
+
+<#list tools.system.getCommandLineArgs() as arg>
+[#${arg?counter}] ${arg}
+</#list>
+
 FreeMarker Generator Template Loader Directories
-------------------------------------------------------------------------------
+==============================================================================
+
 <#list tools.system.getTemplateDirectories() as directory>
 [#${directory?counter}] ${directory}
 </#list>
 
 FreeMarker Generator Data Model
----------------------------------------------------------------------------
+==============================================================================
+
 <#list .data_model?keys?sort as key>
 - ${key}<#lt>
 </#list>
 
-FreeMarker Generator DataSources
-------------------------------------------------------------------------------
+FreeMarker Generator Data Sources
+==============================================================================
 <#if dataSources?has_content>
-<#list dataSources?values as ds>
-[#${ds?counter}]: name=${ds.name}, group=${ds.group}, fileName=${ds.fileName}, mimeType=${ds.mimeType}, charset=${ds.charset}, length=${ds.length} Bytes
-URI : ${ds.uri}
+<#list dataSources as dataSource>
+
+DataSource #${dataSource?counter}
+------------------------------------------------------------------------------
+name                  : ${dataSource.name}
+group                 : ${dataSource.group}
+contentType           : ${dataSource.contentType}
+fileName              : ${dataSource.fileName}
+baseName              : ${dataSource.baseName}
+extension             : ${dataSource.extension}
+relativeFilePath      : ${dataSource.relativeFilePath}
+charset               : ${dataSource.charset}
+mimeType              : ${dataSource.mimeType}
+uri                   : ${dataSource.uri}
+length                : ${dataSource.length} bytes
+metadata              : ${dataSource.metadata?size} entries
 </#list>
 <#else>
+
 No data sources found ...
 </#if>
 
 FreeMarker Generator Parameters
-------------------------------------------------------------------------------
+==============================================================================
+
 <#if tools.system.parameters?has_content>
 <#list tools.system.parameters as key,value>
 <#if value?is_hash>
@@ -63,7 +88,8 @@
 </#if>
 
 FreeMarker Generator Tools
-------------------------------------------------------------------------------
+==============================================================================
+
 <#list tools?keys?sort as name>
-- ${name?right_pad(20)} : ${tools[name]}
+- ${name?right_pad(19)} : ${tools[name]}
 </#list>
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/json/yaml/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/json/yaml/transform.ftl
index 6e48945..d5b32f9 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/json/yaml/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/json/yaml/transform.ftl
@@ -14,4 +14,4 @@
   specific language governing permissions and limitations
   under the License.
 -->
-${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))}
\ No newline at end of file
+${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))}
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/app/templates/freemarker-generator/yaml/json/transform.ftl b/freemarker-generator-cli/src/app/templates/freemarker-generator/yaml/json/transform.ftl
index 81a704a..d0f0581 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/yaml/json/transform.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/yaml/json/transform.ftl
@@ -14,4 +14,4 @@
   specific language governing permissions and limitations
   under the License.
 -->
-${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))}
\ No newline at end of file
+${tools.gson.toJson(tools.yaml.parse(dataSources[0]))}
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
index 5158668..7c9c4d9 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
@@ -140,15 +140,14 @@
 
     @Override
     public Integer call() {
-        validate();
+        validateCommandLineParameters();
+        updateGlobalSystemProperties();
         return IntStream.range(0, times).map(i -> onCall()).max().orElse(0);
     }
 
     private Integer onCall() {
-        updateSystemProperties();
-
         final String currentConfigFile = isNotEmpty(configFile) ? configFile : getDefaultConfigFileName();
-        final Properties configuration = loadFreeMarkerCliConfiguration(currentConfigFile);
+        final Properties configuration = loadFreeMarkerGeneratorConfiguration(currentConfigFile);
         final List<File> templateDirectories = getTemplateDirectories(templateDir);
         final Settings settings = settings(configuration, templateDirectories, outputGeneratorDefinitions);
 
@@ -166,7 +165,11 @@
         }
     }
 
-    void validate() {
+    /**
+     * Invoke a custom validation of the command line parameters supplementing
+     * the checks already done by Picocli.
+     */
+    void validateCommandLineParameters() {
         outputGeneratorDefinitions.forEach(t -> t.validate(spec.commandLine()));
     }
 
@@ -195,7 +198,7 @@
                 .build();
     }
 
-    private void updateSystemProperties() {
+    private void updateGlobalSystemProperties() {
         if (systemProperties != null && !systemProperties.isEmpty()) {
             System.getProperties().putAll(systemProperties);
         }
@@ -203,7 +206,7 @@
 
     /**
      * Data sources can be passed via command line option and/or
-     * positional parameter so we need to merge them.
+     * positional parameter, so we need to merge them.
      *
      * @return List of data sources
      */
@@ -226,7 +229,7 @@
         return isNotEmpty(appHome) ? new File(appHome, Configuration.CONFIG_FILE_NAME).getAbsolutePath() : null;
     }
 
-    private static Properties loadFreeMarkerCliConfiguration(String fileName) {
+    private static Properties loadFreeMarkerGeneratorConfiguration(String fileName) {
         if (isEmpty(fileName)) {
             return new Properties();
         }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
index 593f7f7..a54f98d 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
@@ -20,6 +20,7 @@
 import freemarker.template.Configuration;
 import freemarker.template.Version;
 import org.apache.freemarker.generator.base.util.PropertiesTransformer;
+import org.apache.freemarker.generator.cli.wrapper.FreeMarkerGeneratorObjectWrapper;
 
 import java.util.Properties;
 import java.util.function.Supplier;
@@ -50,6 +51,10 @@
         try {
             final Configuration configuration = new Configuration(FREEMARKER_VERSION);
 
+            // support a custom "DataSourcesAdaptor"
+            configuration.setAPIBuiltinEnabled(true);
+            configuration.setObjectWrapper(new FreeMarkerGeneratorObjectWrapper(configuration.getIncompatibleImprovements()));
+
             // apply all "freemarker.configuration.setting" values
             configuration.setSettings(freeMarkerConfigurationSettings());
 
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 57591d6..6cc81d0 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
@@ -16,6 +16,7 @@
  */
 package org.apache.freemarker.generator.cli.config;
 
+import org.apache.commons.io.FilenameUtils;
 import org.apache.freemarker.generator.base.datasource.DataSource;
 import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
 import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
@@ -79,15 +80,22 @@
 
         if (contentType.startsWith(MIME_APPLICATION_JSON)) {
             return fromJson(dataSource, isExplodedDataModel);
+        } else if (isYamlDataSource(dataSource)) {
+            return fromYaml(dataSource, isExplodedDataModel);
         } else if (contentType.startsWith(MIME_TEXT_PLAIN)) {
             return fromProperties(dataSource, isExplodedDataModel);
-        } else if (contentType.startsWith(MIME_TEXT_YAML)) {
-            return fromYaml(dataSource, isExplodedDataModel);
         } else {
             throw new IllegalArgumentException("Don't know how to handle content type: " + contentType);
         }
     }
 
+    private static boolean isYamlDataSource(DataSource dataSource) {
+        final String contentType = dataSource.getContentType();
+        final String extension = FilenameUtils.getExtension(dataSource.getUri().toString());
+        return contentType.startsWith(MIME_TEXT_YAML)
+                || (contentType.startsWith(MIME_TEXT_PLAIN)) && "yaml".equalsIgnoreCase(extension);
+    }
+
     private static Map<String, Object> fromJson(DataSource dataSource, boolean isExplodedDataModel) {
         final GsonTool gsonTool = new GsonTool();
         final Object json = gsonTool.parse(dataSource);
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/OutputGeneratorsSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/OutputGeneratorsSupplier.java
index 059a9ab..e1b11b1 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/OutputGeneratorsSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/OutputGeneratorsSupplier.java
@@ -16,127 +16,46 @@
  */
 package org.apache.freemarker.generator.cli.config;
 
-import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
-import org.apache.freemarker.generator.base.datasource.DataSource;
-import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
-import org.apache.freemarker.generator.base.datasource.DataSourcesSupplier;
+import org.apache.freemarker.generator.base.FreeMarkerConstants.SeedType;
 import org.apache.freemarker.generator.base.output.OutputGenerator;
-import org.apache.freemarker.generator.base.template.TemplateTransformation;
-import org.apache.freemarker.generator.base.template.TemplateTransformationsBuilder;
-import org.apache.freemarker.generator.base.util.UriUtils;
+import org.apache.freemarker.generator.cli.config.output.DataSourceSeedingOutputGenerator;
+import org.apache.freemarker.generator.cli.config.output.TemplateSeedingOutputGenerator;
 import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
-import org.apache.freemarker.generator.cli.picocli.TemplateOutputDefinition;
-import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
 
-import java.net.URI;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
-import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
-import static org.apache.freemarker.generator.base.FreeMarkerConstants.Location.STDIN;
-import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_PLAIN;
 
+/**
+ * Supplies a list of <code>OutputGenerators</code> based on the user input.
+ */
 public class OutputGeneratorsSupplier implements Supplier<List<OutputGenerator>> {
 
     private final Settings settings;
 
     public OutputGeneratorsSupplier(Settings settings) {
-        this.settings = settings;
+        this.settings = requireNonNull(settings);
     }
 
     @Override
     public List<OutputGenerator> get() {
         return settings.getOutputGeneratorDefinitions().stream()
-                .map(this::outputGenerator)
+                .map(definition -> outputGenerator(settings, definition))
                 .flatMap(Collection::stream)
                 .collect(Collectors.toList());
     }
 
-    private List<OutputGenerator> outputGenerator(OutputGeneratorDefinition definition) {
-        final List<OutputGenerator> result = new ArrayList<>();
-        final TemplateSourceDefinition templateSourceDefinition = requireNonNull(definition.getTemplateSourceDefinition());
-        final TemplateOutputDefinition templateOutputDefinition = definition.getTemplateOutputDefinition();
-        final TemplateTransformationsBuilder builder = TemplateTransformationsBuilder.builder();
-
-        // set the template
-        if (templateSourceDefinition.isInteractiveTemplate()) {
-            builder.setInteractiveTemplate(templateSourceDefinition.interactiveTemplate);
+    private List<OutputGenerator> outputGenerator(Settings settings, OutputGeneratorDefinition definition) {
+        final String seedType = definition.getOutputSeedType();
+        if (SeedType.TEMPLATE.equalsIgnoreCase(seedType)) {
+            return new TemplateSeedingOutputGenerator(settings).apply(definition);
+        } else if (SeedType.DATASOURCE.equalsIgnoreCase(seedType)) {
+            return new DataSourceSeedingOutputGenerator(settings).apply(definition);
         } else {
-            builder.setTemplateSource(templateSourceDefinition.template);
+            throw new RuntimeException("Unknown seed type:" + seedType);
         }
-
-        // set encoding of loaded templates
-        builder.setTemplateEncoding(settings.getTemplateEncoding());
-
-        // set the writer
-        builder.setCallerSuppliedWriter(settings.getCallerSuppliedWriter());
-
-        // set template output
-        if (templateOutputDefinition != null && templateOutputDefinition.hasOutput()) {
-            builder.setOutput(templateOutputDefinition.outputs.get(0));
-        }
-
-        // set the output encoding
-        builder.setOutputEncoding(settings.getOutputEncoding());
-
-        // set template filter
-        if (definition.hasTemplateSourceIncludes()) {
-            builder.addInclude(definition.getTemplateSourceFilterDefinition().templateIncludePatterns.get(0));
-        }
-
-        if (definition.hasTemplateSourceExcludes()) {
-            builder.addExclude(definition.getTemplateSourceFilterDefinition().templateExcludePatterns.get(0));
-        }
-
-        final List<TemplateTransformation> templateTransformations = builder.build();
-
-        for (TemplateTransformation templateTransformation : templateTransformations) {
-            final OutputGenerator outputGenerator = new OutputGenerator(
-                    templateTransformation.getTemplateSource(),
-                    templateTransformation.getTemplateOutput(),
-                    dataSources(settings, definition),
-                    dataModels(definition)
-            );
-            result.add(outputGenerator);
-        }
-
-        return result;
-    }
-
-    private List<DataSource> dataSources(Settings settings, OutputGeneratorDefinition outputGeneratorDefinition) {
-        final ArrayList<DataSource> result = new ArrayList<>();
-
-        // Add optional data source from STDIN at the start of the list since
-        // this allows easy sequence slicing in FreeMarker.
-        if (settings.isReadFromStdin()) {
-            result.add(0, stdinDataSource());
-        }
-
-        final DataSourcesSupplier outputGeneratorDataSourcesSupplier = new DataSourcesSupplier(
-                outputGeneratorDefinition.getDataSources(),
-                settings.getSourceIncludePattern(),
-                settings.getSourceExcludePattern(),
-                settings.getInputEncoding()
-        );
-
-        result.addAll(outputGeneratorDataSourcesSupplier.get());
-
-        return result;
-    }
-
-    private Map<String, Object> dataModels(OutputGeneratorDefinition outputGeneratorDefinition) {
-        return new DataModelSupplier(outputGeneratorDefinition.getDataModels()).get();
-    }
-
-    private static DataSource stdinDataSource() {
-        final URI uri = UriUtils.toUri(Location.SYSTEM, STDIN);
-        return DataSourceFactory.fromInputStream(STDIN, DEFAULT_GROUP, uri, System.in, MIME_TEXT_PLAIN, UTF_8, new HashMap<>());
     }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
index c45a479..d699a8d 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
@@ -16,6 +16,7 @@
  */
 package org.apache.freemarker.generator.cli.config;
 
+import org.apache.freemarker.generator.base.FreeMarkerConstants;
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
 import org.apache.freemarker.generator.base.util.LocaleUtils;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
@@ -36,8 +37,6 @@
 import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Configuration.LOCALE_KEY;
-import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_CHARSET;
-import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_LOCALE;
 
 /**
  * Capture all the settings required for rendering a FreeMarker template.
@@ -65,11 +64,11 @@
     /** List of additional shared data models */
     private final List<String> sharedDataModels;
 
-    /** Include pattern for sources */
-    private final String sourceIncludePattern;
+    /** Global include pattern for data sources */
+    private final String dataSourceIncludePattern;
 
-    /** Exclude pattern for sources */
-    private final String sourceExcludePattern;
+    /** Global exclude pattern for data sources */
+    private final String dataSourceExcludePattern;
 
     /** Encoding of input files */
     private final Charset inputEncoding;
@@ -86,10 +85,10 @@
     /** Read from stdin? */
     private final boolean isReadFromStdin;
 
-    /** User-supplied parameters */
+    /** User-supplied parameters available for template processing */
     private final Map<String, Object> userParameters;
 
-    /** User-supplied system properties */
+    /** User-supplied properties to be copied to global system properties */
     private final Properties userSystemProperties;
 
     /** Caller-supplied writer */
@@ -103,8 +102,8 @@
             List<OutputGeneratorDefinition> outputGeneratorDefinitions,
             List<String> sharedDataSources,
             List<String> sharedDataModels,
-            String sourceIncludePattern,
-            String sourceExcludePattern,
+            String dataSourceIncludePattern,
+            String dataSourceExcludePattern,
             Charset inputEncoding,
             Charset outputEncoding,
             boolean verbose,
@@ -120,8 +119,8 @@
         this.outputGeneratorDefinitions = requireNonNull(outputGeneratorDefinitions);
         this.sharedDataSources = requireNonNull(sharedDataSources);
         this.sharedDataModels = requireNonNull(sharedDataModels);
-        this.sourceIncludePattern = sourceIncludePattern;
-        this.sourceExcludePattern = sourceExcludePattern;
+        this.dataSourceIncludePattern = dataSourceIncludePattern;
+        this.dataSourceExcludePattern = dataSourceExcludePattern;
         this.inputEncoding = inputEncoding;
         this.outputEncoding = outputEncoding;
         this.verbose = verbose;
@@ -161,12 +160,12 @@
         return sharedDataModels;
     }
 
-    public String getSourceIncludePattern() {
-        return sourceIncludePattern;
+    public String getDataSourceIncludePattern() {
+        return dataSourceIncludePattern;
     }
 
-    public String getSourceExcludePattern() {
-        return sourceExcludePattern;
+    public String getDataSourceExcludePattern() {
+        return dataSourceExcludePattern;
     }
 
     public Charset getInputEncoding() {
@@ -231,8 +230,8 @@
                 ", outputGeneratorDefinitions=" + outputGeneratorDefinitions +
                 ", sharedDataSources=" + sharedDataSources +
                 ", sharedDataModels=" + sharedDataModels +
-                ", sourceIncludePattern='" + sourceIncludePattern + '\'' +
-                ", sourceExcludePattern='" + sourceExcludePattern + '\'' +
+                ", dataSourceIncludePattern='" + dataSourceIncludePattern +
+                ", dataSourceExcludePattern='" + dataSourceExcludePattern +
                 ", inputEncoding=" + inputEncoding +
                 ", outputEncoding=" + outputEncoding +
                 ", verbose=" + verbose +
@@ -275,11 +274,11 @@
             this.sourceIncludePattern = null;
             this.sourceExcludePattern = null;
             this.configuration = new Properties();
-            this.locale = DEFAULT_LOCALE.toString();
+            this.locale = FreeMarkerConstants.DEFAULT_LOCALE.toString();
             this.parameters = new HashMap<>();
             this.systemProperties = new Properties();
-            this.setInputEncoding(DEFAULT_CHARSET.name());
-            this.setOutputEncoding(DEFAULT_CHARSET.name());
+            this.setInputEncoding(FreeMarkerConstants.DEFAULT_CHARSET.name());
+            this.setOutputEncoding(FreeMarkerConstants.DEFAULT_CHARSET.name());
         }
 
         public SettingsBuilder setCommandLineArgs(String[] args) {
@@ -406,7 +405,7 @@
         private String getDefaultLocale() {
             return configuration.getProperty(
                     LOCALE_KEY,
-                    System.getProperty(LOCALE_KEY, DEFAULT_LOCALE.toString()));
+                    System.getProperty(LOCALE_KEY, FreeMarkerConstants.DEFAULT_LOCALE.toString()));
         }
     }
 }
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 ab3681e..310744a 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
@@ -48,8 +48,8 @@
 
     public static DataSourcesSupplier sharedDataSourcesSupplier(Settings settings) {
         return new DataSourcesSupplier(settings.getSharedDataSources(),
-                settings.getSourceIncludePattern(),
-                settings.getSourceExcludePattern(),
+                settings.getDataSourceIncludePattern(),
+                settings.getDataSourceExcludePattern(),
                 settings.getInputEncoding());
     }
 
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/AbstractOutputGenerator.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/AbstractOutputGenerator.java
new file mode 100644
index 0000000..81f9633
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/AbstractOutputGenerator.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.cli.config.output;
+
+import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourcesSupplier;
+import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
+import org.apache.freemarker.generator.base.util.UriUtils;
+import org.apache.freemarker.generator.cli.config.DataModelSupplier;
+import org.apache.freemarker.generator.cli.config.Settings;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+
+import java.io.BufferedWriter;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.Location.STDIN;
+import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_PLAIN;
+
+public abstract class AbstractOutputGenerator {
+
+    protected List<DataSource> dataSources(Settings settings, OutputGeneratorDefinition outputGeneratorDefinition) {
+        final ArrayList<DataSource> result = new ArrayList<>();
+
+        // Add optional data source from STDIN at the start of the list since
+        // this allows easy sequence slicing in FreeMarker.
+        if (settings.isReadFromStdin()) {
+            result.add(0, stdinDataSource());
+        }
+
+        final DataSourcesSupplier outputGeneratorDataSourcesSupplier = new DataSourcesSupplier(
+                outputGeneratorDefinition.getDataSources(),
+                settings.getDataSourceIncludePattern(),
+                settings.getDataSourceExcludePattern(),
+                settings.getInputEncoding()
+        );
+
+        result.addAll(outputGeneratorDataSourcesSupplier.get());
+
+        return result;
+    }
+
+    protected Map<String, Object> dataModels(OutputGeneratorDefinition outputGeneratorDefinition) {
+        return new DataModelSupplier(outputGeneratorDefinition.getDataModels()).get();
+    }
+
+    protected Writer stdoutWriter(Charset outputEncoding) {
+        // avoid closing System.out after rendering the template
+        return new BufferedWriter(new NonClosableWriterWrapper(new OutputStreamWriter(System.out, outputEncoding)));
+    }
+
+    private static DataSource stdinDataSource() {
+        final URI uri = UriUtils.toUri(Location.SYSTEM, STDIN);
+        return DataSourceFactory.fromInputStream(STDIN, DEFAULT_GROUP, uri, System.in, MIME_TEXT_PLAIN, UTF_8, new HashMap<>());
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGenerator.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGenerator.java
new file mode 100644
index 0000000..fc9a418
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGenerator.java
@@ -0,0 +1,112 @@
+/*
+ * 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.cli.config.output;
+
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.base.output.OutputGenerator.SeedType;
+import org.apache.freemarker.generator.base.template.TemplateOutput;
+import org.apache.freemarker.generator.base.template.TemplateSource;
+import org.apache.freemarker.generator.base.util.ListUtils;
+import org.apache.freemarker.generator.base.util.Validate;
+import org.apache.freemarker.generator.cli.config.Settings;
+import org.apache.freemarker.generator.cli.config.Suppliers;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateOutputDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
+import org.apache.freemarker.generator.cli.util.TemplateSourceFactory;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * Generates an <code>OutputGenerator</code> per <code>DataSource</code>.
+ */
+public class DataSourceSeedingOutputGenerator
+        extends AbstractOutputGenerator
+        implements Function<OutputGeneratorDefinition, List<OutputGenerator>> {
+
+    private final Settings settings;
+
+    public DataSourceSeedingOutputGenerator(Settings settings) {
+        this.settings = settings;
+    }
+
+    @Override
+    public List<OutputGenerator> apply(OutputGeneratorDefinition outputGeneratorDefinition) {
+        Validate.notNull(outputGeneratorDefinition, "outputGeneratorDefinition must not be null");
+
+        final List<OutputGenerator> result = new ArrayList<>();
+        final TemplateSourceDefinition templateSourceDefinition = outputGeneratorDefinition.getTemplateSourceDefinition();
+        final TemplateOutputDefinition templateOutputDefinition = outputGeneratorDefinition.getTemplateOutputDefinition();
+        final Map<String, Object> dataModels = super.dataModels(outputGeneratorDefinition);
+        final List<DataSource> dataSources = super.dataSources(settings, outputGeneratorDefinition);
+        final List<DataSource> sharedDataSources = Suppliers.sharedDataSourcesSupplier(settings).get();
+        final List<DataSource> combinedDataSources = ListUtils.concatenate(dataSources, sharedDataSources);
+        final TemplateSource templateSource = TemplateSourceFactory.create(templateSourceDefinition, settings.getTemplateEncoding());
+        final DataSourceSeedingOutputMapper outputMapper = outputMapper(outputGeneratorDefinition.getOutputMapper());
+
+        for (DataSource dataSource : combinedDataSources) {
+            final TemplateOutput templateOutput = templateOutput(templateOutputDefinition, settings, dataSource, outputMapper);
+            final OutputGenerator outputGenerator = new OutputGenerator(
+                    templateSource,
+                    templateOutput,
+                    singletonList(dataSource),
+                    dataModels,
+                    SeedType.DATASOURCE
+            );
+            result.add(outputGenerator);
+        }
+
+        return result;
+    }
+
+    private TemplateOutput templateOutput(TemplateOutputDefinition templateOutputDefinition, Settings settings, DataSource dataSource, DataSourceSeedingOutputMapper outputMapper) {
+        final Charset outputEncoding = settings.getOutputEncoding();
+        final File templateOutputFile = templateOutputFile(templateOutputDefinition, dataSource, outputMapper);
+        if (settings.getCallerSuppliedWriter() != null) {
+            return TemplateOutput.fromWriter(settings.getCallerSuppliedWriter());
+        } else if (templateOutputFile != null) {
+            return TemplateOutput.fromFile(templateOutputFile, outputEncoding);
+        } else {
+            return TemplateOutput.fromWriter(stdoutWriter(outputEncoding));
+        }
+    }
+
+    private File templateOutputFile(
+            TemplateOutputDefinition templateOutputDefinition,
+            DataSource dataSource,
+            DataSourceSeedingOutputMapper outputMapper) {
+
+        if (templateOutputDefinition == null || !templateOutputDefinition.hasOutput()) {
+            return null;
+        }
+
+        final File outputDirectory = new File(templateOutputDefinition.outputs.get(0));
+        return outputMapper.map(outputDirectory, dataSource);
+    }
+
+    private DataSourceSeedingOutputMapper outputMapper(String template) {
+        return new DataSourceSeedingOutputMapper(template);
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapper.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapper.java
new file mode 100644
index 0000000..43d56d5
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapper.java
@@ -0,0 +1,58 @@
+/*
+ * 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.cli.config.output;
+
+import org.apache.freemarker.generator.base.datasource.DataSource;
+
+import java.io.File;
+
+import static org.apache.freemarker.generator.base.util.StringUtils.firstNonEmpty;
+import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
+
+public class DataSourceSeedingOutputMapper {
+
+    private final String template;
+
+    public DataSourceSeedingOutputMapper(String template) {
+        this.template = template;
+    }
+
+    public File map(File outputDirectory, DataSource dataSource) {
+        final String relativeFilePath = dataSource.getRelativeFilePath();
+        final String fileName = isEmpty(template) ? fromDataSource(dataSource) : fromTemplate(template, dataSource);
+
+        return isEmpty(relativeFilePath) ?
+                new File(outputDirectory, fileName) :
+                new File(new File(outputDirectory, relativeFilePath), fileName);
+    }
+
+    private static String fromTemplate(String value, DataSource dataSource) {
+        return value.replace("*", firstNonEmpty(dataSource.getBaseName(), dataSource.getName()));
+    }
+
+    private static String fromDataSource(DataSource dataSource) {
+        if (isEmpty(dataSource.getBaseName())) {
+            return dataSource.getName();
+        } else {
+            if (isEmpty(dataSource.getExtension())) {
+                return dataSource.getBaseName();
+            } else {
+                return dataSource.getBaseName() + "." + dataSource.getExtension();
+            }
+        }
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateSeedingOutputGenerator.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateSeedingOutputGenerator.java
new file mode 100644
index 0000000..8dfa8bb
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateSeedingOutputGenerator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.cli.config.output;
+
+import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.base.output.OutputGenerator.SeedType;
+import org.apache.freemarker.generator.cli.config.Settings;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateOutputDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Generates an <code>OutputGenerator</code> per <code>Template</code>.
+ */
+public class TemplateSeedingOutputGenerator
+        extends AbstractOutputGenerator
+        implements Function<OutputGeneratorDefinition, List<OutputGenerator>> {
+
+    private final Settings settings;
+
+    public TemplateSeedingOutputGenerator(Settings settings) {
+        this.settings = requireNonNull(settings);
+    }
+
+    @Override
+    public List<OutputGenerator> apply(OutputGeneratorDefinition definition) {
+        final List<OutputGenerator> result = new ArrayList<>();
+        final TemplateSourceDefinition templateSourceDefinition = requireNonNull(definition.getTemplateSourceDefinition());
+        final TemplateOutputDefinition templateOutputDefinition = definition.getTemplateOutputDefinition();
+        final TemplateTransformationsBuilder builder = TemplateTransformationsBuilder.builder();
+
+        // set the template
+        if (templateSourceDefinition.isInteractiveTemplate()) {
+            builder.setInteractiveTemplate(templateSourceDefinition.interactiveTemplate);
+        } else {
+            builder.setTemplateSource(templateSourceDefinition.template);
+        }
+
+        // set encoding of loaded templates
+        builder.setTemplateEncoding(settings.getTemplateEncoding());
+
+        // set the writer
+        builder.setCallerSuppliedWriter(settings.getCallerSuppliedWriter());
+
+        // set template output
+        if (templateOutputDefinition != null && templateOutputDefinition.hasOutput()) {
+            builder.setOutput(templateOutputDefinition.outputs.get(0));
+        }
+
+        // set the output encoding
+        builder.setOutputEncoding(settings.getOutputEncoding());
+
+        // set template filter
+        if (definition.hasTemplateSourceIncludes()) {
+            builder.addInclude(definition.getTemplateSourceFilterDefinition().templateIncludePatterns.get(0));
+        }
+
+        if (definition.hasTemplateSourceExcludes()) {
+            builder.addExclude(definition.getTemplateSourceFilterDefinition().templateExcludePatterns.get(0));
+        }
+
+        final List<TemplateTransformation> templateTransformations = builder.build();
+
+        for (TemplateTransformation templateTransformation : templateTransformations) {
+            final OutputGenerator outputGenerator = new OutputGenerator(
+                    templateTransformation.getTemplateSource(),
+                    templateTransformation.getTemplateOutput(),
+                    dataSources(settings, definition),
+                    dataModels(definition),
+                    SeedType.TEMPLATE
+            );
+            result.add(outputGenerator);
+        }
+
+        return result;
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformation.java
similarity index 89%
rename from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
rename to freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformation.java
index 6f0ed90..74a1522 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformation.java
@@ -14,7 +14,10 @@
  * 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.cli.config.output;
+
+import org.apache.freemarker.generator.base.template.TemplateOutput;
+import org.apache.freemarker.generator.base.template.TemplateSource;
 
 import static java.util.Objects.requireNonNull;
 
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilder.java
similarity index 97%
rename from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
rename to freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilder.java
index c9b0814..2e9d04b 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilder.java
@@ -14,13 +14,15 @@
  * 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.cli.config.output;
 
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
 import org.apache.freemarker.generator.base.datasource.DataSource;
 import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
 import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
 import org.apache.freemarker.generator.base.file.RecursiveFileSupplier;
+import org.apache.freemarker.generator.base.template.TemplateOutput;
+import org.apache.freemarker.generator.base.template.TemplateSource;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
 import org.apache.freemarker.generator.base.util.StringUtils;
 import org.apache.freemarker.generator.base.util.Validate;
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
index 0c4ffcf..f586feb 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
@@ -27,4 +27,11 @@
 
     @Option(names = { "-m", "--data-model" }, description = "data model used for rendering")
     public List<String> dataModels;
+
+    @Override
+    public String toString() {
+        return "DataModelDefinition{" +
+                "dataModels=" + dataModels +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
index 61e9924..0adc292 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
@@ -27,4 +27,11 @@
 
     @Option(names = { "-s", "--data-source" }, description = "data source used for rendering")
     public List<String> dataSources;
+
+    @Override
+    public String toString() {
+        return "DataSourceDefinition{" +
+                "dataSources=" + dataSources +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/GitVersionProvider.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/GitVersionProvider.java
index bfa3f3d..ce8555e 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/GitVersionProvider.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/GitVersionProvider.java
@@ -53,4 +53,13 @@
         }
         return properties;
     }
+
+    @Override
+    public String toString() {
+        return "GitVersionProvider{" +
+                "gitBuildVersion='" + gitBuildVersion + '\'' +
+                ", gitCommitId='" + gitCommitId + '\'' +
+                ", gitCommitTime='" + gitCommitTime + '\'' +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
index 532cf42..728d6a7 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
@@ -16,6 +16,7 @@
  */
 package org.apache.freemarker.generator.cli.picocli;
 
+import org.apache.freemarker.generator.base.FreeMarkerConstants.SeedType;
 import picocli.CommandLine;
 import picocli.CommandLine.ArgGroup;
 import picocli.CommandLine.ParameterException;
@@ -24,6 +25,9 @@
 
 import static java.util.Collections.emptyList;
 
+/**
+ * Collects the setting for an output generator.
+ */
 public class OutputGeneratorDefinition {
 
     @ArgGroup(multiplicity = "1")
@@ -41,6 +45,12 @@
     @ArgGroup(exclusive = false)
     public DataModelDefinition dataModelDefinition;
 
+    @ArgGroup(exclusive = false)
+    public OutputSeedDefinition outputSeedDefinition;
+
+    @ArgGroup(exclusive = false)
+    public OutputMapperDefinition outputMapperDefinition;
+
     public void validate(CommandLine commandLine) {
         if (templateSourceDefinition == null) {
             throw new ParameterException(commandLine, "No template defined to be rendered");
@@ -99,6 +109,31 @@
                 !getTemplateSourceFilterDefinition().templateExcludePatterns.isEmpty();
     }
 
+    public String getOutputSeedType() {
+        if (outputSeedDefinition != null && outputSeedDefinition.type != null) {
+            return outputSeedDefinition.type;
+        } else {
+            return SeedType.TEMPLATE;
+        }
+    }
+
+    public String getOutputMapper() {
+        return (outputMapperDefinition != null) ? outputMapperDefinition.outputMapper : null;
+    }
+
+    @Override
+    public String toString() {
+        return "OutputGeneratorDefinition{" +
+                "templateSourceDefinition=" + templateSourceDefinition +
+                ", templateSourceFilterDefinition=" + templateSourceFilterDefinition +
+                ", templateOutputDefinition=" + templateOutputDefinition +
+                ", dataSourceDefinition=" + dataSourceDefinition +
+                ", dataModelDefinition=" + dataModelDefinition +
+                ", outputSeedDefinition=" + outputSeedDefinition +
+                ", outputMapperDefinition=" + outputMapperDefinition +
+                '}';
+    }
+
     private static boolean isFileSource(String source) {
         if (source.contains("file://")) {
             return true;
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputMapperDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputMapperDefinition.java
new file mode 100644
index 0000000..c4bed1a
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputMapperDefinition.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cli.picocli;
+
+import picocli.CommandLine.Option;
+
+public class OutputMapperDefinition {
+
+    @Option(names = { "--output-mapper" }, description = "maps the name of the output file")
+    public String outputMapper;
+
+    @Override
+    public String toString() {
+        return "OutputMapperDefinition{" +
+                "outputMapper='" + outputMapper + '\'' +
+                '}';
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputSeedDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputSeedDefinition.java
new file mode 100644
index 0000000..50cea95
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputSeedDefinition.java
@@ -0,0 +1,35 @@
+/*
+ * 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.cli.picocli;
+
+import picocli.CommandLine.Option;
+
+/**
+ * Seeding of template output.
+ */
+public class OutputSeedDefinition {
+
+    @Option(names = { "--seed" }, defaultValue = "template", description = "seed mode: [template|datasource]")
+    public String type;
+
+    @Override
+    public String toString() {
+        return "OutputSeedDefinition{" +
+                "type='" + type + '\'' +
+                '}';
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
index 59627e3..3f3244f 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
@@ -28,4 +28,11 @@
     public boolean hasOutput() {
         return outputs != null && !outputs.isEmpty();
     }
+
+    @Override
+    public String toString() {
+        return "TemplateOutputDefinition{" +
+                "outputs=" + outputs +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceDefinition.java
index 6193c7b..0d35752 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceDefinition.java
@@ -29,4 +29,12 @@
     public boolean isInteractiveTemplate() {
         return interactiveTemplate != null && !interactiveTemplate.isEmpty();
     }
+
+    @Override
+    public String toString() {
+        return "TemplateSourceDefinition{" +
+                "template='" + template + '\'' +
+                ", interactiveTemplate='" + interactiveTemplate + '\'' +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceFilterDefinition.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceFilterDefinition.java
index 68cce4e..0a3bb4b 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceFilterDefinition.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceFilterDefinition.java
@@ -20,7 +20,9 @@
 
 import java.util.List;
 
-/** Include/exclude pattern when processing template directories */
+/**
+ * Include/exclude pattern when processing template directories
+ */
 public class TemplateSourceFilterDefinition {
 
     @Option(names = { "--template-include" }, description = "template include pattern")
@@ -28,4 +30,12 @@
 
     @Option(names = { "--template-exclude" }, description = "template exclude pattern")
     public List<String> templateExcludePatterns;
+
+    @Override
+    public String toString() {
+        return "TemplateSourceFilterDefinition{" +
+                "templateIncludePatterns=" + templateIncludePatterns +
+                ", templateExcludePatterns=" + templateExcludePatterns +
+                '}';
+    }
 }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
index 4ae0377..8e158e4 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
@@ -23,8 +23,11 @@
 import org.apache.freemarker.generator.base.datasource.DataSource;
 import org.apache.freemarker.generator.base.datasource.DataSources;
 import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.base.output.OutputGenerator.SeedType;
 import org.apache.freemarker.generator.base.template.TemplateOutput;
 import org.apache.freemarker.generator.base.template.TemplateSource;
+import org.apache.freemarker.generator.base.util.ListUtils;
+import org.apache.freemarker.generator.base.util.Validate;
 
 import java.io.BufferedWriter;
 import java.io.File;
@@ -33,14 +36,11 @@
 import java.io.OutputStreamWriter;
 import java.io.Writer;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import static java.util.Objects.requireNonNull;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
@@ -50,7 +50,7 @@
  */
 public class FreeMarkerTask implements Callable<Integer> {
 
-    private static final int SUCCESS = 0;
+    private static final int SUCCESS_CODE = 0;
 
     private final Supplier<Configuration> configurationSupplier;
     private final Supplier<List<OutputGenerator>> outputGeneratorsSupplier;
@@ -85,7 +85,7 @@
                 sharedDataSources,
                 sharedParameters));
 
-        return SUCCESS;
+        return SUCCESS_CODE;
     }
 
     private void process(Configuration configuration,
@@ -102,22 +102,43 @@
         try (Writer writer = writer(templateOutput)) {
             final Template template = template(configuration, templateSource);
             template.process(templateDataModel, writer);
-        } catch (TemplateException | IOException e) {
+        } catch (TemplateException | IOException | RuntimeException e) {
             throw new RuntimeException("Failed to process template: " + templateSource.getName(), e);
         }
     }
 
+    /**
+     * Merge the <code>DataSourced</code>.
+     * The data sources to be used are determined by the seed type
+     * <ul>
+     *     <li>TEMPLATE: aggregates a list of data source</li>
+     *     <li>DATASOURCE: only takes a single data source</li>
+     * </ul>
+     *
+     * @param outputGenerator   current output generator
+     * @param sharedDataSources shared data sources
+     * @return <code>DataSources</code> to be passed to FreeMarker
+     */
     private static DataSources toDataSources(OutputGenerator outputGenerator, List<DataSource> sharedDataSources) {
-        return new DataSources(Stream.of(outputGenerator.getDataSources(), sharedDataSources)
-                .flatMap(Collection::stream).collect(Collectors.toList()));
+        final List<DataSource> dataSources = outputGenerator.getDataSources();
+        final SeedType seedType = outputGenerator.getSeedType();
+
+        if (seedType == SeedType.TEMPLATE) {
+            return new DataSources(ListUtils.concatenate(dataSources, sharedDataSources));
+        } else if (seedType == SeedType.DATASOURCE) {
+            // Since every data source shall generate an output there can be only 1 datasource supplied.
+            Validate.isTrue(dataSources.size() == 1, "One data source expected for generation driven by data sources");
+            return new DataSources(dataSources);
+        } else {
+            throw new IllegalArgumentException("Don't know how to handle the seed type: " + seedType);
+        }
     }
 
     @SafeVarargs
     private static Map<String, Object> toTemplateDataModel(DataSources dataSources, Map<String, Object>... maps) {
         final Map<String, Object> result = new HashMap<>();
         Arrays.stream(maps).forEach(result::putAll);
-        // expose only the map and not the "DataSources" instance (see FREEMARKER-174)
-        result.put(Model.DATASOURCES, dataSources.toMap());
+        result.put(Model.DATASOURCES, dataSources);
         return result;
     }
 
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/util/TemplateSourceFactory.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/util/TemplateSourceFactory.java
new file mode 100644
index 0000000..606f57c
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/util/TemplateSourceFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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.cli.util;
+
+import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
+import org.apache.freemarker.generator.base.template.TemplateSource;
+import org.apache.freemarker.generator.base.util.Validate;
+import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
+
+import java.io.File;
+import java.nio.charset.Charset;
+
+public class TemplateSourceFactory {
+
+    public static TemplateSource create(TemplateSourceDefinition templateSourceDefinition, Charset charset) {
+        Validate.notNull(templateSourceDefinition, "templateSourceDefinition must not be null");
+        Validate.notNull(charset, "charset must not be null");
+
+        final String template = templateSourceDefinition.template;
+        final DataSourceLoader dataSourceLoader = DataSourceLoaderFactory.create();
+
+        if (templateSourceDefinition.isInteractiveTemplate()) {
+            return TemplateSource.fromCode(Location.INTERACTIVE, templateSourceDefinition.interactiveTemplate);
+        } else if (new File(template).exists()) {
+            final String templateSource = templateSourceDefinition.template;
+            try (DataSource dataSource = dataSourceLoader.load(templateSource)) {
+                return TemplateSource.fromCode(dataSource.getName(), dataSource.getText(charset.name()));
+            }
+        } else {
+            return TemplateSource.fromPath(template, charset);
+        }
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/DataSourcesAdapter.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/DataSourcesAdapter.java
new file mode 100644
index 0000000..de25ed4
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/DataSourcesAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * 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.cli.wrapper;
+
+import freemarker.ext.util.WrapperTemplateModel;
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.MapKeyValuePairIterator;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.SimpleCollection;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelWithAPISupport;
+import freemarker.template.TemplateSequenceModel;
+import freemarker.template.WrappingTemplateModel;
+import freemarker.template.utility.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.generator.base.datasource.DataSources;
+
+import java.io.Serializable;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Wraps an instance of <code>DataSources</code> into a FreeMarker template model
+ * providing sequence and hash type access. If required the <code>DataSources</code>
+ * API can be accessed using FreeMarkers "?api" built-in.
+ */
+public class DataSourcesAdapter extends WrappingTemplateModel
+        implements TemplateHashModelEx2, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport,
+        TemplateSequenceModel, Serializable {
+
+    /** Wrapped instance */
+    private final DataSources dataSources;
+
+    /**
+     * Factory method for creating new adapter instances.
+     *
+     * @param dataSources The dataSources to adapt; can't be {@code null}.
+     * @param wrapper     The {@link ObjectWrapper} used to wrap the items in the array.
+     * @return adapter
+     */
+    public static DataSourcesAdapter create(DataSources dataSources, ObjectWrapperWithAPISupport wrapper) {
+        return new DataSourcesAdapter(dataSources, wrapper);
+    }
+
+    private DataSourcesAdapter(DataSources dataSources, ObjectWrapper wrapper) {
+        super(requireNonNull(wrapper));
+        this.dataSources = requireNonNull(dataSources);
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        return wrap(dataSources.toMap().get(key));
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return dataSources.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return dataSources.size();
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new SimpleCollection(dataSources.toMap().keySet(), getObjectWrapper());
+    }
+
+    @Override
+    public TemplateCollectionModel values() {
+        return new SimpleCollection(dataSources.toMap().values(), getObjectWrapper());
+    }
+
+    @Override
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(dataSources.toMap(), getObjectWrapper());
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        return index >= 0 && index < dataSources.size() ? wrap(dataSources.get(index)) : null;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return dataSources;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return dataSources;
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(dataSources);
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/FreeMarkerGeneratorObjectWrapper.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/FreeMarkerGeneratorObjectWrapper.java
new file mode 100644
index 0000000..102a84e
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/wrapper/FreeMarkerGeneratorObjectWrapper.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cli.wrapper;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.Version;
+import org.apache.freemarker.generator.base.datasource.DataSources;
+
+public class FreeMarkerGeneratorObjectWrapper extends DefaultObjectWrapper {
+
+    public FreeMarkerGeneratorObjectWrapper(Version incompatibleImprovements) {
+        super(incompatibleImprovements);
+    }
+
+    @Override
+    protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException {
+        if (obj instanceof DataSources) {
+            return DataSourcesAdapter.create((DataSources) obj, this);
+        }
+
+        return super.handleUnknownType(obj);
+    }
+}
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-models.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-models.md
index bd56d76..42b048f 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-models.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-models.md
@@ -43,7 +43,7 @@
 post title is: qui est esse
 ```
 
-Expose all environment variables as `env` in theFreeMarker model
+Expose all environment variables as `env` in the FreeMarker model
  
 ```
 > freemarker-generator --data-model env=env:/// -i '<#list env as name,value>${name}=${value}${"\n"}</#list>'
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
index f46f71d..f04675d 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
@@ -96,9 +96,9 @@
 
 ### Selecting A DataSource
 
-After loading one or more `DataSource` they are accessible as `dataSource` map in the FreeMarker model
+After loading one or more `DataSource` they are accessible as `dataSources` map in the FreeMarker model
 
-* `dataSources?values[0]` or `dataSources?values?first` selects the first data source
+* `dataSources[0]` or `dataSources?first` selects the first data source
 * `dataSources["user.csv"]` selects the data source with the name "user.csv"
 
 ### Iterating Over DataSources
@@ -122,7 +122,7 @@
 </#list>
 
 <#-- Iterate over a list of data sources -->
-<#list dataSources?values as dataSource>
+<#list dataSources as dataSource>
 - [#${dataSource?counter}]: name=${dataSource.name}
 </#list>
 ```
@@ -134,22 +134,22 @@
 
 ```
 <#-- List all data sources containing "test" in the name -->
-<#list dataSources?values?filter(ds -> ds.match("name", "*test*")) as ds>
+<#list dataSources?filter(ds -> ds.match("name", "*test*")) as ds>
 - ${ds.name}
 </#list>
 
 <#-- List all data sources having "json" extension -->
-<#list dataSources?values?filter(ds -> ds.match("extension", "json")) as ds>
+<#list dataSources?filter(ds -> ds.match("extension", "json")) as ds>
 - ${ds.name}
 </#list>
 
 <#-- List all data sources having "src/test/data/properties" in their file path -->
-<#list dataSources?values?filter(ds -> ds.match("filePath", "*/src/test/data/properties")) as ds>
+<#list dataSources?filter(ds -> ds.match("filePath", "*/src/test/data/properties")) as ds>
 - ${ds.name}
 </#list>
 
 <#-- List all data sources of a group -->
-<#list dataSources?values?filter(ds -> ds.match("group", "default")) as ds>
+<#list dataSources?filter(ds -> ds.match("group", "default")) as ds>
 - ${ds.name}
 </#list>
 
@@ -163,7 +163,7 @@
 Invoke Arbitrary Methods On DataSource
 ---------------------------------------------------------------------------
 <#if dataSources?has_content>
-<#assign dataSource=dataSources?values?first>
+<#assign dataSource=dataSources?first>
 Name            : ${dataSource.name}
 Nr of lines     : ${dataSource.lines?size}
 Content Type    : ${dataSource.contentType}
@@ -199,10 +199,10 @@
 ---------------------------------------------------------------------------
 extension       : csv
 filename        : contract.csv
-basename        : contract
-filepath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv
+baseName        : contract
+filePath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv
 name            : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv/contract.csv
-mimetype        : text/csv
+mimeType        : text/csv
 uri             : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv/contract.csv
 group           : default
 ```
@@ -230,16 +230,16 @@
 Nr of chars     : 12,643
 Nr of bytes     : 12,643
 File name       : transactions.csv
-URI schema      : file
+URI scheme      : file
 
 Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
 extension       : csv
-basename        : transactions
-filename        : transactions.csv
-filepath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv
+baseName        : transactions
+fileName        : transactions.csv
+filePath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv
 name            : transactions
-mimetype        : text/csv
+mimeType        : text/csv
 uri             : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv/transactions.csv
 group           : csv
 
@@ -261,16 +261,16 @@
 Nr of chars     : 330
 Nr of bytes     : 330
 File name       :
-URI schema      : https
+URI scheme      : https
 
 Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
 extension       :
-basename        :
-filename        :
-filepath        : /
+baseName        :
+fileName        :
+filePath        : /
 name            : https://xkcd.com/info.0.json
-mimetype        : application/json
+mimeType        : application/json
 uri             : https://xkcd.com/info.0.json
 group           : default
 
@@ -291,23 +291,21 @@
 Nr of chars     : 1,476
 Nr of bytes     : 1,478
 File name       :
-URI schema      : env
+URI scheme     : env
 
 Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
 extension       :
-basename        :
-filename        :
-filepath        : /
+baseName        :
+fileName        :
+filePath        : /
 name            : envvars
-mimetype        : text/plain
+mimeType        : text/plain
 uri             : env:///
 group           : default
 
 Iterating Over Properties Of A Datasource
 ---------------------------------------------------------------------------
-
-
 ```
 
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/named-uris.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/named-uris.md
index 5fecc46..bb84d92 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/named-uris.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/named-uris.md
@@ -93,7 +93,7 @@
 or use a charset for all files of a directory
 
 ```text
-freemarker-generator -t freemarker-generator/info.ftl 'examples/data/csv#charset=UTF-16&mimetype=text/plain'
+freemarker-generator -t freemarker-generator/info.ftl 'examples/data/csv#charset=UTF-16&mimeType=text/plain'
 
 FreeMarker Generator DataSources
 ------------------------------------------------------------------------------
@@ -123,16 +123,16 @@
 Nr of chars     : 376
 Nr of bytes     : 376
 File name       : user.csv
-URI schema      : file
+URI scheme      : file
 
 Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
 extension       : csv
-basename        : user
-filename        : user.csv
-filepath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv
+baseName        : user
+fileName        : user.csv
+filePath        : /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv
 name            : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv/user.csv
-mimetype        : text/csv
+mimeType        : text/csv
 uri             : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/csv/user.csv
 group           : default
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
index 9b20e10..b6ce911 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
@@ -3,15 +3,17 @@
 The `freemarker-generator` generates text output based on processing FreeMarker templates and data 
 
 * A command line invocation requires 1..n `templates` and 0..n `data sources` / `data models` 
-* A command line invocation is internally mapped to a list of `output generators`
+* A command line invocation is internally mapped to a list of `output generators` - the mapping is controlled by the `seed`
+  * `template` transforms 0 ..n `data sources` using a `template` to an `output`
+  * `datasource` applies a `template` for 1..n `data sources` generating 1..n `outputs`
 * The `output generator` consists of exactly one `template`, 0..n `data sources` / `data models` written to an `output` destination
 * The `output` of an `output generator` is either written to
     * `stdout`
-    * an output file
-    * an output directory
+    * An output file
+    * An output directory
 * When the output is written to a directory
-    * the structure of the input directory is preserved
-    * any `ftl` file extension is removed
+    * The structure of the input directory is preserved
+    * Any `ftl` file extension is removed
 * Positional command line arguments are interpreted as `data sources` (or directories) and accessible by a all `output generators`   
 
 ### Examples
@@ -25,7 +27,7 @@
 Transform multiple templates to multiple output files (1:1 mapping between templates and outputs)
 
 ```
-freemarker-generator \
+> freemarker-generator \
 -t freemarker-generator/csv/md/transform.ftl -o target/contract.md \
 -t freemarker-generator/csv/html/transform.ftl -o target/contract.html \
 examples/data/csv/contract.csv
@@ -40,7 +42,8 @@
 
 ```
 > freemarker-generator \
--t examples/data/template -o target/template1
+-t examples/data/template \
+-o target/template1
 
 > tree target     
 target
@@ -53,7 +56,7 @@
 Transforming multiple template directories to multiple output directories
 
 ```
-freemarker-generator \
+> freemarker-generator \
 -t examples/data/template -o target/template1 \
 -t examples/data/template -o target/template2 
 
@@ -69,6 +72,31 @@
         `-- nginx.conf
 ```
 
+Transforming multiple `data sources` to multiple output files (aka source code generation)
+
+```
+> freemarker-generator \
+	--seed=datasource \
+	--template freemarker-generator/csv/html/transform.ftl \
+	--data-source . \
+	--data-source-include="*.csv" \
+	--output target \
+	--output-mapper="*.html"
+
+> tree target               
+target
+`-- examples
+    `-- data
+        `-- csv
+            |-- contract.html
+            |-- dataframe.html
+            |-- excel-export-utf8.html
+            |-- locker-test-users.html
+            |-- sales-records.html
+            |-- transactions.html
+            `-- user.html
+```
+
 Defining multiple transformation on the command line can be clumsy but [Picolic's @-Files](https://picocli.info/#AtFiles) can help - the following `@-File` defined, e.g. `@examples.args`
 
 ```
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/parsing-with-grok.md b/freemarker-generator-cli/src/site/markdown/cli/usage/parsing-with-grok.md
index 5cbadda..7f4404c 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/parsing-with-grok.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/parsing-with-grok.md
@@ -33,7 +33,7 @@
 ```text
 <#ftl output_format="plainText" strip_whitespace=true>
 <#assign grok = tools.grok.create("%{COMBINEDAPACHELOG}")>
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign lines = dataSource.getLineIterator()>
 
 <#compress>
@@ -93,7 +93,7 @@
 <#compress>
     TIMESTAMP;MILLIS
     <#if dataSources?has_content>
-        <#list dataSources?values as dataSource>
+        <#list dataSources as dataSource>
             <#list dataSource.getLineIterator() as line>
                 <#assign parts = grok.match(line).capture()>
                 <#if parts?has_content>
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/running-examples.md b/freemarker-generator-cli/src/site/markdown/cli/usage/running-examples.md
index e4cc875..ab46cea 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/running-examples.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/running-examples.md
@@ -6,72 +6,44 @@
 
 ```text
 ./run-examples.sh 
-templates/info.ftl
+templates/freemarker-generator/info.ftl
 examples/templates/demo.ftl
-templates/csv/html/transform.ftl
-templates/csv/md/transform.ftl
+examples/templates/datasources.ftl
+examples/templates/datasources.ftl
+templates/freemarker-generator/csv/confluence/transform.ftl
+templates/freemarker-generator/csv/html/transform.ftl
+templates/freemarker-generator/csv/md/transform.ftl
 examples/templates/csv/shell/curl.ftl
 examples/templates/csv/md/filter.ftl
 examples/templates/csv/fo/transform.ftl
 fop -fo target/out/locker-test-users.fo target/out/locker-test-users.pdf
 examples/templates/csv/fo/transactions.ftl
 fop -fo target/out/transactions.fo target/out/transactions-fo.pdf
-templates/csv/html/transform.ftl
+examples/templates/csv/html/transform.ftl
 wkhtmltopdf -O landscape target/out/transactions.html target/out/transactions-html.pdf
 examples/templates/dataframe/example.ftl
 examples/templates/accesslog/combined-access.ftl
+examples/templates/logs/csv/serverlog-to-csv.ftl
 examples/templates/excel/dataframe/transform.ftl
-templates/excel/html/transform.ftl
-templates/excel/md/transform.ftl
-templates/excel/csv/transform.ftl
+templates/freemarker-generator/excel/html/transform.ftl
+templates/freemarker-generator/excel/md/transform.ftl
+templates/freemarker-generator/excel/csv/transform.ftl
 examples/templates/excel/csv/custom.ftl
 examples/templates/html/csv/dependencies.ftl
+examples/templates/html/txt/licence.ftl
+examples/templates/javafaker/csv/testdata.ftl
 examples/templates/json/csv/swagger-endpoints.ftl
-templates/json/yaml/transform.ftl
+templates/freemarker-generator/json/yaml/transform.ftl
 examples/templates/json/md/github-users.ftl
 examples/templates/properties/csv/locker-test-users.ftl
 examples/data/template
-examples/templates/yaml/txt/transform.ftl
-templates/yaml/json/transform.ftl
+examples/templates/utahparser/csv/transform.ftl
+examples/templates/utahparser/json/transform.ftl
 examples/templates/xml/txt/recipients.ftl
-Created the following sample files in ./target/out
-total 1464
--rw-r--r--  1 sgoeschl  staff     646 Jun 27 16:38 combined-access.log.txt
--rw-r--r--  1 sgoeschl  staff   25676 Jun 27 16:38 contract.html
--rw-r--r--  1 sgoeschl  staff    7933 Jun 27 16:38 contract.md
--rw-r--r--  1 sgoeschl  staff     784 Jun 27 16:38 curl.sh
--rw-r--r--  1 sgoeschl  staff     232 Jun 27 16:38 customer.txt
--rw-r--r--  1 sgoeschl  staff    6486 Jun 27 16:38 dataframe.txt
--rw-r--r--  1 sgoeschl  staff   15613 Jun 27 16:38 demo.txt
--rw-r--r--  1 sgoeschl  staff    1310 Jun 27 16:38 dependencies.csv
--rw-r--r--  1 sgoeschl  staff    2029 Jun 27 16:38 github-users-curl.md
--rw-r--r--  1 sgoeschl  staff    2627 Jun 27 16:38 info.txt
--rw-r--r--  1 sgoeschl  staff    8075 Jun 27 16:38 interactive-dataframe.txt
--rw-r--r--  1 sgoeschl  staff      66 Jun 27 16:38 interactive-html.txt
--rw-r--r--  1 sgoeschl  staff      16 Jun 27 16:38 interactive-json.txt
--rw-r--r--  1 sgoeschl  staff   25090 Jun 27 16:38 interactive-swagger.json
--rw-r--r--  1 sgoeschl  staff   16870 Jun 27 16:38 interactive-swagger.yaml
--rw-r--r--  1 sgoeschl  staff      10 Jun 27 16:38 interactive-xml.txt
--rw-r--r--  1 sgoeschl  staff     285 Jun 27 16:38 locker-test-users.csv
--rw-r--r--  1 sgoeschl  staff    6341 Jun 27 16:38 locker-test-users.fo
--rw-r--r--  1 sgoeschl  staff    5526 Jun 27 16:38 locker-test-users.pdf
--rw-r--r--  1 sgoeschl  staff     921 Jun 27 16:38 recipients.txt
--rw-r--r--  1 sgoeschl  staff     910 Jun 27 16:38 sales-records.md
--rw-r--r--  1 sgoeschl  staff    2453 Jun 27 16:38 swagger-spec.csv
--rw-r--r--  1 sgoeschl  staff   25090 Jun 27 16:38 swagger-spec.json
--rw-r--r--  1 sgoeschl  staff   16870 Jun 27 16:38 swagger-spec.yaml
-drwxr-xr-x  4 sgoeschl  staff     128 Jun 27 16:38 template
--rw-r--r--  1 sgoeschl  staff     154 Jun 27 16:38 test-multiple-sheets.xlsx.csv
--rw-r--r--  1 sgoeschl  staff    1917 Jun 27 16:38 test-multiple-sheets.xlsx.html
--rw-r--r--  1 sgoeschl  staff     389 Jun 27 16:38 test-multiple-sheets.xlsx.md
--rw-r--r--  1 sgoeschl  staff     155 Jun 27 16:38 test-transform-xls.csv
--rw-r--r--  1 sgoeschl  staff    1439 Jun 27 16:38 test.xls.dataframe.txt
--rw-r--r--  1 sgoeschl  staff    1556 Jun 27 16:38 test.xls.html
--rw-r--r--  1 sgoeschl  staff    1558 Jun 27 16:38 test.xslx.html
--rw-r--r--  1 sgoeschl  staff   25757 Jun 27 16:38 transactions-fo.pdf
--rw-r--r--  1 sgoeschl  staff   66016 Jun 27 16:38 transactions-html.pdf
--rw-r--r--  1 sgoeschl  staff  330128 Jun 27 16:38 transactions.fo
--rw-r--r--  1 sgoeschl  staff   51008 Jun 27 16:38 transactions.html
+examples/templates/yaml/txt/transform.ftl
+templates/freemarker-generator/yaml/json/transform.ftl
+examples/data/csv
+examples/data/csv
 ```
 
 Please note that generated PDF files are very likely not found since they require `wkhtmltopdf` and `Apache FOP` installation.
@@ -92,7 +64,7 @@
 
 ```text
 <#ftl output_format="plainText" >
-<#assign json = tools.jsonpath.parse(dataSources?values[0])>
+<#assign json = tools.jsonpath.parse(dataSources[0])>
 <#assign users = json.read("$[*]")>
 <#--------------------------------------------------------------------------->
 # GitHub Users
@@ -130,7 +102,7 @@
 ```text
 <#ftl output_format="plainText">
 <#assign cvsFormat = tools.csv.formats["DEFAULT"].withHeader()>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign csvHeaders = csvParser.getHeaderMap()?keys>
 <#assign csvRecords = csvParser.records>
 <#--------------------------------------------------------------------------->
@@ -167,7 +139,7 @@
 
 ```text
 <#ftl output_format="plainText" >
-<#assign xml = tools.xml.parse(dataSources?values[0])>
+<#assign xml = tools.xml.parse(dataSources[0])>
 <#list xml.recipients.person as recipient>
 To: ${recipient.name}
 ${recipient.address}
@@ -214,7 +186,7 @@
 
 ```text
 <#ftl output_format="plainText" strip_text="true">
-<#assign json = tools.jsonpath.parse(dataSources?values[0])>
+<#assign json = tools.jsonpath.parse(dataSources[0])>
 <#assign basePath = json.read("$.basePath")>
 <#assign paths = json.read("$.paths")>
 
@@ -276,7 +248,7 @@
 
 ```text
 <#ftl output_format="HTML" >
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign workbook = tools.excel.parse(dataSource)>
 <#assign date = .now?iso_utc>
@@ -402,7 +374,7 @@
 
 ```text
 <#ftl output_format="XML" >
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign name = dataSource.name>
 <#assign cvsFormat = tools.csv.formats.DEFAULT.withDelimiter('\t').withHeader()>
 <#assign csvParser = tools.csv.parse(dataSource, cvsFormat)>
@@ -523,7 +495,7 @@
 
 ```text
 <#ftl output_format="plainText" strip_text="true">
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign html = tools.jsoup.parse(dataSource)>
 
 <#compress>
@@ -598,7 +570,7 @@
 ```text
 <#ftl output_format="plainText">
 <#assign cvsFormat = tools.csv.formats["DEFAULT"].withHeader()>
-<#assign csvParser = tools.csv.parse(dataSources?values[0], cvsFormat)>
+<#assign csvParser = tools.csv.parse(dataSources[0], cvsFormat)>
 <#assign records = csvParser.records>
 <#assign csvMap = tools.csv.toMap(records, "disposer")>
 <#--------------------------------------------------------------------------->
@@ -713,16 +685,16 @@
 > bin/freemarker-generator -i 'Hello ${tools.system.envs["USER"]}'; echo
 Hello sgoeschl
 
-> bin/freemarker-generator -i '${tools.jsonpath.parse(dataSources?values[0]).read("$.info.title")}' examples/data/json/swagger-spec.json; echo
+> bin/freemarker-generator -i '${tools.jsonpath.parse(dataSources[0]).read("$.info.title")}' examples/data/json/swagger-spec.json; echo
 Swagger Petstore
 
-> bin/freemarker-generator -i 'Post Title : ${tools.jsonpath.parse(dataSources?values[0]).read("$.title")}' https://jsonplaceholder.typicode.com/posts/2; echo
+> bin/freemarker-generator -i 'Post Title : ${tools.jsonpath.parse(dataSources[0]).read("$.title")}' https://jsonplaceholder.typicode.com/posts/2; echo
 Post Title : qui est esse
 
-> bin/freemarker-generator -i '${tools.xml.parse(dataSources?values[0])["recipients/person[1]/name"]}' examples/data/xml/recipients.xml; echo
+> bin/freemarker-generator -i '${tools.xml.parse(dataSources[0])["recipients/person[1]/name"]}' examples/data/xml/recipients.xml; echo
 John Smith
 
-> bin/freemarker-generator -i '${tools.jsoup.parse(dataSources?values[0]).select("a")[0]}' examples/data/html/dependencies.html; echo
+> bin/freemarker-generator -i '${tools.jsoup.parse(dataSources[0]).select("a")[0]}' examples/data/html/dependencies.html; echo
 <a href="${project.url}" title="FreeMarker Generator">FreeMarker Generator</a>
 
 > freemarker-generator -i '<#list tools.system.envs as name,value>${name} ==> ${value}${"\n"}</#list>'
@@ -749,7 +721,7 @@
 
 ```text
 <#ftl output_format="plainText" strip_text="true">
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign parser = parser(dataSource)>
 <#assign headers = parser.getHeaderNames()>
 <#assign column = tools.system.getParameter("column")>
@@ -835,11 +807,11 @@
 
 ```
 > freemarker-generator -t freemarker-generator/yaml/json/transform.ftl examples/data/yaml/swagger-spec.yaml 
-> freemarker-generator -i '${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))}' examples/data/yaml/swagger-spec.yaml
+> freemarker-generator -i '${tools.gson.toJson(tools.yaml.parse(dataSources[0]))}' examples/data/yaml/swagger-spec.yaml
 > freemarker-generator -i '${tools.gson.toJson(yaml)}' -m yaml=examples/data/yaml/swagger-spec.yaml
 
 > freemarker-generator -t freemarker-generator/json/yaml/transform.ftl examples/data/json/swagger-spec.json
-> freemarker-generator -i '${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))}' examples/data/json/swagger-spec.json
+> freemarker-generator -i '${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))}' examples/data/json/swagger-spec.json
 > freemarker-generator -i '${tools.yaml.toYaml(json)}' -m json=examples/data/json/swagger-spec.json
 ```
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/using-dataframes.md b/freemarker-generator-cli/src/site/markdown/cli/usage/using-dataframes.md
index 9fbf9d2..8866983 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/using-dataframes.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/using-dataframes.md
@@ -30,7 +30,7 @@
 and create a `DateFrame` using the following code snippet
 
 ```
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign csvParser = tools.csv.parse(dataSource, tools.csv.formats["DATAFRAME"])>
 <#assign users = tools.dataframe.fromCSVParser(csvParser)>
 ```
@@ -162,7 +162,7 @@
 
 ```
 freemarker-generator \
--i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources?values[0])))}' \
+-i '${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources[0])))}' \
 examples/data/json/github-users.json
 
 ┌────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┬────────────┐
@@ -189,7 +189,7 @@
 Let's transform an Excel Sheet to a `DataFrame` being printed using the following template
 
 ```
-<#assign dataSource = dataSources?values[0]>
+<#assign dataSource = dataSources[0]>
 <#assign workbook = tools.excel.parse(dataSource)>
 <#list tools.excel.getSheets(workbook) as sheet>
     <#assign table = tools.excel.toTable(sheet)>
diff --git a/freemarker-generator-base/src/test/template/application.properties b/freemarker-generator-cli/src/test/data/template/application.properties
similarity index 100%
rename from freemarker-generator-base/src/test/template/application.properties
rename to freemarker-generator-cli/src/test/data/template/application.properties
diff --git a/freemarker-generator-base/src/test/template/nginx/nginx.conf.ftl b/freemarker-generator-cli/src/test/data/template/nginx/nginx.conf.ftl
similarity index 100%
rename from freemarker-generator-base/src/test/template/nginx/nginx.conf.ftl
rename to freemarker-generator-cli/src/test/data/template/nginx/nginx.conf.ftl
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 8b84c71..1a09d23 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
@@ -49,11 +49,13 @@
 
     @Test
     public void shouldRunDataSourceExamples() throws IOException {
+        assertValid(execute("-t src/app/examples/templates/datasources.ftl"));
         assertValid(execute("-t src/app/examples/templates/datasources.ftl -s :csv=src/app/examples/data/csv"));
     }
 
     @Test
     public void shouldRunCsvExamples() throws IOException {
+        assertValid(execute("-t freemarker-generator/csv/confluence/transform.ftl src/app/examples/data/csv/contract.csv"));
         assertValid(execute("-t freemarker-generator/csv/html/transform.ftl src/app/examples/data/csv/contract.csv"));
         assertValid(execute("-t freemarker-generator/csv/md/transform.ftl src/app/examples/data/csv/contract.csv"));
         assertValid(execute("-t src/app/examples/templates/csv/shell/curl.ftl src/app/examples/data/csv/user.csv"));
@@ -80,6 +82,11 @@
     }
 
     @Test
+    public void shouldRunNginxParsingExamples() throws IOException {
+        assertValid(execute("-t src/app/examples/templates/nginx/confluence/nginx-config-parser.ftl -s src/app/examples/data/nginx"));
+    }
+
+    @Test
     public void shouldRunJsonExamples() throws IOException {
         assertValid(execute("-t src/app/examples/templates/json/csv/swagger-endpoints.ftl src/app/examples/data/json/swagger-spec.json"));
         assertValid(execute("-t src/app/examples/templates/json/md/github-users.ftl src/app/examples/data/json/github-users.json"));
@@ -124,15 +131,28 @@
     }
 
     @Test
+    public void shouldRunUtahParserExamples() throws IOException {
+        assertValid(execute("-PCSV_TARGET_FORMAT=EXCEL " +
+                "-PCSV_TARGET_DELIMITER=SEMICOLON " +
+                "-t src/app/examples/templates/utahparser/csv/transform.ftl " +
+                "src/app/examples/data/text/utahparser/juniper_bgp_summary_template.xml " +
+                "src/app/examples/data/text/utahparser/juniper_bgp_summary_example.txt"));
+
+        assertValid(execute("-t src/app/examples/templates/utahparser/json/transform.ftl " +
+                "src/app/examples/data/text/utahparser/juniper_bgp_summary_template.xml " +
+                "src/app/examples/data/text/utahparser/juniper_bgp_summary_example.txt"));
+    }
+
+    @Test
     public void shouldRunInteractiveTemplateExamples() throws IOException {
-        assertValid(execute("-i ${tools.jsonpath.parse(dataSources?values[0]).read(\"$.info.title\")} src/app/examples/data/json/swagger-spec.json"));
-        assertValid(execute("-i ${tools.xml.parse(dataSources?values[0])[\"recipients/person[1]/name\"]} src/app/examples/data/xml/recipients.xml"));
-        assertValid(execute("-i ${tools.jsoup.parse(dataSources?values[0]).select(\"a\")[0]} src/app/examples/data/html/dependencies.html"));
-        assertValid(execute("-i ${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))} src/app/examples/data/yaml/swagger-spec.yaml"));
+        assertValid(execute("-i ${tools.jsonpath.parse(dataSources[0]).read(\"$.info.title\")} src/app/examples/data/json/swagger-spec.json"));
+        assertValid(execute("-i ${tools.xml.parse(dataSources[0])[\"recipients/person[1]/name\"]} src/app/examples/data/xml/recipients.xml"));
+        assertValid(execute("-i ${tools.jsoup.parse(dataSources[0]).select(\"a\")[0]} src/app/examples/data/html/dependencies.html"));
+        assertValid(execute("-i ${tools.gson.toJson(tools.yaml.parse(dataSources[0]))} src/app/examples/data/yaml/swagger-spec.yaml"));
         assertValid(execute("-i ${tools.gson.toJson(yaml)} -m yaml=src/app/examples/data/yaml/swagger-spec.yaml"));
-        assertValid(execute("-i ${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))} src/app/examples/data/json/swagger-spec.json"));
+        assertValid(execute("-i ${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))} src/app/examples/data/json/swagger-spec.json"));
         assertValid(execute("-i ${tools.yaml.toYaml(json)} -m json=src/app/examples/data/json/swagger-spec.json"));
-        assertValid(execute("-i ${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources?values[0])))} src/app/examples/data/json/github-users.json"));
+        assertValid(execute("-i ${tools.dataframe.print(tools.dataframe.fromMaps(tools.gson.parse(dataSources[0])))} src/app/examples/data/json/github-users.json"));
     }
 
     @Test
@@ -178,6 +198,20 @@
     }
 
     @Test
+    public void shouldSupportDataSourceSeedingTransformation() throws IOException {
+        final String output = execute("--seed=datasource " +
+                "--template freemarker-generator/csv/html/transform.ftl " +
+                "--data-source src/app/examples/data/csv " +
+                "--data-source-include=*.csv " +
+                "--output target " +
+                "--output-mapper=*.html");
+
+        assertTrue(output.startsWith("<!DOCTYPE html>"));
+        assertTrue(output.contains("The Electric Company"));
+        assertTrue(output.contains("test user DDDDDDD"));
+    }
+
+    @Test
     @Ignore("Manual test to check memory consumption and resource handling")
     public void shouldCloseAllResources() throws IOException {
         for (int i = 0; i < 500; i++) {
@@ -199,6 +233,7 @@
     }
 
     private static void assertValid(String output) {
+        // System.out.println(output);
         assertTrue(output.length() > MIN_OUTPUT_SIZE);
         assertFalse(output.contains("Exception"));
         assertFalse(output.contains("FreeMarker template error"));
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 b9cce26..2f1dd69 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
@@ -24,7 +24,9 @@
 public class ManualTest extends AbstractMainTest {
 
     // private static final String CMD = "-V";
-    private static final String CMD = "-t src/app/examples/templates/datasources.ftl transactions:csv=src/app/examples/data/csv/transactions.csv#delimiter=TAB https://xkcd.com/info.0.json envvars=env:///";
+    private static final String CMD =
+            "-t src/app/examples/templates/nginx/confluence/nginx-config-parser.ftl -s src/app/examples/data/nginx";
+            // "-t src/app/examples/templates/datasources.ftl";
 
     @Override
     public String execute(String commandLine) throws IOException {
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
index 4a3a5b8..4531b4f 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
@@ -148,7 +148,7 @@
                 "-i", "some-interactive-template01", "-s", "datasource30.csv", "-o", "out.txt",
                 "-i", "some-interactive-template02");
 
-        main.validate();
+        main.validateCommandLineParameters();
 
         final List<OutputGeneratorDefinition> defs = main.outputGeneratorDefinitions;
         assertEquals(4, defs.size());
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/AbstractOutputGeneratorTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/AbstractOutputGeneratorTest.java
new file mode 100644
index 0000000..99eef2e
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/AbstractOutputGeneratorTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.cli.config;
+
+import org.apache.freemarker.generator.cli.picocli.DataSourceDefinition;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.apache.freemarker.generator.cli.picocli.OutputMapperDefinition;
+import org.apache.freemarker.generator.cli.picocli.OutputSeedDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateOutputDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
+
+import java.util.Arrays;
+
+import static java.util.Collections.singletonList;
+
+public abstract class AbstractOutputGeneratorTest {
+
+    protected static final String ANY_TEMPLATE_NAME = "cat.ftl";
+    protected static final String ANY_TEMPLATE = "src/app/templates/freemarker-generator/" + ANY_TEMPLATE_NAME;
+    protected static final String ANY_DATASOURCE_DIRECTORY = "src/test/data/json";
+    protected static final String ANY_DATASOURCE_FILE_01 = ANY_DATASOURCE_DIRECTORY + "/environments.json";
+    protected static final String ANY_DATASOURCE_FILE_02 = ANY_DATASOURCE_DIRECTORY + "/list.json";
+    protected static final String ANY_OUTPUT_DIRECTORY = "target";
+    protected static final String ANY_OUTPUT_FILE = "target/out.txt";
+
+    protected static OutputGeneratorDefinition outputGeneratorDefinition() {
+        return new OutputGeneratorDefinition();
+    }
+
+    protected static Settings settings(OutputGeneratorDefinition... outputGeneratorDefinitions) {
+        return Settings.builder()
+                .setOutputGeneratorDefinitions(Arrays.asList(outputGeneratorDefinitions))
+                .build();
+    }
+
+    protected static OutputSeedDefinition outputSeedDefinition(String type) {
+        final OutputSeedDefinition outputSeedDefinition = new OutputSeedDefinition();
+        outputSeedDefinition.type = type;
+        return outputSeedDefinition;
+    }
+
+    protected static TemplateSourceDefinition templateSourceDefinition(String template) {
+        final TemplateSourceDefinition templateSourceDefinition = new TemplateSourceDefinition();
+        templateSourceDefinition.template = template;
+        return templateSourceDefinition;
+    }
+
+    protected static DataSourceDefinition dataSourceDefinition(String... dataSources) {
+        final DataSourceDefinition dataSourceDefinition = new DataSourceDefinition();
+        dataSourceDefinition.dataSources = Arrays.asList(dataSources);
+        return dataSourceDefinition;
+    }
+
+    protected static TemplateOutputDefinition templateOutputDirectoryDefinition(String directoryName) {
+        final TemplateOutputDefinition templateOutputDefinition = new TemplateOutputDefinition();
+        templateOutputDefinition.outputs = singletonList(directoryName);
+        return templateOutputDefinition;
+    }
+
+    protected static OutputMapperDefinition outputMapperDefinition(String template) {
+        final OutputMapperDefinition outputMapperDefinition = new OutputMapperDefinition();
+        outputMapperDefinition.outputMapper = template;
+        return outputMapperDefinition;
+    }
+}
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
index 3c8f6ef..b2daff2 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
@@ -29,8 +29,6 @@
 
 public class ConfigurationSupplierTest {
 
-    private static final String ANY_TEMPLATE_NAME = "templateName";
-
     @Test
     public void shouldProvideDefaultConfiguration() {
         final ConfigurationSupplier configurationSupplier = configurationSupplier(settingsBuilder().build());
@@ -43,11 +41,12 @@
         assertTrue(configuration.isOutputEncodingSet());
 
         assertFalse(configuration.isCacheStorageExplicitlySet());
-        assertFalse(configuration.isObjectWrapperExplicitlySet());
+        assertTrue(configuration.isObjectWrapperExplicitlySet());
         assertFalse(configuration.isOutputFormatExplicitlySet());
         assertFalse(configuration.isTemplateExceptionHandlerExplicitlySet());
         assertFalse(configuration.isTimeZoneExplicitlySet());
         assertFalse(configuration.isWrapUncheckedExceptionsExplicitlySet());
+        assertTrue(configuration.isAPIBuiltinEnabled());
     }
 
     private ConfigurationSupplier configurationSupplier(Settings settings) {
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
index a12cb1d..fa9b47e 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
@@ -39,7 +39,6 @@
 
 import static java.util.Collections.singletonList;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -95,7 +94,6 @@
 
         assertNotNull(configuration.getSharedVariable(Model.TOOLS));
         assertTrue(configuration.isTemplateLoaderExplicitlySet());
-        assertFalse(configuration.isObjectWrapperExplicitlySet());
     }
 
     @Test
@@ -106,7 +104,7 @@
         final List<DataSource> dataSourceList = dataSourcesSupplier.get();
 
         assertEquals(1, dataSourceList.size());
-        assertTrue(dataSourceList.get(0).getName().endsWith(ANY_DATA_SOURCE_NAME));
+        assertTrue(dataSourceList.get(0).getName().endsWith(fixSeparators(ANY_DATA_SOURCE_NAME)));
     }
 
     @Test
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/TemplateSeedingOutputGeneratorTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/TemplateSeedingOutputGeneratorTest.java
new file mode 100644
index 0000000..c75a121
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/TemplateSeedingOutputGeneratorTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.cli.config;
+
+import org.apache.freemarker.generator.base.FreeMarkerConstants.SeedType;
+import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class TemplateSeedingOutputGeneratorTest extends AbstractOutputGeneratorTest {
+
+    /**
+     * One data source -> one output generator
+     */
+    @Test
+    public void shouldCreateSingleOutputGeneratorForMultipleDataSources() {
+        final OutputGeneratorDefinition outputGeneratorDefinition = outputGeneratorDefinition();
+        outputGeneratorDefinition.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_FILE);
+        outputGeneratorDefinition.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_01, ANY_DATASOURCE_FILE_02);
+        outputGeneratorDefinition.outputSeedDefinition = outputSeedDefinition(SeedType.TEMPLATE);
+        final Settings settings = settings(outputGeneratorDefinition);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator = outputGenerators.get(0);
+
+        assertEquals(1, outputGenerators.size());
+        assertEquals(2, outputGenerator.getDataSources().size());
+
+        assertEquals(OutputGenerator.SeedType.TEMPLATE, outputGenerator.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_FILE), outputGenerator.getTemplateOutput().getFile());
+        assertEquals("environments.json", outputGenerator.getDataSources().get(0).getFileName());
+        assertEquals("list.json", outputGenerator.getDataSources().get(1).getFileName());
+    }
+
+    /**
+     * N file data sources for a given directory -> one output generator
+     */
+    @Test
+    public void shouldCreateSingleOutputGeneratorForMultipleDataSourcesInDirectory() {
+        final OutputGeneratorDefinition outputGeneratorDefinition = outputGeneratorDefinition();
+        outputGeneratorDefinition.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_FILE);
+        outputGeneratorDefinition.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_DIRECTORY);
+        outputGeneratorDefinition.outputSeedDefinition = outputSeedDefinition(SeedType.TEMPLATE);
+        final Settings settings = settings(outputGeneratorDefinition);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator = outputGenerators.get(0);
+
+        assertEquals(1, outputGenerators.size());
+        assertEquals(2, outputGenerator.getDataSources().size());
+
+        assertEquals(OutputGenerator.SeedType.TEMPLATE, outputGenerator.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_FILE), outputGenerator.getTemplateOutput().getFile());
+        assertEquals("environments.json", outputGenerator.getDataSources().get(0).getFileName());
+        assertEquals("list.json", outputGenerator.getDataSources().get(1).getFileName());
+    }
+
+    /**
+     * N output generator definitions -> N output generators
+     */
+    @Test
+    public void shouldCreateOutputGeneratorForEachOutputGeneratorDefinition() {
+        final OutputGeneratorDefinition outputGeneratorDefinition_01 = outputGeneratorDefinition();
+        outputGeneratorDefinition_01.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition_01.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_FILE);
+        outputGeneratorDefinition_01.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_01);
+        outputGeneratorDefinition_01.outputSeedDefinition = outputSeedDefinition(SeedType.TEMPLATE);
+
+        final OutputGeneratorDefinition outputGeneratorDefinition_02 = outputGeneratorDefinition();
+        outputGeneratorDefinition_02.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition_02.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_FILE);
+        outputGeneratorDefinition_02.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_02);
+        outputGeneratorDefinition_02.outputSeedDefinition = outputSeedDefinition(SeedType.TEMPLATE);
+
+        final Settings settings = settings(outputGeneratorDefinition_01, outputGeneratorDefinition_02);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator_01 = outputGenerators.get(0);
+        final OutputGenerator outputGenerator_02 = outputGenerators.get(1);
+
+        assertEquals(2, outputGenerators.size());
+
+        assertEquals(1, outputGenerator_01.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.TEMPLATE, outputGenerator_01.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_01.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_FILE), outputGenerator_01.getTemplateOutput().getFile());
+        assertEquals("environments.json", outputGenerator_01.getDataSources().get(0).getFileName());
+
+        assertEquals(1, outputGenerator_02.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.TEMPLATE, outputGenerator_02.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_02.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_FILE), outputGenerator_02.getTemplateOutput().getFile());
+        assertEquals("list.json", outputGenerator_02.getDataSources().get(0).getFileName());
+    }
+}
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGeneratorTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGeneratorTest.java
new file mode 100644
index 0000000..5b1219b
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputGeneratorTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.cli.config.output;
+
+import org.apache.freemarker.generator.base.FreeMarkerConstants.SeedType;
+import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.cli.config.AbstractOutputGeneratorTest;
+import org.apache.freemarker.generator.cli.config.OutputGeneratorsSupplier;
+import org.apache.freemarker.generator.cli.config.Settings;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class DataSourceSeedingOutputGeneratorTest extends AbstractOutputGeneratorTest {
+
+    /**
+     * Edge case - a single data source shall create a single output generator
+     */
+    @Test
+    public void shouldCreateOutputGeneratorForSingleDataSource() {
+        final OutputGeneratorDefinition outputGeneratorDefinition = outputGeneratorDefinition();
+        outputGeneratorDefinition.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_DIRECTORY);
+        outputGeneratorDefinition.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_01);
+        outputGeneratorDefinition.outputSeedDefinition = outputSeedDefinition(SeedType.DATASOURCE);
+        outputGeneratorDefinition.outputMapperDefinition = outputMapperDefinition("*.txt");
+        final Settings settings = settings(outputGeneratorDefinition);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator = outputGenerators.get(0);
+
+        assertEquals(1, outputGenerators.size());
+
+        assertEquals(1, outputGenerator.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.DATASOURCE, outputGenerator.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_DIRECTORY, "environments.txt"), outputGenerator.getTemplateOutput().getFile());
+        assertEquals("environments.json", outputGenerator.getDataSources().get(0).getFileName());
+    }
+
+    /**
+     * N given data sources -> N output generators
+     */
+    @Test
+    public void shouldCreateOutputGeneratorForEachDataSource() {
+        final OutputGeneratorDefinition outputGeneratorDefinition_01 = outputGeneratorDefinition();
+        outputGeneratorDefinition_01.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition_01.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_DIRECTORY);
+        outputGeneratorDefinition_01.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_01);
+        outputGeneratorDefinition_01.outputSeedDefinition = outputSeedDefinition(SeedType.DATASOURCE);
+        outputGeneratorDefinition_01.outputMapperDefinition = outputMapperDefinition("*.txt");
+
+        final OutputGeneratorDefinition outputGeneratorDefinition_02 = outputGeneratorDefinition();
+        outputGeneratorDefinition_02.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition_02.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_DIRECTORY);
+        outputGeneratorDefinition_02.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_FILE_02);
+        outputGeneratorDefinition_02.outputSeedDefinition = outputSeedDefinition(SeedType.DATASOURCE);
+        outputGeneratorDefinition_02.outputMapperDefinition = outputMapperDefinition("*.txt");
+
+        final Settings settings = settings(outputGeneratorDefinition_01, outputGeneratorDefinition_02);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator_01 = outputGenerators.get(0);
+        final OutputGenerator outputGenerator_02 = outputGenerators.get(1);
+
+        assertEquals(2, outputGenerators.size());
+
+        assertEquals(1, outputGenerator_01.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.DATASOURCE, outputGenerator_01.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_01.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_DIRECTORY, "environments.txt"), outputGenerator_01.getTemplateOutput()
+                .getFile());
+        assertEquals("environments.json", outputGenerator_01.getDataSources().get(0).getFileName());
+
+        assertEquals(1, outputGenerator_02.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.DATASOURCE, outputGenerator_02.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_02.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_DIRECTORY, "list.txt"), outputGenerator_02.getTemplateOutput().getFile());
+        assertEquals("list.json", outputGenerator_02.getDataSources().get(0).getFileName());
+    }
+
+    /**
+     * N file data sources in a given directory -> N output generators
+     */
+    @Test
+    public void shouldCreateOutputGeneratorsForAllDataSourcesInDirectory() {
+        final OutputGeneratorDefinition outputGeneratorDefinition = outputGeneratorDefinition();
+        outputGeneratorDefinition.templateSourceDefinition = templateSourceDefinition(ANY_TEMPLATE);
+        outputGeneratorDefinition.templateOutputDefinition = templateOutputDirectoryDefinition(ANY_OUTPUT_DIRECTORY);
+        outputGeneratorDefinition.dataSourceDefinition = dataSourceDefinition(ANY_DATASOURCE_DIRECTORY);
+        outputGeneratorDefinition.outputSeedDefinition = outputSeedDefinition(SeedType.DATASOURCE);
+        outputGeneratorDefinition.outputMapperDefinition = outputMapperDefinition("*.txt");
+        final Settings settings = settings(outputGeneratorDefinition);
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = new OutputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator_01 = outputGenerators.get(0);
+        final OutputGenerator outputGenerator_02 = outputGenerators.get(1);
+
+        assertEquals(2, outputGenerators.size());
+
+        assertEquals(1, outputGenerator_01.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.DATASOURCE, outputGenerator_01.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_01.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_DIRECTORY, "environments.txt"), outputGenerator_01.getTemplateOutput()
+                .getFile());
+        assertEquals("environments.json", outputGenerator_01.getDataSources().get(0).getFileName());
+
+        assertEquals(1, outputGenerator_02.getDataSources().size());
+        assertEquals(OutputGenerator.SeedType.DATASOURCE, outputGenerator_02.getSeedType());
+        assertEquals(ANY_TEMPLATE_NAME, outputGenerator_02.getTemplateSource().getName());
+        assertEquals(new File(ANY_OUTPUT_DIRECTORY, "list.txt"), outputGenerator_02.getTemplateOutput().getFile());
+        assertEquals("list.json", outputGenerator_02.getDataSources().get(0).getFileName());
+    }
+}
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapperTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapperTest.java
new file mode 100644
index 0000000..3d58930
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/DataSourceSeedingOutputMapperTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.cli.config.output;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.util.FileUtils;
+import org.apache.freemarker.generator.base.util.OperatingSystem;
+import org.apache.freemarker.generator.base.util.StringUtils;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+
+public class DataSourceSeedingOutputMapperTest {
+
+    private final File CURRENT_DIRECTORY = new File(".");
+    private final File PARENT_DIRECTORY = new File("..");
+    private final File ANY_DIRECTORY = new File("target");
+    private final File ANY_FILE = new File("pom.xml");
+    private final DataSource FILE_DATA_SOURCE = DataSourceFactory.fromFile(ANY_FILE, StandardCharsets.UTF_8);
+    private final DataSource URL_DATA_SOURCE = DataSourceFactory.fromUrl("google.com", "group", DataSourceFactory.toUrl("https://www.google.com"));
+
+    @Test
+    public void shouldGenerateOutputFileForCurrentDirectory() {
+        assertEquals("pom.xml", path(outputMapper(null).map(CURRENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("pom.xml", path(outputMapper("").map(CURRENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("pom.html", path(outputMapper("*.html").map(CURRENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("pom.html", path(outputMapper("pom.html").map(CURRENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("html/pom.html", path(outputMapper("html/*.html").map(CURRENT_DIRECTORY, FILE_DATA_SOURCE)));
+    }
+
+    @Test
+    public void shouldGenerateOutputFileForParentDirectory() {
+        assertEquals("../pom.xml", path(outputMapper(null).map(PARENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("../pom.xml", path(outputMapper("").map(PARENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("../pom.html", path(outputMapper("*.html").map(PARENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("../pom.html", path(outputMapper("pom.html").map(PARENT_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("../html/pom.html", path(outputMapper("html/*.html").map(PARENT_DIRECTORY, FILE_DATA_SOURCE)));
+    }
+
+    @Test
+    public void shouldGenerateOutputFileForAnyDirectory() {
+        assertEquals("target/pom.xml", path(outputMapper(null).map(ANY_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("target/pom.xml", path(outputMapper("").map(ANY_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("target/pom.html", path(outputMapper("*.html").map(ANY_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("target/pom.html", path(outputMapper("pom.html").map(ANY_DIRECTORY, FILE_DATA_SOURCE)));
+        assertEquals("target/html/pom.html", path(outputMapper("html/*.html").map(ANY_DIRECTORY, FILE_DATA_SOURCE)));
+    }
+
+    @Test
+    public void shouldHandleUrlDataSources() {
+        assertEquals("target/google.com", path(outputMapper(null).map(ANY_DIRECTORY, URL_DATA_SOURCE)));
+        assertEquals("target/google.com", path(outputMapper("").map(ANY_DIRECTORY, URL_DATA_SOURCE)));
+        assertEquals("target/google.com.html", path(outputMapper("*.html").map(ANY_DIRECTORY, URL_DATA_SOURCE)));
+    }
+
+    private static DataSourceSeedingOutputMapper outputMapper(String template) {
+        return new DataSourceSeedingOutputMapper(template);
+    }
+
+    private static String fixSeparators(String str) {
+        if (OperatingSystem.isWindows()) {
+            return FilenameUtils.separatorsToUnix(str);
+        } else {
+            return str;
+        }
+    }
+
+    private static String path(File file) {
+        final File workingDir = new File(".");
+        final String relativePath = FileUtils.getRelativePath(workingDir, file);
+
+        return fixSeparators(StringUtils.isEmpty(relativePath) ?
+                file.getName() :
+                relativePath + File.separatorChar + file.getName());
+    }
+}
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilderTest.java
similarity index 95%
rename from freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
rename to freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilderTest.java
index 7170cd7..6755404 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/output/TemplateTransformationsBuilderTest.java
@@ -14,14 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.freemarker.generator.template;
+package org.apache.freemarker.generator.cli.config.output;
 
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
 import org.apache.freemarker.generator.base.template.TemplateOutput;
 import org.apache.freemarker.generator.base.template.TemplateSource;
 import org.apache.freemarker.generator.base.template.TemplateSource.Origin;
-import org.apache.freemarker.generator.base.template.TemplateTransformation;
-import org.apache.freemarker.generator.base.template.TemplateTransformationsBuilder;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -41,10 +39,9 @@
 
 public class TemplateTransformationsBuilderTest {
 
-    private static final String ANY_TEMPLATE_FILE_NAME = "src/test/template/application.properties";
-    private static final String OTHER_TEMPLATE_FILE_NAME = "src/test/template/nginx/nginx.conf.ftl";
+    private static final String ANY_TEMPLATE_FILE_NAME = "src/test/data/template/application.properties";
     private static final String ANY_TEMPLATE_PATH = "template/info.ftl";
-    private static final String ANY_TEMPLATE_DIRECTORY_NAME = "src/test/template";
+    private static final String ANY_TEMPLATE_DIRECTORY_NAME = "src/test/data/template";
     private static final String ANY_TEMPLATE_URL = "https://raw.githubusercontent.com/apache/freemarker-generator/master/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl";
     private static final String ANY_ENV_URI = "env:///JAVA_HOME";
 
diff --git a/freemarker-generator-cli/src/test/templates/echo.ftl b/freemarker-generator-cli/src/test/templates/echo.ftl
index 38627f4..56c0b56 100644
--- a/freemarker-generator-cli/src/test/templates/echo.ftl
+++ b/freemarker-generator-cli/src/test/templates/echo.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#list dataSources?values as ds>
+<#list dataSources as ds>
 ${ds.name}, ${ds.uri}
 =============================================================================
 ${ds.text}
diff --git a/freemarker-generator-cli/src/test/templates/manual.ftl b/freemarker-generator-cli/src/test/templates/manual.ftl
index c1112be..1e6f5ac 100644
--- a/freemarker-generator-cli/src/test/templates/manual.ftl
+++ b/freemarker-generator-cli/src/test/templates/manual.ftl
@@ -22,8 +22,8 @@
 
 Use FTL Array-style Access
 ==============================================================================
-${dataSources?values[0].toString()}
-${dataSources?values?first.toString()}
+${dataSources[0].toString()}
+${dataSources?first.toString()}
 
 Get Document Names As Keys
 ==============================================================================
@@ -38,13 +38,13 @@
 </#list>
 
 <#if dataSources?has_content>
-    <#list dataSources?values as dataSource>
+    <#list dataSources as dataSource>
 [#${dataSource?counter}] - ${dataSource.name}
 ==============================================================================
 
 Invoke Arbitrary Methods On DataSource
 ---------------------------------------------------------------------------
-<#assign dataSource=dataSources?values?first>
+<#assign dataSource=dataSources?first>
 Name            : ${dataSource.name}
 Nr of lines     : ${dataSource.lines?size}
 Content Type    : ${dataSource.contentType}
diff --git a/freemarker-generator-cli/src/test/templates/tools/csv.ftl b/freemarker-generator-cli/src/test/templates/tools/csv.ftl
index 25dfb3b..07c4f43 100644
--- a/freemarker-generator-cli/src/test/templates/tools/csv.ftl
+++ b/freemarker-generator-cli/src/test/templates/tools/csv.ftl
@@ -15,7 +15,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign records = tools.csv.parse(dataSources?values[0], CSVFormat.DEFAULT.withHeader()).records>
+<#assign records = tools.csv.parse(dataSources[0], CSVFormat.DEFAULT.withHeader()).records>
 
 tools.csv.toMap(name)
 =============================================================================
diff --git a/freemarker-generator-maven-plugin-sample/pom.xml b/freemarker-generator-maven-plugin-sample/pom.xml
index cbd84a1..1ce4a10 100644
--- a/freemarker-generator-maven-plugin-sample/pom.xml
+++ b/freemarker-generator-maven-plugin-sample/pom.xml
@@ -20,7 +20,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
     <artifactId>freemarker-generator-maven-plugin-sample</artifactId>
     <name>Apache FreeMarker Generator: Maven Plugin Sample</name>
diff --git a/freemarker-generator-maven-plugin/pom.xml b/freemarker-generator-maven-plugin/pom.xml
index d1f9515..c8b014a 100644
--- a/freemarker-generator-maven-plugin/pom.xml
+++ b/freemarker-generator-maven-plugin/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>freemarker-generator-maven-plugin</artifactId>
diff --git a/freemarker-generator-tools/pom.xml b/freemarker-generator-tools/pom.xml
index e532237..04ca3f1 100644
--- a/freemarker-generator-tools/pom.xml
+++ b/freemarker-generator-tools/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>freemarker-generator-tools</artifactId>
@@ -129,6 +129,12 @@
             <artifactId>snakeyaml</artifactId>
             <version>1.28</version>
         </dependency>
+        <!-- UtahPrserTool -->
+        <dependency>
+            <groupId>com.sonalake</groupId>
+            <artifactId>utah-parser</artifactId>
+            <version>1.0.2</version>
+        </dependency>
         <!-- Testing -->
         <dependency>
             <groupId>junit</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 3490ee8..12c757d 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
@@ -110,7 +110,7 @@
     }
 
     /**
-     * Extract the list of unique values (keys) of the column with the given index..
+     * Extract the list of unique values (keys) of the column with the given index.
      *
      * @param records records to process
      * @param index   column index to map
@@ -123,7 +123,7 @@
     /**
      * Map the given value of the CVS record into (key to record). If duplicates
      * are encountered return the first occurrence of the CVS record. The map
-     * retains the insertion order of they keys.
+     * retains the insertion order of their keys.
      *
      * @param records records to process
      * @param name    column name to map
@@ -136,7 +136,7 @@
     /**
      * Map the given value of the CVS record into (key to record). If duplicates
      * are encountered return the first occurrence of the CVS record. The map
-     * retains the insertion order of they keys.
+     * retains the insertion order of their keys.
      *
      * @param records records to process
      * @param index   column index to map
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/impl/CommonsCSVPrinterFacade.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/impl/CommonsCSVPrinterFacade.java
index 9909d17..958dbdb 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/impl/CommonsCSVPrinterFacade.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/commonscsv/impl/CommonsCSVPrinterFacade.java
@@ -25,6 +25,9 @@
 import java.io.StringWriter;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Wrap <code>CSVPrinter</code> so each print method returns
@@ -91,6 +94,15 @@
         return getOutput();
     }
 
+    public String printRecord(Map<String, Object> map, String[] headers) throws IOException {
+        final List<String> values = new ArrayList<>(headers.length);
+        for (String header : headers) {
+            final Object value = map.get(header);
+            values.add(value != null ? value.toString() : "");
+        }
+        return printRecord(values);
+    }
+
     private String getOutput() {
         writer.flush();
         final String output = writer.getBuffer().toString();
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/impl/CSVConverter.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/impl/CSVConverter.java
index 5745c35..30c1938 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/impl/CSVConverter.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/dataframe/impl/CSVConverter.java
@@ -40,7 +40,7 @@
             final CSVRecord firstRecord = records.get(0);
 
             //  build dataframe with headers
-            if (headerNames != null && !headerNames.isEmpty()) {
+            if (!headerNames.isEmpty()) {
                 headerNames.forEach(builder::addStringColumn);
             } else {
                 for (int i = 0; i < firstRecord.size(); i++) {
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 eff89ea..300cf44 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
@@ -80,7 +80,7 @@
     }
 
     /**
-     * Transform the Excel sheet into a table. Please not that locales are mostly
+     * Transform the Excel sheet into a table. Please note that locales are mostly
      * ignored by Apache POI (see https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/DataFormatter.html)
      *
      * @param sheet Excel sheet
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/grok/GrokTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/grok/GrokTool.java
index 5f3a485..34af4bd 100644
--- a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/grok/GrokTool.java
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/grok/GrokTool.java
@@ -28,7 +28,7 @@
     private static final String DEFAULT_PATTERN_FILE = "/patterns/patterns";
 
     /**
-     * Create a default Grok instance using the the default pattern files loaded
+     * Create a default Grok instance using the default pattern files loaded
      * from the classpath.
      *
      * @param pattern Grok pattern to compile
@@ -39,7 +39,7 @@
     }
 
     /**
-     * Get a default Grok instance using the the default pattern files loaded
+     * Get a default Grok instance using the default pattern files loaded
      * from the classpath.
      *
      * @param pattern            Grok pattern to compile
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 36a8139..be6ad05 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
@@ -95,18 +95,41 @@
         return System.getProperties();
     }
 
+    /**
+     * Get the value of a user-supplied parameter.
+     *
+     * @param name name of the parameter
+     * @return value or null
+     */
     public String getParameter(String name) {
         return parameters.get(name);
     }
 
+    /**
+     * Get the value of a user-supplied parameter.
+     *
+     * @param name name of the parameter
+     * @param def  default value
+     * @return value or default value
+     */
     public String getParameter(String name, String def) {
         return parameters.getOrDefault(name, def);
     }
 
+    /**
+     * Get the map of user-supplied parameters.
+     *
+     * @return map of parameters
+     */
     public Map<String, String> getParameters() {
         return parameters;
     }
 
+    /**
+     * Get the list of system properties provided by the caller.
+     *
+     * @return list of user-supplied system properties
+     */
     public Properties getUserSystemProperties() {
         return userSystemProperties;
     }
@@ -132,7 +155,7 @@
      * @param name name of the configuration parameter
      * @return value of null
      */
-    public String getString(String name) {
+    public String getProperty(String name) {
         return StringUtils.firstNonEmpty(
                 getParameter(name),
                 System.getProperty(name),
@@ -148,8 +171,8 @@
      * @param def  default value
      * @return value of null
      */
-    public String getString(String name, String def) {
-        return StringUtils.firstNonEmpty(getString(name), def);
+    public String getProperty(String name, String def) {
+        return StringUtils.firstNonEmpty(getProperty(name), def);
     }
 
     public String getHostName() {
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/UtahParserTool.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/UtahParserTool.java
new file mode 100644
index 0000000..82520ef
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/UtahParserTool.java
@@ -0,0 +1,100 @@
+/*
+ * 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.utahparser;
+
+import com.sonalake.utah.Parser;
+import com.sonalake.utah.config.Config;
+import com.sonalake.utah.config.ConfigLoader;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
+import org.apache.freemarker.generator.tools.utahparser.impl.ParserWrapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class UtahParserTool {
+
+    /**
+     * Create a Utah Parser config based on the given source.
+     *
+     * @param source source of configuration file
+     * @return config instance
+     */
+    public Config getConfig(String source) {
+        return getConfig(DataSourceLoaderFactory.create().load(source));
+    }
+
+    /**
+     * Create a Utah Parser config based on teh textual content of the data source,
+     *
+     * @param dataSource XML configuration file
+     * @return config instance
+     */
+    public Config getConfig(DataSource dataSource) {
+        try (InputStream is = dataSource.getUnsafeInputStream()) {
+            return loadConfig(is);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to load parser configuration: " + dataSource, e);
+        }
+    }
+
+    /**
+     * Create a parser instance.
+     *
+     * @param config     configuration
+     * @param dataSource data source to be parsed
+     * @return parser
+     */
+    public ParserWrapper getParser(Config config, DataSource dataSource) {
+        final InputStreamReader is = new InputStreamReader(dataSource.getInputStream());
+        final Parser parser = Parser.parse(config, is);
+        return new ParserWrapper(parser);
+    }
+
+    public List<String> getHeaders(Map<String, Object> record) {
+        if (record == null || record.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        return new ArrayList<>(record.keySet());
+    }
+
+    public List<String> getHeaders(Collection<Map<String, String>> records) {
+        if (records == null || records.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        return records.stream()
+                .map(Map::keySet)
+                .flatMap(Collection::stream).distinct().sorted().collect(Collectors.toList());
+    }
+
+    public String toString() {
+        return "Parse semi-structured text using regular expressions (see https://github.com/sonalake/utah-parser)";
+    }
+
+    private static Config loadConfig(InputStream is) throws IOException {
+        return new ConfigLoader().loadConfig(new InputStreamReader(is));
+    }
+}
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/impl/ParserWrapper.java b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/impl/ParserWrapper.java
new file mode 100644
index 0000000..9aad9bc
--- /dev/null
+++ b/freemarker-generator-tools/src/main/java/org/apache/freemarker/generator/tools/utahparser/impl/ParserWrapper.java
@@ -0,0 +1,86 @@
+/*
+ * 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.utahparser.impl;
+
+import com.sonalake.utah.Parser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Wraps the <code>com.sonalake.utah.Parser</code> to provide convenience
+ * methods such as support of iterators.
+ */
+public class ParserWrapper implements Iterable<Map<String, String>> {
+
+    /** The wrapped parser instance */
+    private final Parser parser;
+
+    public ParserWrapper(Parser parser) {
+        this.parser = requireNonNull(parser);
+    }
+
+    @Override
+    public Iterator<Map<String, String>> iterator() {
+        return new RecordsIterator(parser);
+    }
+
+    /**
+     * Iterates over all records and return a list.
+     *
+     * @return list of records
+     */
+    public List<Map<String, String>> toList() {
+        final List<Map<String, String>> result = new ArrayList<>();
+        for (Map<String, String> record : this) {
+            result.add(record);
+        }
+        return result;
+    }
+
+    private static final class RecordsIterator implements Iterator<Map<String, String>> {
+
+        private final Parser parser;
+        private Map<String, String> nextRecord;
+
+        RecordsIterator(Parser parser) {
+            this.parser = requireNonNull(parser);
+            this.nextRecord = parser.next();
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nextRecord != null;
+        }
+
+        @Override
+        public Map<String, String> next() {
+            if (nextRecord == null) {
+                throw new NoSuchElementException();
+            }
+
+            final Map<String, String> currentRecord = nextRecord;
+            nextRecord = parser.next();
+            return currentRecord;
+        }
+    }
+}
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_example.txt b/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_example.txt
new file mode 100644
index 0000000..c4a209e
--- /dev/null
+++ b/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_example.txt
@@ -0,0 +1,12 @@
+Groups: 3 Peers: 3 Down peers: 0
+Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
+inet.0               947        310          0          0          0          0
+inet6.0              849        807          0          0          0          0
+Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Damped...
+10.247.68.182         65550     131725   28179233       0      11     6w3d17h Establ
+  inet.0: 4/5/1
+  inet6.0: 0/0/0
+10.254.166.246        65550     136159   29104942       0       0      6w5d6h Establ
+  inet.0: 0/0/0
+  inet6.0: 7/8/1
+192.0.2.100           65551    1269381    1363320       0       1      9w5d6h 1/2/3 4/5/6
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_template.xml b/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_template.xml
new file mode 100644
index 0000000..bb1093a
--- /dev/null
+++ b/freemarker-generator-tools/src/test/data/utahparser/juniper_bgp_summary_template.xml
@@ -0,0 +1,69 @@
+<config>
+    <searches>
+
+        <!-- in this case, we have a CSV (space delimited file) so we define the line once, and then reuse it over
+        and again for each value -->
+        <search id="QUERY-LINE"><![CDATA[\s*{ipAddress}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbers}\s+{numbersThenText}]]></search>
+
+
+        <search id="inetInline"><![CDATA[{inet} {inet}]]></search>
+        <search id="inet4"><![CDATA[inet.0:\s*{inet}]]></search>
+        <search id="inet6"><![CDATA[inet6.0:\s*{inet}]]></search>
+        <search id="inet"><![CDATA[{numbers}/{numbers}/{numbers}]]></search>
+
+        <!-- Some rules for finding text, to make the values a little easier below -->
+        <search id="numbers"><![CDATA[(\d+)]]></search>
+        <search id="numbersThenText"><![CDATA[(\d+\S+)]]></search>
+        <search id="string"><![CDATA[(\S+?)]]></search>
+        <search id="ipAddress"><![CDATA[(\d+(\.\d+){3})]]></search>
+        <search id="EOL"><![CDATA[[\n\r]]]></search>
+    </searches>
+
+    <!-- the record starts with a line with an ip address and ends with either an inet6 line, or where the ids are at
+    the end of the line-->
+    <delim retain="true">{ipAddress}.*(\/\d+)\s*{EOL}</delim>
+    <delim>\s*({inet6})</delim>
+
+    <!--
+    This is the last line of the header
+     -->
+    <header-delim><![CDATA[Peer\s+AS\s+InPkt]]></header-delim>
+
+    <!--
+    Files look like this:
+
+    10.247.68.182         65550     131725   28179233       0      11     6w3d17h Establ
+      inet.0: 4/5/1
+      inet6.0: 0/0/0
+
+      or
+
+    192.0.2.100           65551    1269381    1363320       0       1      9w5d6h 2/3/0 0/0/0
+  -->
+    <values>
+        <!-- here we reuse the line pattern, only we pull out different group values -->
+        <value id="remoteIp" group="1"><![CDATA[{QUERY-LINE}]]></value>
+        <value id="uptime" group="8"><![CDATA[{QUERY-LINE}]]></value>
+
+        <!-- here we check for values in the inet* lines and use these -->
+        <value id="activeV4" group="1"><![CDATA[{inet4}]]></value>
+        <value id="receivedV4" group="2"><![CDATA[{inet4}]]></value>
+        <value id="accepted_V4" group="3"><![CDATA[{inet4}]]></value>
+
+        <value id="activeV6" group="1"><![CDATA[{inet6}]]></value>
+        <value id="receivedV6" group="2"><![CDATA[{inet6}]]></value>
+        <value id="accepted_V6" group="3"><![CDATA[{inet6}]]></value>
+
+        <!--
+        here we check for values at the end of the query line, and use these
+         NOTE: since we only set non-null values, these will not overwrite any values set above
+        -->
+        <value id="activeV4" group="9"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="receivedV4" group="10"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="accepted_V4" group="11"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="activeV6" group="12"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="receivedV6" group="13"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+        <value id="accepted_V6" group="14"><![CDATA[{QUERY-LINE}\s*{inetInline}]]></value>
+
+    </values>
+</config>
\ No newline at end of file
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonsexec/CommonsExecToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonsexec/CommonsExecToolTest.java
index 8f30444..a9a49d2 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonsexec/CommonsExecToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/commonsexec/CommonsExecToolTest.java
@@ -16,13 +16,39 @@
  */
 package org.apache.freemarker.generator.tools.commonsexec;
 
+import org.apache.freemarker.generator.base.util.OperatingSystem;
 import org.junit.Test;
 
+import java.util.Collections;
+
+import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertFalse;
 
 public class CommonsExecToolTest {
 
     @Test
+    public void shouldExecuteCommandLine() {
+        if (OperatingSystem.isMac() || OperatingSystem.isUnix()) {
+            final String output = commonsExecTool().execute("echo Hello World!");
+            assertEquals("Hello World!\n", output);
+        }
+    }
+
+    @Test
+    public void shouldExecuteCommandLineArgs() {
+        if (OperatingSystem.isMac() || OperatingSystem.isUnix()) {
+            final String output = commonsExecTool().execute("echo", Collections.singletonList("Hello World!"));
+
+            assertEquals("\"Hello World!\"\n", output);
+        }
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void shouldThrowExceptionForInvalidCommand() {
+        commonsExecTool().execute("does-not-exist.bat");
+    }
+
+    @Test
     public void shouldReturnDescription() {
         assertFalse(commonsExecTool().toString().isEmpty());
     }
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/system/SystemToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/system/SystemToolTest.java
index 23d90ee..de55318 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/system/SystemToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/system/SystemToolTest.java
@@ -36,13 +36,13 @@
     }
 
     @Test
-    public void shouldGetString() {
-        assertEquals(USER, systemTool().getString("USER"));
+    public void shouldGetProperty() {
+        assertEquals(USER, systemTool().getProperty("USER"));
     }
 
     @Test
-    public void shouldGetStringWithDefault() {
-        assertEquals("foo", systemTool().getString("_DOES_NOT_EXIST_", "foo"));
+    public void shouldGetPropertyWithDefault() {
+        assertEquals("foo", systemTool().getProperty("_DOES_NOT_EXIST_", "foo"));
     }
 
     @Test
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/utahparser/UtahParserToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/utahparser/UtahParserToolTest.java
new file mode 100644
index 0000000..fac9e75
--- /dev/null
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/utahparser/UtahParserToolTest.java
@@ -0,0 +1,103 @@
+package org.apache.freemarker.generator.tools.utahparser;
+
+import com.sonalake.utah.config.Config;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.util.MapBuilder;
+import org.apache.freemarker.generator.tools.utahparser.impl.ParserWrapper;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+
+public class UtahParserToolTest {
+
+    private static final String EXAMPLE_FILE_NAME = "src/test/data/utahparser/juniper_bgp_summary_example.txt";
+    private static final String TEMPLATE_FILE_NAME = "src/test/data/utahparser/juniper_bgp_summary_template.xml";
+
+    @Test
+
+    public void shallLoadConfFromFile() {
+        final Config config = utahParserTool().getConfig(TEMPLATE_FILE_NAME);
+
+        assertNotNull(config);
+        assertTrue(config.isDelimiterValid());
+    }
+
+    @Test
+    public void shallLoadConfFromDataSource() {
+        final DataSource dataSource = dataSource(TEMPLATE_FILE_NAME);
+        final Config config = utahParserTool().getConfig(dataSource);
+
+        assertNotNull(config);
+        assertTrue(config.isDelimiterValid());
+    }
+
+    @Test
+    public void shallGetParserInstance() {
+        final DataSource dataSource = dataSource(EXAMPLE_FILE_NAME);
+        final UtahParserTool utahParserTool = utahParserTool();
+        final Config config = utahParserTool.getConfig(TEMPLATE_FILE_NAME);
+        final ParserWrapper parser = utahParserTool.getParser(config, dataSource);
+
+        assertNotNull(parser);
+        assertNotNull(parser.iterator());
+    }
+
+    @Test
+    public void shallParseAllData() {
+        final DataSource dataSource = dataSource(EXAMPLE_FILE_NAME);
+        final UtahParserTool utahParserTool = utahParserTool();
+        final Config config = utahParserTool.getConfig(TEMPLATE_FILE_NAME);
+        final ParserWrapper parser = utahParserTool.getParser(config, dataSource);
+        final List<Map<String, String>> records = parser.toList();
+
+        assertEquals(3, records.size());
+    }
+
+    @Test
+    public void shallGetHeadersFromRecord() {
+        final UtahParserTool utahParserTool = utahParserTool();
+        final Map<String, Object> record = MapBuilder.toLinkedMap("header1", "foo", "header2", "bar");
+
+        final List<String> headers = utahParserTool.getHeaders(record);
+
+        assertEquals("header1", headers.get(0));
+        assertEquals("header2", headers.get(1));
+    }
+
+    @Test
+    public void shallGetHeadersFromRecords() {
+        final DataSource dataSource = dataSource(EXAMPLE_FILE_NAME);
+        final UtahParserTool utahParserTool = utahParserTool();
+        final Config config = utahParserTool.getConfig(TEMPLATE_FILE_NAME);
+        final ParserWrapper parser = utahParserTool.getParser(config, dataSource);
+        final List<Map<String, String>> records = parser.toList();
+
+        final List<String> headers = utahParserTool.getHeaders(records);
+
+        assertEquals("accepted_V4", headers.get(0));
+        assertEquals("accepted_V6", headers.get(1));
+        assertEquals("activeV4", headers.get(2));
+        assertEquals("activeV6", headers.get(3));
+        assertEquals("receivedV4", headers.get(4));
+        assertEquals("receivedV6", headers.get(5));
+        assertEquals("remoteIp", headers.get(6));
+        assertEquals("uptime", headers.get(7));
+    }
+
+    private static UtahParserTool utahParserTool() {
+        return new UtahParserTool();
+    }
+
+    private static DataSource dataSource(String fileName) {
+        return DataSourceFactory.fromFile(new File(fileName), UTF_8);
+    }
+
+}
diff --git a/freemarker-generator-website/pom.xml b/freemarker-generator-website/pom.xml
index a5cc6ff..f8424c5 100644
--- a/freemarker-generator-website/pom.xml
+++ b/freemarker-generator-website/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.apache.freemarker.generator</groupId>
         <artifactId>freemarker-generator</artifactId>
-        <version>0.1.0-SNAPSHOT</version>
+        <version>0.2.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>freemarker-generator-website</artifactId>
diff --git a/freemarker-generator-website/src/main/docgen/book.xml b/freemarker-generator-website/src/main/docgen/book.xml
index 791051c..0da2433 100644
--- a/freemarker-generator-website/src/main/docgen/book.xml
+++ b/freemarker-generator-website/src/main/docgen/book.xml
@@ -627,15 +627,15 @@
 [/docgen.insertWithOutput]
 
 [docgen.insertWithOutput]
-freemarker-generator -i '${tools.jsonpath.parse(dataSources?values[0]).read("$.info.title")}' [docgen.wd]/examples/data/json/swagger-spec.json
+freemarker-generator -i '${tools.jsonpath.parse(dataSources[0]).read("$.info.title")}' [docgen.wd]/examples/data/json/swagger-spec.json
 [/docgen.insertWithOutput]
 
 [docgen.insertWithOutput]
-freemarker-generator -i '${tools.xml.parse(dataSources?values[0])["recipients/person[1]/name"]}' [docgen.wd]/examples/data/xml/recipients.xml
+freemarker-generator -i '${tools.xml.parse(dataSources[0])["recipients/person[1]/name"]}' [docgen.wd]/examples/data/xml/recipients.xml
 [/docgen.insertWithOutput]
 
 [docgen.insertWithOutput]
-freemarker-generator -i '${tools.jsoup.parse(dataSources?values[0]).select("a")[0]}' [docgen.wd]/examples/data/html/dependencies.html
+freemarker-generator -i '${tools.jsoup.parse(dataSources[0]).select("a")[0]}' [docgen.wd]/examples/data/html/dependencies.html
 [/docgen.insertWithOutput]</programlisting>
 
         <remark>I have deleted some examples above, as they were problematic
@@ -653,7 +653,7 @@
         filtering (based on some primary key) and and transforming into a
         human-readable output format (Markdown).</para>
 
-        <para>So lets start the filtering &amp; transformaction using the
+        <para>So lets start the filtering &amp; transformation using the
         following command line</para>
 
         <programlisting>[docgen.insertWithOutput]
@@ -674,11 +674,11 @@
         YAML or the other way around.</para>
 
         <programlisting>[docgen.insertWithOutput]freemarker-generator -t freemarker-generator/yaml/json/transform.ftl [docgen.wd]/examples/data/yaml/swagger-spec.yaml[/docgen.insertWithOutput]
-[docgen.insertWithOutput]freemarker-generator -i '${tools.gson.toJson(tools.yaml.parse(dataSources?values[0]))}' [docgen.wd]/examples/data/yaml/swagger-spec.yaml[/docgen.insertWithOutput]
+[docgen.insertWithOutput]freemarker-generator -i '${tools.gson.toJson(tools.yaml.parse(dataSources[0]))}' [docgen.wd]/examples/data/yaml/swagger-spec.yaml[/docgen.insertWithOutput]
 [docgen.insertWithOutput]freemarker-generator -i '${tools.gson.toJson(yaml)}' -m yaml=[docgen.wd]/examples/data/yaml/swagger-spec.yaml[/docgen.insertWithOutput]
 
 [docgen.insertWithOutput]freemarker-generator -t freemarker-generator/json/yaml/transform.ftl [docgen.wd]/examples/data/json/swagger-spec.json[/docgen.insertWithOutput]
-[docgen.insertWithOutput]freemarker-generator -i '${tools.yaml.toYaml(tools.gson.parse(dataSources?values[0]))}' [docgen.wd]/examples/data/json/swagger-spec.json[/docgen.insertWithOutput]
+[docgen.insertWithOutput]freemarker-generator -i '${tools.yaml.toYaml(tools.gson.parse(dataSources[0]))}' [docgen.wd]/examples/data/json/swagger-spec.json[/docgen.insertWithOutput]
 [docgen.insertWithOutput]freemarker-generator -i '${tools.yaml.toYaml(json)}' -m json=[docgen.wd]/examples/data/json/swagger-spec.json[/docgen.insertWithOutput]</programlisting>
       </section>
 
@@ -848,20 +848,20 @@
         <para>A <literal>DataSource</literal> can be loaded from the file
         system, e.g., as positional command line argument:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl [docgen.wd]/README.md
 [/docgen.insertWithOutput]</programlisting>
 
         <para>from an URL:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl --data-source xkcd=https://xkcd.com/info.0.json
 [/docgen.insertWithOutput]</programlisting>
 
         <para>or from an environment variable, e.g.
         <literal>NGINX_CONF</literal> having a JSON payload:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"
   systemProperties={'freemarker.generator.datasource.envOverride.NGINX_CONF': '{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'}
 ]
 freemarker-generator -t freemarker-generator/info.ftl --data-source conf=env:///NGINX_CONF#mimeType=application/json
@@ -869,20 +869,20 @@
 
         <para>Of course you can load multiple data sources directly:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl [docgen.wd]/README.md xkcd=https://xkcd.com/info.0.json
 [/docgen.insertWithOutput]</programlisting>
 
         <para>or load them from a directory:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl --data-source [docgen.wd]/examples/data
 [/docgen.insertWithOutput]</programlisting>
 
         <para>which can be combined with <literal>include</literal> and
         <literal>exclude</literal> filters:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl -s [docgen.wd]/examples/data --data-source-include='*.json' 
 [/docgen.insertWithOutput]</programlisting>
 
@@ -894,7 +894,7 @@
 
         <programlisting>cat examples/data/csv/contract.csv | bin/freemarker-generator -t freemarker-generator/info.ftl --stdin
 
-FreeMarker Generator DataSources
+FreeMarker Generator Data Sources
 ------------------------------------------------------------------------------
 [#1]: name=stdin, group=default, fileName=stdin mimeType=text/plain, charset=UTF-8, length=-1 Bytes
 URI : system:///stdin</programlisting>
@@ -909,8 +909,8 @@
 
         <itemizedlist>
           <listitem>
-            <para><literal>dataSources?values[0]</literal> or
-            <literal>dataSources?values?first</literal> selects the first data
+            <para><literal>dataSources[0]</literal> or
+            <literal>dataSources?first</literal> selects the first data
             source</para>
           </listitem>
 
@@ -944,7 +944,7 @@
 &lt;/#list&gt;
 
 &lt;#-- Iterate over a list of data sources --&gt;
-&lt;#list dataSources?values as dataSource&gt;
+&lt;#list dataSources as dataSource&gt;
 - [#${dataSource?counter}]: name=${dataSource.name}
 &lt;/#list&gt;</programlisting>
       </section>
@@ -959,22 +959,22 @@
         here)</remark>:</para>
 
         <programlisting>&lt;#-- List all data sources containing "test" in the name --&gt;
-&lt;#list dataSources?values?filter(ds -&gt; ds.match("name", "*test*")) as ds&gt;
+&lt;#list dataSources?filter(ds -&gt; ds.match("name", "*test*")) as ds&gt;
 - ${ds.name}
 &lt;/#list&gt;
 
 &lt;#-- List all data sources having "json" extension --&gt;
-&lt;#list dataSources?values?filter(ds -&gt; ds.match("extension", "json")) as ds&gt;
+&lt;#list dataSources?filter(ds -&gt; ds.match("extension", "json")) as ds&gt;
 - ${ds.name}
 &lt;/#list&gt;
 
 &lt;#-- List all data sources having "src/test/data/properties" in their file path --&gt;
-&lt;#list dataSources?values?filter(ds -&gt; ds.match("filePath", "*/src/test/data/properties")) as ds&gt;
+&lt;#list dataSources?filter(ds -&gt; ds.match("filePath", "*/src/test/data/properties")) as ds&gt;
 - ${ds.name}
 &lt;/#list&gt;
 
 &lt;#-- List all data sources of a group --&gt;
-&lt;#list dataSources?values?filter(ds -&gt; ds.match("group", "default")) as ds&gt;
+&lt;#list dataSources?filter(ds -&gt; ds.match("group", "default")) as ds&gt;
 - ${ds.name}
 &lt;/#list&gt;</programlisting>
       </section>
@@ -1073,27 +1073,27 @@
         <para>The following Named URI loads a <literal>user.csv</literal> and
         the data source is available as <literal>my_users</literal>:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl my_users=[docgen.wd]/examples/data/csv/user.csv
 [/docgen.insertWithOutput]</programlisting>
 
         <para>A Named URI allows to pass additional information as part of the
         fragment, e.g. the charset of the text file:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl my_users=[docgen.wd]/examples/data/csv/user.csv#charset=UTF-16
 [/docgen.insertWithOutput]</programlisting>
 
         <para>In addition to the simplified file syntax full URIs can be
         used:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl https://www.google.com#charset=ISO-8859-1
 [/docgen.insertWithOutput]</programlisting>
 
         <para>and also combined with a name:</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl page=http://www.google.com#charset=ISO-8859-1
 [/docgen.insertWithOutput]</programlisting>
       </section>
@@ -1105,14 +1105,14 @@
 
         <para>Load all CVS files of a directory using the group "csv":</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
 freemarker-generator -t freemarker-generator/info.ftl :csv=[docgen.wd]/examples/data/csv
 [/docgen.insertWithOutput]</programlisting>
 
         <para>or use a charset for all files of a directory</para>
 
-        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator DataSources" to=r"^\s*$"]
-freemarker-generator -t freemarker-generator/info.ftl '[docgen.wd]/examples/data/csv#charset=UTF-16&amp;mimetype=text/plain'
+        <programlisting>[docgen.insertWithOutput from="FreeMarker Generator Data Sources" to=r"^\s*$"]
+freemarker-generator -t freemarker-generator/info.ftl '[docgen.wd]/examples/data/csv#charset=UTF-16&amp;mimeType=text/plain'
 [/docgen.insertWithOutput]</programlisting>
 
         <para>It is also possible to provide data source properties to all
diff --git a/licences/LICENCE_utahparser.txt b/licences/LICENCE_utahparser.txt
new file mode 100644
index 0000000..9c8f3ea
--- /dev/null
+++ b/licences/LICENCE_utahparser.txt
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   Licensed 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.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e15144c..daf9e42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
     <groupId>org.apache.freemarker.generator</groupId>
     <artifactId>freemarker-generator</artifactId>
     <packaging>pom</packaging>
-    <version>0.1.0-SNAPSHOT</version>
+    <version>0.2.0-SNAPSHOT</version>
     <name>Apache FreeMarker Generator</name>
     <url>https://freemarker-generator.apache.org/</url>
 
@@ -163,6 +163,10 @@
                     <version>3.12.0</version>
                 </plugin>
                 <plugin>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>3.0.0-M5</version>
+                </plugin>
+                <plugin>
                     <!-- Required by custom Maven skin -->
                     <artifactId>maven-site-plugin</artifactId>
                     <version>3.8.2</version>
@@ -219,6 +223,10 @@
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-report-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-pmd-plugin</artifactId>
                 <configuration>
                     <linkXRef>false</linkXRef>