FREEMARKER-161 [freemarker-generator] Allow multiple transformations on the CLI (#25)

diff --git a/NOTICE b/NOTICE
index c27f50e..60b453f 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
 Apache FreeMarker Site
-Copyright 2020 The Apache Software Foundation
+Copyright 2021 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
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 b2498d8..bc89df3 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
@@ -85,7 +85,7 @@
         } else if (file.isDirectory()) {
             return resolveDirectory(file);
         } else {
-            throw new IllegalArgumentException("Unable to find source: " + source);
+            throw new IllegalArgumentException("Unable to find file: " + source);
         }
     }
 
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
new file mode 100644
index 0000000..5ce4cf4
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/output/OutputGenerator.java
@@ -0,0 +1,81 @@
+/*
+ * 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.output;
+
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.template.TemplateOutput;
+import org.apache.freemarker.generator.base.template.TemplateSource;
+
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Information about loading templates and writing their output.
+ */
+public class OutputGenerator {
+
+    /** Source of template */
+    private final TemplateSource templateSource;
+
+    /** Output of template */
+    private final TemplateOutput templateOutput;
+
+    /** Data sources used for the transformation */
+    private final List<DataSource> dataSources;
+
+    /** Variables (as a map) used for the transformation */
+    private final Map<String, Object> variables;
+
+    public OutputGenerator(
+            TemplateSource templateSource,
+            TemplateOutput templateOutput,
+            List<DataSource> dataSources,
+            Map<String, Object> variables) {
+        this.templateSource = requireNonNull(templateSource);
+        this.templateOutput = requireNonNull(templateOutput);
+        this.dataSources = requireNonNull(dataSources);
+        this.variables = requireNonNull(variables);
+    }
+
+    public TemplateSource getTemplateSource() {
+        return templateSource;
+    }
+
+    public TemplateOutput getTemplateOutput() {
+        return templateOutput;
+    }
+
+    public List<DataSource> getDataSources() {
+        return dataSources;
+    }
+
+    public Map<String, Object> getVariables() {
+        return variables;
+    }
+
+    @Override
+    public String toString() {
+        return "OutputGenerator{" +
+                "templateSource=" + templateSource +
+                ", templateOutput=" + templateOutput +
+                ", dataSources=" + dataSources +
+                ", variables=" + variables +
+                '}';
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
index 818d66d..89a8bb5 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
@@ -16,12 +16,9 @@
  */
 package org.apache.freemarker.generator.base.template;
 
-import org.apache.freemarker.generator.base.util.Validate;
-
 import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
 import java.io.Writer;
+import java.nio.charset.Charset;
 
 import static java.util.Objects.requireNonNull;
 
@@ -36,43 +33,42 @@
 
     private final Writer writer;
     private final File file;
+    private final Charset charset;
 
-    private TemplateOutput(File file) {
+    private TemplateOutput(File file, final Charset charset) {
         this.writer = null;
         this.file = requireNonNull(file);
+        this.charset = requireNonNull(charset);
     }
 
     private TemplateOutput(Writer writer) {
         this.writer = requireNonNull(writer);
         this.file = null;
+        this.charset = null;
     }
 
     public static TemplateOutput fromWriter(Writer writer) {
         return new TemplateOutput(writer);
     }
 
-    public static TemplateOutput fromFile(File file) {
-        return new TemplateOutput(file);
+    public static TemplateOutput fromFile(File file, final Charset charset) {
+        return new TemplateOutput(file, charset);
     }
 
     public Writer getWriter() {
         return writer;
     }
 
+    public boolean hasWriter() {
+        return writer != null;
+    }
+
     public File getFile() {
         return file;
     }
 
-    public boolean isWrittenToFile() {
-        return file != null;
-    }
-
-    public boolean isWrittenToSuppliedWriter() {
-        return writer != null;
-    }
-
-    public Writer writer() {
-        return writer != null ? writer : fileWriter();
+    public Charset getCharset() {
+        return charset;
     }
 
     @Override
@@ -80,16 +76,7 @@
         return "TemplateOutput{" +
                 "writer=" + writer +
                 ", file=" + file +
+                ", charset=" + charset +
                 '}';
     }
-
-    private FileWriter fileWriter() {
-        Validate.notNull(file, "Output file is null");
-
-        try {
-            return new FileWriter(file);
-        } catch (IOException e) {
-            throw new RuntimeException("Failed to create FileWriter: " + file.getAbsolutePath(), e);
-        }
-    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSource.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSource.java
index fb50b2a..a8b0c65 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSource.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSource.java
@@ -29,8 +29,8 @@
 public class TemplateSource {
 
     public enum Origin {
-        PATH,
-        CODE
+        TEMPLATE_LOADER,
+        TEMPLATE_CODE
     }
 
     /** Name of template for diagnostics */
@@ -50,7 +50,7 @@
 
     private TemplateSource(String name, String code) {
         this.name = name;
-        this.origin = Origin.CODE;
+        this.origin = Origin.TEMPLATE_CODE;
         this.code = code;
         this.path = null;
         this.encoding = StandardCharsets.UTF_8;
@@ -58,7 +58,7 @@
 
     private TemplateSource(String name, String path, Charset encoding) {
         this.name = name;
-        this.origin = Origin.PATH;
+        this.origin = Origin.TEMPLATE_LOADER;
         this.code = null;
         this.path = path;
         this.encoding = encoding;
@@ -67,22 +67,10 @@
     /**
      * Template will be loaded from path using a file-base template loader.
      *
-     * @param path template path
-     * @return file-based template source
-     */
-    public static TemplateSource fromPath(String path) {
-        Validate.notEmpty(path, "Template path is empty");
-        return new TemplateSource(path, path, StandardCharsets.UTF_8);
-    }
-
-    /**
-     * Template will be loaded from path using a file-base template loader.
-     *
      * @param path     template path
      * @param encoding character encoding og template
      * @return file-based template source
      */
-
     public static TemplateSource fromPath(String path, Charset encoding) {
         Validate.notEmpty(path, "Template path is empty");
         Validate.notNull(encoding, "Template encoding is null");
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSourceFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSourceFactory.java
deleted file mode 100644
index ab53ee3..0000000
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSourceFactory.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.template;
-
-import org.apache.freemarker.generator.base.datasource.DataSource;
-import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
-import org.apache.freemarker.generator.base.util.UriUtils;
-
-import java.io.File;
-
-public abstract class TemplateSourceFactory {
-
-    public static TemplateSource create(String str) {
-        if (isTemplatePath(str)) {
-            return TemplateSource.fromPath(str);
-        } else {
-            try (DataSource dataSource = DataSourceFactory.create(str)) {
-                return TemplateSource.fromCode(dataSource.getName(), dataSource.getText());
-            }
-        }
-    }
-
-    private static boolean isTemplatePath(String str) {
-        return !UriUtils.isUri(str) && !new File(str).exists();
-    }
-}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
index 8c19478..6f0ed90 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
@@ -44,7 +44,7 @@
 
     @Override
     public String toString() {
-        return "TemplateTransformation{" +
+        return "OutputGenerator{" +
                 "templateSource=" + templateSource +
                 ", templateOutput=" + templateOutput +
                 '}';
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformations.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformations.java
deleted file mode 100644
index e41162d..0000000
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformations.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.template;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Keeps track of all transformations being executed.
- */
-public class TemplateTransformations {
-
-    private final List<TemplateTransformation> templateTransformations;
-
-    public TemplateTransformations(Collection<? extends TemplateTransformation> templateTransformations) {
-        this.templateTransformations = new ArrayList<>(requireNonNull(templateTransformations));
-    }
-
-    public List<? extends TemplateTransformation> getList() {
-        return templateTransformations;
-    }
-
-    public TemplateTransformation get(int index) {
-        return templateTransformations.get(index);
-    }
-
-    public int size() {
-        return templateTransformations.size();
-    }
-}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
index bdef609..6aaf494 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
@@ -17,6 +17,8 @@
 package org.apache.freemarker.generator.base.template;
 
 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.file.RecursiveFileSupplier;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
 import org.apache.freemarker.generator.base.util.StringUtils;
@@ -26,24 +28,24 @@
 import java.io.File;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
+import java.nio.charset.Charset;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
-import java.util.Optional;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Collections.singletonList;
 
 /**
- * Provide the logic to define multiple transformations from the user input.
+ * Maps user-supplied templates (interactive, files, directories, paths) to a
+ * list of TemplateTransformation.
  */
 public class TemplateTransformationsBuilder {
 
     /** Interactive template */
-    private TemplateSource template;
+    private TemplateSource interactiveTemplate;
 
-    /** List of templates and/or template directories to be rendered */
-    private final List<String> sources;
+    /** Template source - either a single template or a directory */
+    private String templateSource;
 
     /** Optional include patterns for resolving source templates or template directories */
     private final List<String> includes;
@@ -51,59 +53,56 @@
     /** Optional exclude patterns for resolving source templates or template directories */
     private final List<String> excludes;
 
-    /** Optional output file or directory */
-    private final List<String> outputs;
+    /** Encoding used to load templates */
+    private Charset templateEncoding;
 
-    /** Optional user-supplied writer */
-    private Writer writer;
+    /** Optional output file or directory - if none is defined everything is written to STDOUT */
+    private String output;
+
+    /** Optional output encoding */
+    private Charset outputEncoding;
+
+    /** Optional caller-supplied writer used for testing */
+    private Writer callerSuppliedWriter;
 
     private TemplateTransformationsBuilder() {
-        this.sources = new ArrayList<>();
+        this.templateSource = null;
         this.includes = new ArrayList<>();
         this.excludes = new ArrayList<>();
-        this.outputs = new ArrayList<>();
-        this.writer = null;
+        this.templateEncoding = UTF_8;
+        this.output = null;
+        this.callerSuppliedWriter = null;
+        this.outputEncoding = UTF_8;
     }
 
     public static TemplateTransformationsBuilder builder() {
         return new TemplateTransformationsBuilder();
     }
 
-    public TemplateTransformations build() {
+    public List<TemplateTransformation> build() {
         validate();
 
         final List<TemplateTransformation> result = new ArrayList<>();
+        final File outputFile = getOutputFile();
 
         if (hasInteractiveTemplate()) {
-            final File outputFile = getOutputFile(0).orElse(null);
             result.add(resolveInteractiveTemplate(outputFile));
         } else {
-            for (int i = 0; i < sources.size(); i++) {
-                final String source = sources.get(i);
-                final File output = getOutputFile(i).orElse(null);
-                result.addAll(resolve(source, output));
-            }
+            result.addAll(resolve(templateSource, outputFile));
         }
 
-        return new TemplateTransformations(result);
+        return result;
     }
 
-    public TemplateTransformationsBuilder setTemplate(String name, String code) {
+    public TemplateTransformationsBuilder setInteractiveTemplate(String code) {
         if (StringUtils.isNotEmpty(code)) {
-            this.template = TemplateSource.fromCode(name, code);
+            this.interactiveTemplate = TemplateSource.fromCode(Location.INTERACTIVE, code);
         }
         return this;
     }
 
-    public TemplateTransformationsBuilder addSource(String source) {
-        if (StringUtils.isNotEmpty(source)) {
-            this.sources.add(source);
-        }
-        return this;
-    }
-
-    public TemplateTransformationsBuilder addSources(Collection<String> sources) {
-        sources.forEach(this::addSource);
+    public TemplateTransformationsBuilder setTemplateSource(String source) {
+        this.templateSource = source;
         return this;
     }
 
@@ -114,13 +113,6 @@
         return this;
     }
 
-    public TemplateTransformationsBuilder addIncludes(Collection<String> includes) {
-        if (includes != null) {
-            this.includes.addAll(includes);
-        }
-        return this;
-    }
-
     public TemplateTransformationsBuilder addExclude(String exclude) {
         if (StringUtils.isNotEmpty(exclude)) {
             this.excludes.add(exclude);
@@ -128,40 +120,34 @@
         return this;
     }
 
-    public TemplateTransformationsBuilder addExcludes(Collection<String> excludes) {
-        if (excludes != null) {
-            this.excludes.addAll(excludes);
+    public TemplateTransformationsBuilder setTemplateEncoding(Charset charset) {
+        if (charset != null) {
+            this.templateEncoding = charset;
         }
         return this;
     }
 
-    public TemplateTransformationsBuilder addOutputs(Collection<String> outputs) {
-        if (outputs != null) {
-            this.outputs.addAll(outputs);
+    public TemplateTransformationsBuilder setOutput(String output) {
+        this.output = output;
+        return this;
+    }
+
+    public TemplateTransformationsBuilder setOutputEncoding(Charset outputEncoding) {
+        if (outputEncoding != null) {
+            // keep UTF-8 here
+            this.outputEncoding = outputEncoding;
         }
+
         return this;
     }
 
-    public TemplateTransformationsBuilder addOutput(String output) {
-        if (StringUtils.isNotEmpty(output)) {
-            this.outputs.add(output);
-        }
-        return this;
-    }
-
-    public TemplateTransformationsBuilder setWriter(Writer writer) {
-        this.writer = writer;
-        return this;
-    }
-
-    public TemplateTransformationsBuilder setStdOut() {
-        this.writer = new NonClosableWriterWrapper(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)));
+    public TemplateTransformationsBuilder setCallerSuppliedWriter(Writer callerSuppliedWriter) {
+        this.callerSuppliedWriter = callerSuppliedWriter;
         return this;
     }
 
     private void validate() {
-        Validate.isTrue(template != null || !sources.isEmpty(), "No template was provided");
-        Validate.isTrue(template == null || sources.isEmpty(), "Interactive template does not support multiple sources");
+        Validate.isTrue(interactiveTemplate == null || templateSource == null, "No template was provided");
     }
 
     /**
@@ -172,14 +158,16 @@
      * @return list of <code>TemplateTransformation</code>
      */
     private List<TemplateTransformation> resolve(String source, File output) {
-        if (isTemplateFile(source)) {
+        if (isTemplateFileFound(source)) {
             return resolveTemplateFile(source, output);
-        } else if (isTemplateDirectory(source)) {
+        } else if (isTemplateDirectoryFound(source)) {
             return resolveTemplateDirectory(source, output);
-        } else if (isTemplatePath(source)) {
-            return resolveTemplatePath(source, output);
+        } else if (isTemplateHttpUrl(source)) {
+            return resolveTemplateHttpUrl(source, output);
+        } else if (isTemplateUri(source)) {
+            return resolveTemplateUri(source, output);
         } else {
-            return resolveTemplateCode(source, output);
+            return resolveTemplatePath(source, output);
         }
     }
 
@@ -206,33 +194,43 @@
         return templateTransformations;
     }
 
-    private List<TemplateTransformation> resolveTemplatePath(String source, File out) {
+    private List<TemplateTransformation> resolveTemplateHttpUrl(String source, File out) {
         final TemplateSource templateSource = templateSource(source);
         final TemplateOutput templateOutput = templateOutput(out);
         return singletonList(new TemplateTransformation(templateSource, templateOutput));
     }
 
+    private List<TemplateTransformation> resolveTemplateUri(String source, File out) {
+        final TemplateSource templateSource = templateSource(source);
+        final TemplateOutput templateOutput = templateOutput(out);
+        return singletonList(new TemplateTransformation(templateSource, templateOutput));
+    }
+
+    private List<TemplateTransformation> resolveTemplatePath(String source, File out) {
+        final TemplateSource templateSource = TemplateSource.fromPath(source, templateEncoding);
+        final TemplateOutput templateOutput = templateOutput(out);
+        return singletonList(new TemplateTransformation(templateSource, templateOutput));
+    }
+
     private TemplateTransformation resolveInteractiveTemplate(File out) {
         final TemplateOutput templateOutput = templateOutput(out);
-        return new TemplateTransformation(template, templateOutput);
-    }
-
-    private List<TemplateTransformation> resolveTemplateCode(String source, File out) {
-        final TemplateSource templateSource = TemplateSource.fromCode(Location.INTERACTIVE, source);
-        final TemplateOutput templateOutput = templateOutput(out);
-        return singletonList(new TemplateTransformation(templateSource, templateOutput));
+        return new TemplateTransformation(interactiveTemplate, templateOutput);
     }
 
     private TemplateOutput templateOutput(File templateOutputFile) {
-        if (writer == null && templateOutputFile != null) {
-            return TemplateOutput.fromFile(templateOutputFile);
+        if (callerSuppliedWriter != null) {
+            return TemplateOutput.fromWriter(callerSuppliedWriter);
+        } else if (templateOutputFile != null) {
+            return TemplateOutput.fromFile(templateOutputFile, outputEncoding);
         } else {
-            return TemplateOutput.fromWriter(writer);
+            return TemplateOutput.fromWriter(stdoutWriter(outputEncoding));
         }
     }
 
     private TemplateSource templateSource(String source) {
-        return TemplateSourceFactory.create(source);
+        try (DataSource dataSource = DataSourceFactory.create(source)) {
+            return TemplateSource.fromCode(dataSource.getName(), dataSource.getText(templateEncoding.name()));
+        }
     }
 
     private String getInclude() {
@@ -244,37 +242,40 @@
     }
 
     private boolean hasInteractiveTemplate() {
-        return template != null;
+        return interactiveTemplate != null;
     }
 
-    private Optional<File> getOutputFile(int i) {
-        if (outputs.isEmpty()) {
-            return Optional.empty();
-        } else if (i < outputs.size()) {
-            return Optional.of(new File(outputs.get(i)));
-        } else {
-            return Optional.of(new File(outputs.get(0)));
-        }
+    private File getOutputFile() {
+        return output == null ? null : new File(output);
     }
 
     private static File getTemplateOutputFile(File templateDirectory, File templateFile, File outputDirectory) {
+        if (outputDirectory == null) {
+            // missing output directory uses STDOUT
+            return null;
+        }
+
         final String relativePath = relativePath(templateDirectory, templateFile);
         final String relativeOutputFileName = mapExtension(relativePath);
         return new File(outputDirectory, relativeOutputFileName);
     }
 
-    private static boolean isTemplateFile(String source) {
+    private static boolean isTemplateFileFound(String source) {
         final File file = new File(source);
         return file.exists() && file.isFile();
     }
 
-    private static boolean isTemplateDirectory(String source) {
+    private static boolean isTemplateDirectoryFound(String source) {
         final File file = new File(source);
         return file.exists() && file.isDirectory();
     }
 
-    private static boolean isTemplatePath(String source) {
-        return !isTemplateFile(source) && !isTemplateDirectory(source);
+    private static boolean isTemplateHttpUrl(String source) {
+        return source.contains("http://") || source.contains("https://");
+    }
+
+    private static boolean isTemplateUri(String source) {
+        return source.contains("://");
     }
 
     private static RecursiveFileSupplier templateFilesSupplier(String source, String include, String exclude) {
@@ -294,4 +295,9 @@
             return fileName;
         }
     }
+
+    private Writer stdoutWriter(Charset outputEncoding) {
+        // avoid closing System.out after rendering the template
+        return new BufferedWriter(new NonClosableWriterWrapper(new OutputStreamWriter(System.out, outputEncoding)));
+    }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
index 60daa80..561c13b 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
@@ -22,18 +22,18 @@
 
 public class MapBuilder {
 
-    public static Map<String, Object> toLinkedMap(Object... data) {
+    public static Map<String, Object> toLinkedMap(Object... nameValuePairs) {
 
         final HashMap<String, Object> map = new LinkedHashMap<>();
 
-        if (data.length % 2 != 0) {
+        if (nameValuePairs.length % 2 != 0) {
             throw new IllegalArgumentException("Odd number of arguments");
         }
 
         String currKey = null;
         int step = -1;
 
-        for (Object value : data) {
+        for (Object value : nameValuePairs) {
             step++;
             switch (step % 2) {
                 case 0:
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapFlattener.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapFlattener.java
deleted file mode 100644
index c1ad75a..0000000
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapFlattener.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.freemarker.generator.base.util;
-
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.function.Function;
-import java.util.function.UnaryOperator;
-
-/**
- * Flattens a hierarchical {@link Map} of objects into a property {@link Map}.
- * <p>
- * Flattening is particularly useful when representing a JSON object as
- * {@link java.util.Properties}
- * <p>
- * {@link MapFlattener} flattens {@link Map maps} containing nested
- * {@link java.util.List}, {@link Map} and simple values into a flat representation. The
- * hierarchical structure is reflected in properties using dot-notation. Nested maps are
- * considered as sub-documents.
- * <p>
- * Input:
- *
- * <pre class="code">
- *     {"key": {"nested: 1}, "another.key": ["one", "two"] }
- * </pre>
- *
- * <br>
- * Result
- *
- * <pre class="code">
- *  key.nested=1
- *  another.key[0]=one
- *  another.key[1]=two
- * </pre>
- *
- * @author Mark Paluch
- * <p>
- * Copied from https://github.com/spring-projects/spring-vault/blob/master/spring-vault-core/src/main/java/org/springframework/vault/support/JsonMapFlattener.java
- */
-public abstract class MapFlattener {
-
-    private MapFlattener() {
-    }
-
-    /**
-     * Flatten a hierarchical {@link Map} into a flat {@link Map} with key names using
-     * property dot notation.
-     *
-     * @param inputMap must not be {@literal null}.
-     * @return the resulting {@link Map}.
-     */
-    public static Map<String, Object> flatten(Map<String, ?> inputMap) {
-        Validate.notNull(inputMap, "Input Map must not be null");
-
-        final Map<String, Object> resultMap = new LinkedHashMap<>();
-        doFlatten("", inputMap.entrySet().iterator(), resultMap, UnaryOperator.identity());
-        return resultMap;
-    }
-
-    /**
-     * Flatten a hierarchical {@link Map} into a flat {@link Map} with key names using
-     * property dot notation.
-     *
-     * @param inputMap must not be {@literal null}.
-     * @return the resulting {@link Map}.
-     * @since 2.0
-     */
-    public static Map<String, String> flattenToStringMap(Map<String, ?> inputMap) {
-        Validate.notNull(inputMap, "inputMap is null");
-
-        final Map<String, String> resultMap = new LinkedHashMap<>();
-        doFlatten("", inputMap.entrySet().iterator(), resultMap, it -> it == null ? null : it.toString());
-        return resultMap;
-    }
-
-    private static void doFlatten(String propertyPrefix,
-                                  Iterator<? extends Entry<String, ?>> inputMap,
-                                  Map<String, ?> resultMap,
-                                  Function<Object, Object> valueTransformer) {
-
-        if (StringUtils.isNotEmpty(propertyPrefix)) {
-            propertyPrefix = propertyPrefix + ".";
-        }
-
-        while (inputMap.hasNext()) {
-            final Entry<String, ?> entry = inputMap.next();
-            flattenElement(
-                    propertyPrefix.concat(entry.getKey()),
-                    entry.getValue(),
-                    resultMap,
-                    valueTransformer);
-        }
-    }
-
-    @SuppressWarnings({ "rawtypes", "unchecked" })
-    private static void flattenElement(String propertyPrefix, Object source,
-                                       Map<String, ?> resultMap, Function<Object, Object> valueTransformer) {
-
-        if (source instanceof Iterable) {
-            flattenCollection(propertyPrefix, (Iterable<Object>) source, resultMap,
-                    valueTransformer);
-            return;
-        }
-
-        if (source instanceof Map) {
-            doFlatten(propertyPrefix, ((Map<String, ?>) source).entrySet().iterator(),
-                    resultMap, valueTransformer);
-            return;
-        }
-
-        ((Map) resultMap).put(propertyPrefix, valueTransformer.apply(source));
-    }
-
-    private static void flattenCollection(String propertyPrefix,
-                                          Iterable<Object> iterable, Map<String, ?> resultMap,
-                                          Function<Object, Object> valueTransformer) {
-
-        int counter = 0;
-        for (Object element : iterable) {
-            flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap, valueTransformer);
-            counter++;
-        }
-    }
-}
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
new file mode 100644
index 0000000..2a4c84c
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
@@ -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.
+ */
+package org.apache.freemarker.generator.base.util;
+
+import java.util.Locale;
+
+/**
+ * Helper class to detect the operting system (mostly Windows).
+ *
+ * TODO should be moved to "freemarker-generator-base"
+ */
+public class OperatingSystem {
+    private static final String OS = System.getProperty("os.name", "unknown").toLowerCase(Locale.ROOT);
+
+    public static boolean isWindows() {
+        return OS.contains("win");
+    }
+
+    public static boolean isMac() {
+        return OS.contains("mac");
+    }
+
+    public static boolean isUnix() {
+        return OS.contains("nux");
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
index fab27e0..b8135bb 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
@@ -50,7 +50,7 @@
     public static String toStringWithoutFragment(URI uri) {
         final String str = uri.toString();
         final int index = str.indexOf('#');
-        return (index > 0) ? str.substring(0, index) : str;
+        return index > 0 ? str.substring(0, index) : str;
     }
 
     public static boolean isUri(String str) {
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateSourceFactoryTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateSourceFactoryTest.java
deleted file mode 100644
index 0124a98..0000000
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateSourceFactoryTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.template;
-
-import org.apache.freemarker.generator.base.template.TemplateSource;
-import org.apache.freemarker.generator.base.template.TemplateSource.Origin;
-import org.apache.freemarker.generator.base.template.TemplateSourceFactory;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-public class TemplateSourceFactoryTest {
-
-    private static final String ANY_TEMPLATE_PATH = "any/template/path.ftl";
-    private static final String ANY_FILE_NAME = "pom.xml";
-    private static final String ANY_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_VARIABLE = "JAVA_HOME";
-    private static final String ANY_ENV_URI = "env:///" + ANY_ENV_VARIABLE;
-
-    @Test
-    public void shouldCreateFromTemplatePath() {
-        final TemplateSource templateSource = TemplateSourceFactory.create(ANY_TEMPLATE_PATH);
-
-        assertEquals(ANY_TEMPLATE_PATH, templateSource.getName());
-        assertEquals(Origin.PATH, templateSource.getOrigin());
-        assertEquals(ANY_TEMPLATE_PATH, templateSource.getPath());
-        assertNull(templateSource.getCode());
-    }
-
-    @Test
-    public void shouldCreateFromFile() {
-        final TemplateSource templateSource = TemplateSourceFactory.create(ANY_FILE_NAME);
-
-        assertEquals(ANY_FILE_NAME, templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
-        assertNull(templateSource.getPath());
-        assertFalse(templateSource.getCode().isEmpty());
-    }
-
-    @Test
-    public void shouldCreateFromEnvironmentVariable() {
-        final TemplateSource templateSource = TemplateSourceFactory.create(ANY_ENV_URI);
-
-        assertEquals(ANY_ENV_VARIABLE, templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
-        assertNull(templateSource.getPath());
-        assertFalse(templateSource.getCode().isEmpty());
-    }
-
-    @Test
-    // @Ignore("Requires internet access")
-    public void shouldCreateFromUrl() {
-        final TemplateSource templateSource = TemplateSourceFactory.create(ANY_URL);
-
-        assertNotNull(templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
-        assertNull(templateSource.getPath());
-        assertFalse(templateSource.getCode().isEmpty());
-    }
-
-    @Test
-    // @Ignore("Requires internet access")
-    public void shouldCreateFromNamedUri() {
-        final TemplateSource templateSource = TemplateSourceFactory.create("info=" + ANY_URL);
-
-        assertEquals("info", templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
-        assertNull(templateSource.getPath());
-        assertFalse(templateSource.getCode().isEmpty());
-    }
-}
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
index 0c082a0..822b66a 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
@@ -20,16 +20,23 @@
 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.TemplateTransformations;
+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.Test;
 
-import java.io.File;
+import java.io.BufferedWriter;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 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;
 
 public class TemplateTransformationsBuilderTest {
 
@@ -37,14 +44,16 @@
     private static final String OTHER_TEMPLATE_FILE_NAME = "src/test/template/nginx/nginx.conf.ftl";
     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_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";
 
     // === Interactive Template =============================================
 
     @Test
     public void shouldCreateFromInteractiveTemplate() {
-        final TemplateTransformations transformations = builder()
-                .setTemplate(Location.INTERACTIVE, "Hello World")
-                .setStdOut()
+        final List<TemplateTransformation> transformations = builder()
+                .setInteractiveTemplate("Hello World")
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(1, transformations.size());
@@ -53,7 +62,7 @@
         final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
 
         assertEquals(Location.INTERACTIVE, templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
+        assertEquals(Origin.TEMPLATE_CODE, templateSource.getOrigin());
         assertEquals("Hello World", templateSource.getCode());
         assertNull(templateSource.getPath());
         assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
@@ -65,20 +74,19 @@
     @Test(expected = IllegalArgumentException.class)
     public void shouldThrowIllegalArgumentExceptionWheMixingInteractiveTemplateWithSources() {
         builder()
-                .setTemplate(Location.INTERACTIVE, "Hello World")
-                .addSource(ANY_TEMPLATE_FILE_NAME)
-                .setStdOut()
+                .setInteractiveTemplate("Hello World")
+                .setTemplateSource(ANY_TEMPLATE_FILE_NAME)
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
     }
 
-
     // === Template File ====================================================
 
     @Test
     public void shouldCreateFromTemplateFile() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_FILE_NAME)
-                .setStdOut()
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_FILE_NAME)
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(1, transformations.size());
@@ -87,8 +95,8 @@
         final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
 
         assertNotNull(templateSource.getName());
-        assertEquals(Origin.CODE, templateSource.getOrigin());
-        assertNotNull(templateSource.getCode());
+        assertEquals(Origin.TEMPLATE_CODE, templateSource.getOrigin());
+        assertTrue(templateSource.getCode().contains("Licensed to the Apache Software Foundation"));
         assertNull(templateSource.getPath());
         assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
 
@@ -96,27 +104,13 @@
         assertNull(templateOutput.getFile());
     }
 
-    @Test
-    public void shouldCreateFromMultipleTemplateFiles() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_FILE_NAME)
-                .addOutput("foo/first.out")
-                .addSource(OTHER_TEMPLATE_FILE_NAME)
-                .addOutput("foo/second.out")
-                .build();
-
-        assertEquals(2, transformations.size());
-        assertEquals(new File("foo/first.out"), transformations.get(0).getTemplateOutput().getFile());
-        assertEquals(new File("foo/second.out"), transformations.get(1).getTemplateOutput().getFile());
-    }
-
     // === Template Path ====================================================
 
     @Test
     public void shouldCreateFromTemplatePath() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_PATH)
-                .setStdOut()
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_PATH)
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(1, transformations.size());
@@ -124,10 +118,10 @@
         final TemplateSource templateSource = transformations.get(0).getTemplateSource();
         final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
 
-        assertNotNull(templateSource.getName());
-        assertEquals(Origin.PATH, templateSource.getOrigin());
+        assertEquals(ANY_TEMPLATE_PATH, templateSource.getName());
+        assertEquals(Origin.TEMPLATE_LOADER, templateSource.getOrigin());
         assertNull(templateSource.getCode());
-        assertNotNull(templateSource.getPath());
+        assertEquals(ANY_TEMPLATE_PATH, templateSource.getPath());
         assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
 
         assertNotNull(templateOutput.getWriter());
@@ -138,9 +132,9 @@
 
     @Test
     public void shouldCreateFromTemplateDirectory() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
-                .setStdOut()
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(2, transformations.size());
@@ -150,9 +144,9 @@
 
     @Test
     public void shouldCreateFromTemplateDirectoryWithOutputDirectory() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
-                .addOutput("/foo")
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .setOutput("/foo")
                 .build();
 
         assertEquals(2, transformations.size());
@@ -162,10 +156,10 @@
 
     @Test
     public void shouldCreateFromTemplateDirectoryWithInclude() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_DIRECTORY_NAME)
                 .addInclude("*.properties")
-                .setStdOut()
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(1, transformations.size());
@@ -174,18 +168,60 @@
 
     @Test
     public void shouldCreateFromTemplateDirectoryWithExclude() {
-        final TemplateTransformations transformations = builder()
-                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_DIRECTORY_NAME)
                 .addExclude("*.ftl")
-                .setStdOut()
+                .setCallerSuppliedWriter(stdoutWriter())
                 .build();
 
         assertEquals(1, transformations.size());
         assertEquals("application.properties", transformations.get(0).getTemplateSource().getName());
     }
 
+    // === Template URL ===============================================
+
+    @Test
+    public void shouldCreateFromTemplateUrl() {
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_TEMPLATE_URL)
+                .setCallerSuppliedWriter(stdoutWriter())
+                .build();
+
+        final TemplateSource templateSource = transformations.get(0).getTemplateSource();
+
+        assertEquals(1, transformations.size());
+        assertEquals(ANY_TEMPLATE_URL, templateSource.getName());
+        assertEquals(Origin.TEMPLATE_CODE, templateSource.getOrigin());
+        assertNull(templateSource.getPath());
+        assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
+        assertTrue(templateSource.getCode().contains("<#ftl"));
+    }
+
+    // === Template ENV Variable =============================================
+
+    @Test
+    public void shouldCreateFromTemplateEnvironmentVariable() {
+        final List<TemplateTransformation> transformations = builder()
+                .setTemplateSource(ANY_ENV_URI)
+                .setCallerSuppliedWriter(stdoutWriter())
+                .build();
+
+        final TemplateSource templateSource = transformations.get(0).getTemplateSource();
+
+        assertEquals(1, transformations.size());
+        assertEquals("JAVA_HOME", templateSource.getName());
+        assertEquals(Origin.TEMPLATE_CODE, templateSource.getOrigin());
+        assertNull(templateSource.getPath());
+        assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
+        assertFalse(templateSource.getCode().isEmpty());
+    }
+
     private TemplateTransformationsBuilder builder() {
         return TemplateTransformationsBuilder
                 .builder();
     }
+
+    private Writer stdoutWriter() {
+        return new NonClosableWriterWrapper(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)));
+    }
 }
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/MapFlattenerTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/MapFlattenerTest.java
deleted file mode 100644
index 92bd90c..0000000
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/util/MapFlattenerTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.freemarker.generator.base.util.MapFlattener;
-import org.junit.Test;
-
-import java.util.Collections;
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-public class MapFlattenerTest {
-
-    @Test
-    public void shouldHandleEmptyMap() {
-        final Map<String, Object> result = MapFlattener.flatten(Collections.emptyMap());
-
-        assertTrue(result.isEmpty());
-    }
-
-    @Test
-    public void shouldPreserveFlatMap() {
-        final Map<String, Object> result = MapFlattener.flatten(Collections.singletonMap("key", "value"));
-
-        assertEquals("value", result.get("key"));
-    }
-}
diff --git a/freemarker-generator-cli/CHANGELOG.md b/freemarker-generator-cli/CHANGELOG.md
index c6abcd7..6565367 100644
--- a/freemarker-generator-cli/CHANGELOG.md
+++ b/freemarker-generator-cli/CHANGELOG.md
@@ -19,6 +19,7 @@
 * [FREEMARKER-129] Migrate `freemarker-cli` into `freemarker-generator` project (see [https://github.com/sgoeschl/freemarker-cli](https://github.com/sgoeschl/freemarker-cli))
 
 ### Changed
+* [FREEMARKER-161] Allow multiple transformations on the CLI
 * [FREEMARKER-155] Migrate the FTL code to terser dotter form 
 * [FREEMARKER-153] Packaged templates are now prefixed with `freemarker-generator`, e.g. `freemarker-generator/info.ftl`
 * [FREEMARKER-153] Renamed `--basedir` command line option to `--template-dir`
@@ -61,6 +62,7 @@
 [FREEMARKER-151]: https://issues.apache.org/jira/browse/FREEMARKER-151
 [FREEMARKER-153]: https://issues.apache.org/jira/browse/FREEMARKER-153
 [FREEMARKER-155]: https://issues.apache.org/jira/browse/FREEMARKER-155
+[FREEMARKER-161]: https://issues.apache.org/jira/browse/FREEMARKER-161
 [FREEMARKER-163]: https://issues.apache.org/jira/browse/FREEMARKER-163
 [FREEMARKER-164]: https://issues.apache.org/jira/browse/FREEMARKER-164
 [FREEMARKER-168]: https://issues.apache.org/jira/browse/FREEMARKER-168
\ No newline at end of file
diff --git a/freemarker-generator-cli/LICENSE b/freemarker-generator-cli/LICENSE
index caca01c..f6a36bd 100644
--- a/freemarker-generator-cli/LICENSE
+++ b/freemarker-generator-cli/LICENSE
@@ -335,6 +335,12 @@
 
 ==============================================================================
 
+Binary distributions of this product bundles java-faker which
+is available under Apache License Version 2.0.
+See licencens/LICENSE_ASL-2.0.txt for more information ...
+
+==============================================================================
+
 Binary distributions of this product bundles poi which
 is available under Apache License Version 2.0.
 See licencens/LICENSE_ASL-2.0.txt for more information ...
diff --git a/freemarker-generator-cli/pom.xml b/freemarker-generator-cli/pom.xml
index da3494b..c21d5d9 100644
--- a/freemarker-generator-cli/pom.xml
+++ b/freemarker-generator-cli/pom.xml
@@ -161,10 +161,12 @@
                 <configuration>
                     <excludes>
                         <exclude>README.md</exclude>
-                        <exclude>src/site/markdown/**/*.md</exclude>
+                        <exclude>CHANGELOG.md</exclude>
                         <exclude>.travis.yml</exclude>
+                        <exclude>src/app/examples/data/*/**</exclude>
+                        <exclude>src/app/scripts/run-examples.bat</exclude>
                         <exclude>src/main/resources/patterns/*</exclude>
-                        <exclude>examples/data/*/**</exclude>
+                        <exclude>src/site/markdown/**/*.md</exclude>
                         <exclude>src/test/data/encoding/utf8.txt</exclude>
                         <exclude>src/test/data/json/*/**</exclude>
                         <exclude>src/test/data/yaml/environments.yaml</exclude>
@@ -179,7 +181,7 @@
         <dependency>
             <groupId>info.picocli</groupId>
             <artifactId>picocli</artifactId>
-            <version>4.1.4</version>
+            <version>4.6.1</version>
         </dependency>
         <!-- Apache FreeMarker Generator Tools -->
         <dependency>
diff --git a/freemarker-generator-cli/src/app/examples/templates/demo.ftl b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
index 8ec3ac4..879fb3f 100644
--- a/freemarker-generator-cli/src/app/examples/templates/demo.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
@@ -127,7 +127,10 @@
 
 15) Printing Special Characters
 ---------------------------------------------------------------------------
+Windows 1252 Characters: €¥§ÆÇæ®
 German Special Characters: äöüßÄÖÜ
+Cyrillic Characters: Кириллица
+Chinese Characters: 你好吗
 
 16) Locale-specific output
 ---------------------------------------------------------------------------
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 357a3ae..1976644 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
@@ -14,8 +14,9 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<#assign faker = tools.javafaker.faker>
-<#assign nrOfRecords = tools.system.getString("NR_OF_RECORDS","10")>
+<#-- Get a localized JavaFaker instance -->
+<#assign faker = tools.javafaker.getFaker("de_DE")>
+<#assign nrOfRecords = tools.system.getString("NR_OF_RECORDS","100")>
 <#assign days = tools.javafaker.timeUnits["DAYS"]>
 <#assign csvTargetFormat = tools.csv.formats["DEFAULT"].withFirstRecordAsHeader()>
 <#assign csvPrinter = tools.csv.printer(csvTargetFormat)>
@@ -24,15 +25,18 @@
     <#if csvTargetFormat.getSkipHeaderRecord()>
         ${csvPrinter.printRecord(csvHeaders)}<#t>
     </#if>
-    <#list 1..nrOfRecords?number as i>
-        <#assign id = tools.uuid.randomUUID()>
+    <#list 0..nrOfRecords?number as i>
+        <#-- Generate a reproducable id to allow re-importing of test data -->
+        <#assign id = tools.uuid.namedUUID("trxid-" + i?string)>
         <#assign customerId = faker.bothify("?#######")>
         <#assign firstName = faker.name().firstName()>
         <#assign lastName = faker.name().lastName()>
-        <#assign email = firstName + "." + lastName + "@gmail.com">
+        <#assign email = firstName + "." + lastName + "@server.invalid">
+        <#-- JavaFakers IBAN generation is really slow -->
         <#assign iban = faker.finance().iban("DE")>
-
+        <#-- Distribute the creation date up to 10 years in the past -->
         <#assign createAt = faker.date().past(3650, days)>
+        <#-- Use a CSV Printer to properly escape the output -->
         ${csvPrinter.printRecord(
             id,
             customerId,
diff --git a/freemarker-generator-cli/src/app/scripts/run-examples.bat b/freemarker-generator-cli/src/app/scripts/run-examples.bat
index 4bb5e5e..6ef6f9a 100644
--- a/freemarker-generator-cli/src/app/scripts/run-examples.bat
+++ b/freemarker-generator-cli/src/app/scripts/run-examples.bat
@@ -130,7 +130,7 @@
 REM Java Faker
 REM =========================================================================
 echo "examples/templates/javafaker/csv/testdata.ftl"
-%FREEMARKER_CMD% -t examples/templates/javafaker/csv/testdata.ftl > target/out/testdata.csv
+%FREEMARKER_CMD% -DNR_OF_RECORDS=10 -t examples/templates/javafaker/csv/testdata.ftl > target/out/testdata.csv
 
 REM =========================================================================
 REM JSON
diff --git a/freemarker-generator-cli/src/app/scripts/run-examples.sh b/freemarker-generator-cli/src/app/scripts/run-examples.sh
index 316a098..034774f 100755
--- a/freemarker-generator-cli/src/app/scripts/run-examples.sh
+++ b/freemarker-generator-cli/src/app/scripts/run-examples.sh
@@ -151,7 +151,7 @@
 #############################################################################
 
 echo "examples/templates/javafaker/csv/testdata.ftl"
-$FREEMARKER_CMD -t examples/templates/javafaker/csv/testdata.ftl > target/out/testdata.csv || { echo >&2 "Test failed.  Aborting."; exit 1; }
+$FREEMARKER_CMD -DNR_OF_RECORDS=10 -t examples/templates/javafaker/csv/testdata.ftl > target/out/testdata.csv || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
 # JSON
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 1a34880..c165be3 100644
--- a/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
+++ b/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
@@ -37,24 +37,20 @@
 - ${key}<#lt>
 </#list>
 
-FreeMarker Generator Tools
-------------------------------------------------------------------------------
-<#list tools?keys?sort as name>
-- ${name?right_pad(20)} : ${tools[name]}
-</#list>
-
-<#if dataSources?has_content>
 FreeMarker Generator DataSources
 ------------------------------------------------------------------------------
+<#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
+[#${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>
+<#else>
+No data sources found ...
 </#if>
 
-<#if tools.system.parameters?has_content>
 FreeMarker Generator Parameters
 ------------------------------------------------------------------------------
+<#if tools.system.parameters?has_content>
 <#list tools.system.parameters as key,value>
 <#if value?is_hash>
 - ${key} ==> { <#list value as name,value>${name}=${value} </#list>}
@@ -62,4 +58,12 @@
 - ${key} ==> ${value}
 </#if>
 </#list>
+<#else>
+No parameters found ...
 </#if>
+
+FreeMarker Generator Tools
+------------------------------------------------------------------------------
+<#list tools?keys?sort as name>
+- ${name?right_pad(20)} : ${tools[name]}
+</#list>
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 4869909..5158668 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
@@ -20,53 +20,47 @@
 import org.apache.freemarker.generator.base.FreeMarkerConstants.SystemProperties;
 import org.apache.freemarker.generator.base.parameter.ParameterModelSupplier;
 import org.apache.freemarker.generator.base.util.ClosableUtils;
-import org.apache.freemarker.generator.base.util.ListUtils;
 import org.apache.freemarker.generator.cli.config.Settings;
+import org.apache.freemarker.generator.cli.config.Suppliers;
 import org.apache.freemarker.generator.cli.picocli.GitVersionProvider;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
 import org.apache.freemarker.generator.cli.task.FreeMarkerTask;
 import picocli.CommandLine;
 import picocli.CommandLine.ArgGroup;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Model.CommandSpec;
 import picocli.CommandLine.Option;
-import picocli.CommandLine.ParameterException;
 import picocli.CommandLine.Parameters;
 import picocli.CommandLine.Spec;
 
-import java.io.BufferedWriter;
 import java.io.File;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
 import java.io.Writer;
-import java.util.Collection;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Properties;
 import java.util.concurrent.Callable;
-import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import java.util.stream.Stream;
 
+import static java.util.Collections.emptyList;
 import static java.util.Objects.requireNonNull;
 import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
-import static org.apache.freemarker.generator.cli.config.Suppliers.propertiesSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.templateDirectorySupplier;
 
 @Command(description = "Apache FreeMarker Generator", name = "freemarker-generator", mixinStandardHelpOptions = true, versionProvider = GitVersionProvider.class)
 public class Main implements Callable<Integer> {
 
-    @ArgGroup(multiplicity = "1")
-    TemplateSourceOptions templateSourceOptions;
+    @ArgGroup(exclusive = false, multiplicity = "1..*")
+    List<OutputGeneratorDefinition> outputGeneratorDefinitions;
 
-    public static final class TemplateSourceOptions {
-        @Option(names = { "-t", "--template" }, description = "templates to process")
-        public List<String> templates;
+    @Option(names = { "--data-source-include" }, description = "data source include pattern")
+    public String dataSourceIncludePattern;
 
-        @Option(names = { "-i", "--interactive" }, description = "interactive template to process")
-        public String interactiveTemplate;
-    }
+    @Option(names = { "--data-source-exclude" }, description = "data source exclude pattern")
+    public String dataSourceExcludePattern;
+
+    @Option(names = { "--shared-data-model" }, description = "shared data models used for rendering")
+    public List<String> sharedDataModels;
 
     @Option(names = { "-D", "--system-property" }, description = "set system property")
     Properties systemProperties;
@@ -77,27 +71,12 @@
     @Option(names = { "-l", "--locale" }, description = "locale being used for the output, e.g. 'en_US'")
     String locale;
 
-    @Option(names = { "-m", "--data-model" }, description = "data model used for rendering")
-    List<String> dataModels;
-
-    @Option(names = { "-o", "--output" }, description = "output files or directories")
-    List<String> outputs;
-
     @Option(names = { "-P", "--param" }, description = "set parameter")
     Map<String, String> parameters;
 
-    @Option(names = { "-s", "--data-source" }, description = "data source used for rendering")
-    List<String> dataSources;
-
     @Option(names = { "--config" }, description = "FreeMarker Generator configuration file")
     String configFile;
 
-    @Option(names = { "--data-source-include" }, description = "file include pattern for data sources")
-    String dataSourceIncludePattern;
-
-    @Option(names = { "--data-source-exclude" }, description = "file exclude pattern for data sources")
-    String dataSourceExcludePattern;
-
     @Option(names = { "--output-encoding" }, description = "encoding of output, e.g. UTF-8", defaultValue = "UTF-8")
     String outputEncoding;
 
@@ -107,32 +86,32 @@
     @Option(names = { "--template-dir" }, description = "additional template directory")
     String templateDir;
 
+    @Option(names = { "--template-encoding" }, description = "template encoding", defaultValue = "UTF-8")
+    String templateEncoding;
+
     @Option(names = { "--times" }, defaultValue = "1", description = "re-run X times for profiling")
     int times;
 
-    @Parameters(description = "data source files and/or directories")
-    List<String> sources;
+    @Parameters(description = "shared data source files and/or directories")
+    List<String> sharedDataSources;
 
     /** User-supplied command line parameters */
     final String[] args;
 
     /** User-supplied writer (used mainly for unit testing) */
-    Writer userSuppliedWriter;
+    final Writer callerSuppliedWriter;
 
     /** Injected by Picocli */
     @Spec private CommandSpec spec;
 
-    Main() {
-        this.args = new String[0];
+    Main(String[] args) {
+        this.args = requireNonNull(args);
+        this.callerSuppliedWriter = null;
     }
 
-    private Main(String[] args) {
+    private Main(String[] args, Writer callerSuppliedWriter) {
         this.args = requireNonNull(args);
-    }
-
-    private Main(String[] args, Writer userSuppliedWriter) {
-        this.args = requireNonNull(args);
-        this.userSuppliedWriter = requireNonNull(userSuppliedWriter);
+        this.callerSuppliedWriter = requireNonNull(callerSuppliedWriter);
     }
 
     public static void main(String[] args) {
@@ -171,77 +150,51 @@
         final String currentConfigFile = isNotEmpty(configFile) ? configFile : getDefaultConfigFileName();
         final Properties configuration = loadFreeMarkerCliConfiguration(currentConfigFile);
         final List<File> templateDirectories = getTemplateDirectories(templateDir);
-        final Settings settings = settings(configuration, templateDirectories);
+        final Settings settings = settings(configuration, templateDirectories, outputGeneratorDefinitions);
 
         try {
-            final FreeMarkerTask freeMarkerTask = new FreeMarkerTask(settings);
+            final FreeMarkerTask freeMarkerTask = new FreeMarkerTask(
+                    Suppliers.configurationSupplier(settings),
+                    Suppliers.outputGeneratorsSupplier(settings),
+                    Suppliers.sharedDataModelSupplier(settings),
+                    Suppliers.sharedDataSourcesSupplier(settings),
+                    settings::getUserParameters
+            );
             return freeMarkerTask.call();
         } finally {
-            if (settings.hasOutputs()) {
-                ClosableUtils.closeQuietly(settings.getWriter());
-            }
+            ClosableUtils.closeQuietly(settings.getCallerSuppliedWriter());
         }
     }
 
     void validate() {
-        // "-d" or "--data-source" parameter shall not contain wildcard characters
-        if (dataSources != null) {
-            for (String source : dataSources) {
-                if (isFileSource(source) && (source.contains("*") || source.contains("?"))) {
-                    throw new ParameterException(spec.commandLine(), "No wildcards supported for data source: " + source);
-                }
-            }
-        }
-
-        // does the templates match the expected outputs?!
-        // -) no output means it goes to stdout
-        // -) for each template there should be an output
-        final List<String> templates = templateSourceOptions.templates;
-        if (templates != null && templates.size() > 1) {
-            if (outputs != null && outputs.size() != templates.size()) {
-                throw new ParameterException(spec.commandLine(), "Template output does not match specified templates");
-            }
-        }
+        outputGeneratorDefinitions.forEach(t -> t.validate(spec.commandLine()));
     }
 
-    private Settings settings(Properties configuration, List<File> templateDirectories) {
+    private Settings settings(Properties configuration, List<File> templateDirectories, List<OutputGeneratorDefinition> outputGeneratorDefinitions) {
         final ParameterModelSupplier parameterModelSupplier = new ParameterModelSupplier(parameters);
 
         return Settings.builder()
                 .isReadFromStdin(readFromStdin)
-                .setArgs(args)
+                .setCommandLineArgs(args)
                 .setConfiguration(configuration)
-                .setDataModels(dataModels)
-                .setDataSources(getCombinedDataSources())
-                .setDataSourceIncludePattern(dataSourceIncludePattern)
-                .setDataSourceExcludePattern(dataSourceExcludePattern)
+                .setTemplateDirectories(templateDirectories)
+                .setTemplateEncoding(templateEncoding)
+                .setOutputGeneratorDefinitions(outputGeneratorDefinitions)
+                .setSharedDataSources(getSharedDataSources())
+                .setSharedDataModels(sharedDataModels)
+                .setSourceIncludePattern(dataSourceIncludePattern)
+                .setSourceExcludePattern(dataSourceExcludePattern)
                 .setInputEncoding(inputEncoding)
-                .setInteractiveTemplate(templateSourceOptions.interactiveTemplate)
                 .setLocale(locale)
                 .setOutputEncoding(outputEncoding)
-                .setOutputs(outputs)
                 .setParameters(parameterModelSupplier.get())
                 .setSystemProperties(systemProperties != null ? systemProperties : new Properties())
                 .setTemplateDirectories(templateDirectories)
-                .setTemplateNames(templateSourceOptions.templates)
-                .setWriter(writer(outputs, outputEncoding))
+                .setCallerSuppliedWriter(callerSuppliedWriter)
+                .setVerbose(false)
                 .build();
     }
 
-    private Writer writer(List<String> outputFiles, String outputEncoding) {
-        try {
-            if (userSuppliedWriter != null) {
-                return userSuppliedWriter;
-            } else if (ListUtils.isNullOrEmpty(outputFiles)) {
-                return new BufferedWriter(new OutputStreamWriter(System.out, outputEncoding));
-            } else {
-                return null;
-            }
-        } catch (IOException e) {
-            throw new RuntimeException("Unable to create writer", e);
-        }
-    }
-
     private void updateSystemProperties() {
         if (systemProperties != null && !systemProperties.isEmpty()) {
             System.getProperties().putAll(systemProperties);
@@ -254,15 +207,12 @@
      *
      * @return List of data sources
      */
-    private List<String> getCombinedDataSources() {
-        return Stream.of(dataSources, sources)
-                .filter(Objects::nonNull)
-                .flatMap(Collection::stream)
-                .collect(Collectors.toList());
+    private List<String> getSharedDataSources() {
+        return sharedDataSources != null ? new ArrayList<>(sharedDataSources) : emptyList();
     }
 
     private static List<File> getTemplateDirectories(String additionalTemplateDir) {
-        return templateDirectorySupplier(additionalTemplateDir).get();
+        return Suppliers.templateDirectorySupplier(additionalTemplateDir).get();
     }
 
     /**
@@ -281,21 +231,11 @@
             return new Properties();
         }
 
-        final Properties properties = propertiesSupplier(fileName).get();
+        final Properties properties = Suppliers.propertiesSupplier(fileName).get();
         if (properties != null) {
             return properties;
         } else {
             return new Properties();
         }
     }
-
-    private static boolean isFileSource(String source) {
-        if (source.contains("file://")) {
-            return true;
-        } else if (source.contains("://")) {
-            return false;
-        } else {
-            return true;
-        }
-    }
 }
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 8f45e90..60c7743 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
@@ -19,17 +19,16 @@
 import freemarker.cache.TemplateLoader;
 import freemarker.template.Configuration;
 import freemarker.template.Version;
+import org.apache.freemarker.generator.base.util.PropertiesTransformer;
 import org.apache.freemarker.generator.cli.model.GeneratorObjectWrapper;
 
 import java.util.Properties;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import static freemarker.template.Configuration.VERSION_2_3_29;
 import static java.util.Objects.requireNonNull;
-import static java.util.stream.Stream.of;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Configuration.SETTING_PREFIX;
-import static org.apache.freemarker.generator.base.util.PropertiesTransformer.filterKeyPrefix;
-import static org.apache.freemarker.generator.base.util.PropertiesTransformer.removeKeyPrefix;
 
 /**
  * Supply a FreeMarker configuration.
@@ -40,10 +39,12 @@
 
     private final Settings settings;
     private final Supplier<TemplateLoader> templateLoader;
+    private final ToolsSupplier toolsSupplier;
 
-    public ConfigurationSupplier(Settings settings, Supplier<TemplateLoader> templateLoader) {
+    public ConfigurationSupplier(Settings settings, Supplier<TemplateLoader> templateLoader, ToolsSupplier toolsSupplier) {
         this.settings = requireNonNull(settings);
         this.templateLoader = requireNonNull(templateLoader);
+        this.toolsSupplier = requireNonNull(toolsSupplier);
     }
 
     @Override
@@ -63,6 +64,9 @@
             configuration.setOutputEncoding(settings.getOutputEncoding().name());
             configuration.setTemplateLoader(templateLoader.get());
 
+            // instantiate the tools as shared FreeMarker variables
+            configuration.setSharedVariables(toolsSupplier.get());
+
             return configuration;
         } catch (Exception e) {
             throw new RuntimeException("Failed to create FreeMarker configuration", e);
@@ -76,9 +80,9 @@
      * @return FreeMarker configuration settings
      */
     private Properties freeMarkerConfigurationSettings() {
-        return of(settings.getConfiguration())
-                .map(p -> filterKeyPrefix(p, SETTING_PREFIX))
-                .map(p -> removeKeyPrefix(p, SETTING_PREFIX))
+        return Stream.of(settings.getConfiguration())
+                .map(p -> PropertiesTransformer.filterKeyPrefix(p, SETTING_PREFIX))
+                .map(p -> PropertiesTransformer.removeKeyPrefix(p, SETTING_PREFIX))
                 .findFirst().get();
     }
 }
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
new file mode 100644
index 0000000..085ad60
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/OutputGeneratorsSupplier.java
@@ -0,0 +1,141 @@
+/*
+ * 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.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.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.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.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;
+
+public class OutputGeneratorsSupplier implements Supplier<List<OutputGenerator>> {
+
+    private final Settings settings;
+
+    public OutputGeneratorsSupplier(Settings settings) {
+        this.settings = settings;
+    }
+
+    @Override
+    public List<OutputGenerator> get() {
+        return settings.getOutputGeneratorDefinitions().stream()
+                .map(this::outputGenerator)
+                .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);
+        } 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)
+            );
+            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);
+    }
+}
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 427fd93..c45a479 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
@@ -17,9 +17,9 @@
 package org.apache.freemarker.generator.cli.config;
 
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
-import org.apache.freemarker.generator.base.util.ListUtils;
 import org.apache.freemarker.generator.base.util.LocaleUtils;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
 
 import java.io.File;
 import java.io.Writer;
@@ -38,7 +38,6 @@
 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;
-import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 
 /**
  * Capture all the settings required for rendering a FreeMarker template.
@@ -49,22 +48,28 @@
     private final Properties configuration;
 
     /** Command line arguments */
-    private final List<String> args;
+    private final List<String> commandLineArgs;
 
     /** List of FreeMarker template directories to be passed to FreeMarker <code>TemplateLoader</code> */
     private final List<File> templateDirectories;
 
-    /** List of template to be loaded and rendered */
-    private final List<String> templates;
+    /** Encoding to load templates */
+    private final Charset templateEncoding;
 
-    /** Template provided by the user interactively */
-    private final String interactiveTemplate;
+    /** User-provided output generators (transformations) */
+    private final List<OutputGeneratorDefinition> outputGeneratorDefinitions;
 
-    /** Optional include pattern for recursive directly search of template files */
-    private final String templateFileIncludePattern;
+    /** List of additional shared data sources provided by positional parameters */
+    private final List<String> sharedDataSources;
 
-    /** Optional exclude pattern for recursive directly search of data source files */
-    private final String templateFileExcludePattern;
+    /** List of additional shared data models */
+    private final List<String> sharedDataModels;
+
+    /** Include pattern for sources */
+    private final String sourceIncludePattern;
+
+    /** Exclude pattern for sources */
+    private final String sourceExcludePattern;
 
     /** Encoding of input files */
     private final Charset inputEncoding;
@@ -75,81 +80,57 @@
     /** Enable verbose mode (currently not used) **/
     private final boolean verbose;
 
-    /** Optional output files or directories if not written to stdout */
-    private final List<String> outputs;
-
-    /** Optional include pattern for recursive directly search of data source files */
-    private final String dataSourceIncludePattern;
-
-    /** Optional exclude pattern for recursive directly search of data source files */
-    private final String dataSourceExcludePattern;
-
     /** The locale used for rendering the template */
     private final Locale locale;
 
     /** Read from stdin? */
     private final boolean isReadFromStdin;
 
-    /** User-supplied list of data sources or directories */
-    private final List<String> dataSources;
-
-    /** User-supplied list of data sources directly exposed in the data model */
-    private final List<String> dataModels;
-
     /** User-supplied parameters */
     private final Map<String, Object> userParameters;
 
     /** User-supplied system properties */
     private final Properties userSystemProperties;
 
-    /** The writer used for rendering templates, e.g. stdout or a file writer */
-    private final Writer writer;
+    /** Caller-supplied writer */
+    private final Writer callerSuppliedWriter;
 
     private Settings(
             Properties configuration,
-            List<String> args,
+            List<String> commandLineArgs,
             List<File> templateDirectories,
-            List<String> templates,
-            String interactiveTemplate,
-            String templateFileIncludePattern,
-            String templateFileExcludePattern,
+            Charset templateEncoding,
+            List<OutputGeneratorDefinition> outputGeneratorDefinitions,
+            List<String> sharedDataSources,
+            List<String> sharedDataModels,
+            String sourceIncludePattern,
+            String sourceExcludePattern,
             Charset inputEncoding,
             Charset outputEncoding,
             boolean verbose,
-            List<String> outputs,
-            String dataSourceIncludePattern,
-            String dataSourceExcludePattern,
             Locale locale,
             boolean isReadFromStdin,
-            List<String> dataSources,
-            List<String> dataModels,
             Map<String, Object> userParameters,
             Properties userSystemProperties,
-            Writer writer) {
-        if ((templates == null || templates.isEmpty()) && isEmpty(interactiveTemplate)) {
-            throw new IllegalArgumentException("Either 'template' or 'interactiveTemplate' must be provided");
-        }
+            Writer callerSuppliedWriter) {
 
-        this.args = requireNonNull(args);
+        this.commandLineArgs = requireNonNull(commandLineArgs);
         this.templateDirectories = requireNonNull(templateDirectories);
-        this.templates = requireNonNull(templates);
-        this.interactiveTemplate = interactiveTemplate;
-        this.templateFileIncludePattern = templateFileIncludePattern;
-        this.templateFileExcludePattern = templateFileExcludePattern;
+        this.templateEncoding = requireNonNull(templateEncoding);
+        this.outputGeneratorDefinitions = requireNonNull(outputGeneratorDefinitions);
+        this.sharedDataSources = requireNonNull(sharedDataSources);
+        this.sharedDataModels = requireNonNull(sharedDataModels);
+        this.sourceIncludePattern = sourceIncludePattern;
+        this.sourceExcludePattern = sourceExcludePattern;
         this.inputEncoding = inputEncoding;
         this.outputEncoding = outputEncoding;
         this.verbose = verbose;
-        this.outputs = outputs;
-        this.dataSourceIncludePattern = dataSourceIncludePattern;
-        this.dataSourceExcludePattern = dataSourceExcludePattern;
         this.locale = requireNonNull(locale);
         this.isReadFromStdin = isReadFromStdin;
-        this.dataSources = requireNonNull(dataSources);
-        this.dataModels = requireNonNull(dataModels);
         this.userParameters = requireNonNull(userParameters);
         this.userSystemProperties = requireNonNull(userSystemProperties);
         this.configuration = requireNonNull(configuration);
-        this.writer = writer != null ? new NonClosableWriterWrapper(writer) : null;
+        this.callerSuppliedWriter = callerSuppliedWriter != null ? new NonClosableWriterWrapper(callerSuppliedWriter) : null;
     }
 
     public static SettingsBuilder builder() {
@@ -160,28 +141,32 @@
         return configuration;
     }
 
-    public List<String> getArgs() {
-        return args;
+    public List<String> getCommandLineArgs() {
+        return commandLineArgs;
     }
 
     public List<File> getTemplateDirectories() {
         return templateDirectories;
     }
 
-    public List<String> getTemplates() {
-        return templates;
+    public List<OutputGeneratorDefinition> getOutputGeneratorDefinitions() {
+        return outputGeneratorDefinitions;
     }
 
-    public String getInteractiveTemplate() {
-        return interactiveTemplate;
+    public List<String> getSharedDataSources() {
+        return sharedDataSources;
     }
 
-    public String getTemplateFileIncludePattern() {
-        return templateFileIncludePattern;
+    public List<String> getSharedDataModels() {
+        return sharedDataModels;
     }
 
-    public String getTemplateFileExcludePattern() {
-        return templateFileExcludePattern;
+    public String getSourceIncludePattern() {
+        return sourceIncludePattern;
+    }
+
+    public String getSourceExcludePattern() {
+        return sourceExcludePattern;
     }
 
     public Charset getInputEncoding() {
@@ -193,25 +178,13 @@
     }
 
     public Charset getTemplateEncoding() {
-        return UTF_8;
+        return templateEncoding;
     }
 
     public boolean isVerbose() {
         return verbose;
     }
 
-    public List<String> getOutputs() {
-        return outputs;
-    }
-
-    public String getDataSourceIncludePattern() {
-        return dataSourceIncludePattern;
-    }
-
-    public String getDataSourceExcludePattern() {
-        return dataSourceExcludePattern;
-    }
-
     public Locale getLocale() {
         return locale;
     }
@@ -220,14 +193,6 @@
         return isReadFromStdin;
     }
 
-    public List<String> getDataSources() {
-        return dataSources;
-    }
-
-    public List<String> getDataModels() {
-        return dataModels;
-    }
-
     public Map<String, Object> getUserParameters() {
         return userParameters;
     }
@@ -236,12 +201,8 @@
         return userSystemProperties;
     }
 
-    public boolean hasOutputs() {
-        return ListUtils.isNotEmpty(outputs);
-    }
-
-    public Writer getWriter() {
-        return writer;
+    public Writer getCallerSuppliedWriter() {
+        return callerSuppliedWriter;
     }
 
     /**
@@ -252,113 +213,114 @@
      */
     public Map<String, Object> toMap() {
         final Map<String, Object> result = new HashMap<>();
-        result.put(Model.FREEMARKER_CLI_ARGS, getArgs());
+        result.put(Model.FREEMARKER_CLI_ARGS, getCommandLineArgs());
         result.put(Model.FREEMARKER_LOCALE, getLocale());
         result.put(Model.FREEMARKER_TEMPLATE_DIRECTORIES, getTemplateDirectories());
         result.put(Model.FREEMARKER_USER_PARAMETERS, getUserParameters());
         result.put(Model.FREEMARKER_USER_SYSTEM_PROPERTIES, getUserSystemProperties());
-        result.put(Model.FREEMARKER_WRITER, getWriter());
+        result.put(Model.FREEMARKER_WRITER, getCallerSuppliedWriter());
         return result;
     }
 
-    public boolean isInteractiveTemplate() {
-        return interactiveTemplate != null;
-    }
-
     @Override
     public String toString() {
         return "Settings{" +
                 "configuration=" + configuration +
-                ", args=" + args +
+                ", commandLineArgs=" + commandLineArgs +
                 ", templateDirectories=" + templateDirectories +
-                ", templateName=s'" + templates + '\'' +
-                ", interactiveTemplate='" + interactiveTemplate + '\'' +
-                ", templateFileIncludePattern='" + templateFileIncludePattern + '\'' +
-                ", templateFileExcludePattern='" + templateFileExcludePattern + '\'' +
+                ", outputGeneratorDefinitions=" + outputGeneratorDefinitions +
+                ", sharedDataSources=" + sharedDataSources +
+                ", sharedDataModels=" + sharedDataModels +
+                ", sourceIncludePattern='" + sourceIncludePattern + '\'' +
+                ", sourceExcludePattern='" + sourceExcludePattern + '\'' +
                 ", inputEncoding=" + inputEncoding +
                 ", outputEncoding=" + outputEncoding +
                 ", verbose=" + verbose +
-                ", outputs=" + outputs +
-                ", include='" + dataSourceIncludePattern + '\'' +
-                ", exclude='" + dataSourceExcludePattern + '\'' +
                 ", locale=" + locale +
                 ", isReadFromStdin=" + isReadFromStdin +
-                ", dataSources=" + dataSources +
                 ", userParameters=" + userParameters +
                 ", userSystemProperties=" + userSystemProperties +
+                ", callerSuppliedWriter=" + callerSuppliedWriter +
+                ", templateEncoding=" + templateEncoding +
+                ", readFromStdin=" + isReadFromStdin() +
                 '}';
     }
 
     public static class SettingsBuilder {
-        private List<String> args;
+        private List<String> commandLineArgs;
         private List<File> templateDirectories;
-        private List<String> templateNames;
-        private String interactiveTemplate;
-        private String templateFileIncludePattern;
-        private String templateFileExcludePattern;
+        private String templateEncoding;
+        private List<OutputGeneratorDefinition> outputGeneratorDefinitions;
+        private List<String> sharedDataSources;
+        private List<String> sharedDataModels;
+        private String sourceIncludePattern;
+        private String sourceExcludePattern;
         private String inputEncoding;
         private String outputEncoding;
         private boolean verbose;
-        private List<String> outputs;
-        private String dataSourceIncludePattern;
-        private String dataSourceExcludePattern;
         private String locale;
         private boolean isReadFromStdin;
-        private List<String> dataSources;
-        private List<String> dataModels;
         private Map<String, Object> parameters;
         private Properties systemProperties;
         private Properties configuration;
-        private Writer writer;
+        private Writer callerSuppliedWriter;
 
         private SettingsBuilder() {
-            this.args = emptyList();
+            this.commandLineArgs = emptyList();
+            this.templateDirectories = emptyList();
+            this.templateEncoding = UTF_8.name();
+            this.outputGeneratorDefinitions = emptyList();
+            this.sharedDataSources = emptyList();
+            this.sharedDataModels = emptyList();
+            this.sourceIncludePattern = null;
+            this.sourceExcludePattern = null;
             this.configuration = new Properties();
             this.locale = DEFAULT_LOCALE.toString();
             this.parameters = new HashMap<>();
             this.systemProperties = new Properties();
             this.setInputEncoding(DEFAULT_CHARSET.name());
             this.setOutputEncoding(DEFAULT_CHARSET.name());
-            this.templateNames = new ArrayList<>();
-            this.dataSources = emptyList();
-            this.dataModels = emptyList();
-            this.templateDirectories = emptyList();
         }
 
-        public SettingsBuilder setArgs(String[] args) {
-            if (args == null) {
-                this.args = emptyList();
-            } else {
-                this.args = Arrays.asList(args);
-            }
-
+        public SettingsBuilder setCommandLineArgs(String[] args) {
+            this.commandLineArgs = args != null ? Arrays.asList(args) : emptyList();
             return this;
         }
 
         public SettingsBuilder setTemplateDirectories(List<File> list) {
-            this.templateDirectories = list;
+            this.templateDirectories = list != null ? new ArrayList<>(list) : emptyList();
             return this;
         }
 
-        public SettingsBuilder setTemplateNames(List<String> templateNames) {
-            if (templateNames != null) {
-                this.templateNames = templateNames;
+        public SettingsBuilder setTemplateEncoding(String templateEncoding) {
+            if (templateEncoding != null) {
+                this.templateEncoding = templateEncoding;
             }
             return this;
         }
 
-        public SettingsBuilder setInteractiveTemplate(String interactiveTemplate) {
-            this.interactiveTemplate = interactiveTemplate;
+        public SettingsBuilder setOutputGeneratorDefinitions(List<OutputGeneratorDefinition> outputGeneratorDefinitions) {
+            this.outputGeneratorDefinitions = outputGeneratorDefinitions != null ? new ArrayList<>(outputGeneratorDefinitions) : emptyList();
             return this;
         }
 
-        public SettingsBuilder setTemplateFileIncludePattern(String templateFileIncludePattern) {
-            this.templateFileIncludePattern = templateFileIncludePattern;
+        public SettingsBuilder setSharedDataSources(List<String> sharedDataSources) {
+            this.sharedDataSources = sharedDataSources != null ? new ArrayList<>(sharedDataSources) : emptyList();
             return this;
         }
 
-        public SettingsBuilder setTemplateFileExcludePattern(String templateFileExcludePattern) {
-            this.templateFileExcludePattern = templateFileExcludePattern;
+        public SettingsBuilder setSharedDataModels(List<String> sharedDataModels) {
+            this.sharedDataModels = sharedDataModels != null ? new ArrayList<>(sharedDataModels) : emptyList();
+            return this;
+        }
+
+        public SettingsBuilder setSourceIncludePattern(String sourceIncludePattern) {
+            this.sourceIncludePattern = sourceIncludePattern;
+            return this;
+        }
+
+        public SettingsBuilder setSourceExcludePattern(String sourceExcludePattern) {
+            this.sourceExcludePattern = sourceExcludePattern;
             return this;
         }
 
@@ -381,23 +343,6 @@
             return this;
         }
 
-        public SettingsBuilder setOutputs(List<String> outputs) {
-            if (outputs != null) {
-                this.outputs = outputs;
-            }
-            return this;
-        }
-
-        public SettingsBuilder setDataSourceIncludePattern(String dataSourceIncludePattern) {
-            this.dataSourceIncludePattern = dataSourceIncludePattern;
-            return this;
-        }
-
-        public SettingsBuilder setDataSourceExcludePattern(String dataSourceExcludePattern) {
-            this.dataSourceExcludePattern = dataSourceExcludePattern;
-            return this;
-        }
-
         public SettingsBuilder setLocale(String locale) {
             this.locale = locale;
             return this;
@@ -408,20 +353,6 @@
             return this;
         }
 
-        public SettingsBuilder setDataSources(List<String> dataSources) {
-            if (dataSources != null) {
-                this.dataSources = dataSources;
-            }
-            return this;
-        }
-
-        public SettingsBuilder setDataModels(List<String> dataModels) {
-            if (dataModels != null) {
-                this.dataModels = dataModels;
-            }
-            return this;
-        }
-
         public SettingsBuilder setParameters(Map<String, Object> parameters) {
             if (parameters != null) {
                 this.parameters = parameters;
@@ -443,37 +374,32 @@
             return this;
         }
 
-        public SettingsBuilder setWriter(Writer writer) {
-            this.writer = writer;
+        public SettingsBuilder setCallerSuppliedWriter(Writer callerSuppliedWriter) {
+            this.callerSuppliedWriter = callerSuppliedWriter;
             return this;
         }
 
         public Settings build() {
-            final Charset inputEncoding = Charset.forName(this.inputEncoding);
-            final Charset outputEncoding = Charset.forName(this.outputEncoding);
             final String currLocale = locale != null ? locale : getDefaultLocale();
 
             return new Settings(
                     configuration,
-                    args,
+                    commandLineArgs,
                     templateDirectories,
-                    templateNames,
-                    interactiveTemplate,
-                    templateFileIncludePattern,
-                    templateFileExcludePattern,
-                    inputEncoding,
-                    outputEncoding,
+                    Charset.forName(this.templateEncoding),
+                    outputGeneratorDefinitions,
+                    sharedDataSources,
+                    sharedDataModels,
+                    sourceIncludePattern,
+                    sourceExcludePattern,
+                    Charset.forName(this.inputEncoding),
+                    Charset.forName(this.outputEncoding),
                     verbose,
-                    outputs,
-                    dataSourceIncludePattern,
-                    dataSourceExcludePattern,
                     LocaleUtils.parseLocale(currLocale),
                     isReadFromStdin,
-                    dataSources,
-                    dataModels,
                     parameters,
                     systemProperties,
-                    writer
+                    callerSuppliedWriter
             );
         }
 
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 b6e6fd0..ab3681e 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
@@ -16,17 +16,10 @@
  */
 package org.apache.freemarker.generator.cli.config;
 
-import freemarker.cache.TemplateLoader;
-import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
 import org.apache.freemarker.generator.base.datasource.DataSourcesSupplier;
 import org.apache.freemarker.generator.base.file.PropertiesClassPathSupplier;
 import org.apache.freemarker.generator.base.file.PropertiesFileSystemSupplier;
 import org.apache.freemarker.generator.base.file.PropertiesSupplier;
-import org.apache.freemarker.generator.base.template.TemplateTransformationsBuilder;
-import org.apache.freemarker.generator.base.template.TemplateTransformationsSupplier;
-
-import java.util.Map;
-import java.util.function.Supplier;
 
 /**
  * Convenience methods to create suppliers.
@@ -34,11 +27,7 @@
 public class Suppliers {
 
     public static ConfigurationSupplier configurationSupplier(Settings settings) {
-        return new ConfigurationSupplier(settings, templateLoaderSupplier(settings));
-    }
-
-    public static ConfigurationSupplier configurationSupplier(Settings settings, Supplier<TemplateLoader> templateLoader) {
-        return new ConfigurationSupplier(settings, templateLoader);
+        return new ConfigurationSupplier(settings, templateLoaderSupplier(settings), toolsSupplier(settings));
     }
 
     public static TemplateDirectorySupplier templateDirectorySupplier(String additionalTemplateDirName) {
@@ -53,30 +42,19 @@
         return new ToolsSupplier(settings.getConfiguration(), settings.toMap());
     }
 
-    public static DataSourcesSupplier dataSourcesSupplier(Settings settings) {
-        return new DataSourcesSupplier(settings.getDataSources(),
-                settings.getDataSourceIncludePattern(),
-                settings.getDataSourceExcludePattern(),
+    public static OutputGeneratorsSupplier outputGeneratorsSupplier(Settings settings) {
+        return new OutputGeneratorsSupplier(settings);
+    }
+
+    public static DataSourcesSupplier sharedDataSourcesSupplier(Settings settings) {
+        return new DataSourcesSupplier(settings.getSharedDataSources(),
+                settings.getSourceIncludePattern(),
+                settings.getSourceExcludePattern(),
                 settings.getInputEncoding());
     }
 
-    public static DataModelSupplier dataModelSupplier(Settings settings) {
-        return new DataModelSupplier(settings.getDataModels());
-    }
-
-    public static Supplier<Map<String, Object>> parameterSupplier(Settings settings) {
-        return settings::getUserParameters;
-    }
-
-    public static TemplateTransformationsSupplier templateTransformationsSupplier(Settings settings) {
-        return () -> TemplateTransformationsBuilder.builder()
-                .setTemplate(Location.INTERACTIVE, settings.getInteractiveTemplate())
-                .addSources(settings.getTemplates())
-                .addInclude(settings.getTemplateFileIncludePattern())
-                .addExclude(settings.getTemplateFileExcludePattern())
-                .addOutputs(settings.getOutputs())
-                .setWriter(settings.getWriter())
-                .build();
+    public static DataModelSupplier sharedDataModelSupplier(Settings settings) {
+        return new DataModelSupplier(settings.getSharedDataModels());
     }
 
     public static PropertiesSupplier propertiesSupplier(String fileName) {
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ToolsSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ToolsSupplier.java
index 34efb5f..cb0e4d5 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ToolsSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ToolsSupplier.java
@@ -16,20 +16,19 @@
  */
 package org.apache.freemarker.generator.cli.config;
 
+import org.apache.freemarker.generator.base.FreeMarkerConstants.Configuration;
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
 import org.apache.freemarker.generator.base.tools.ToolsFactory;
+import org.apache.freemarker.generator.base.util.PropertiesTransformer;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Stream.of;
-import static org.apache.freemarker.generator.base.FreeMarkerConstants.Configuration.TOOLS_PREFIX;
-import static org.apache.freemarker.generator.base.util.PropertiesTransformer.filterKeyPrefix;
-import static org.apache.freemarker.generator.base.util.PropertiesTransformer.removeKeyPrefix;
 
 /**
  * Supplies FreeMarker tools based on the provided settings.
@@ -76,9 +75,9 @@
      * @return tool properties
      */
     private Properties toolsProperties() {
-        return of(configuration)
-                .map(p -> filterKeyPrefix(p, TOOLS_PREFIX))
-                .map(p -> removeKeyPrefix(p, TOOLS_PREFIX))
+        return Stream.of(configuration)
+                .map(p -> PropertiesTransformer.filterKeyPrefix(p, Configuration.TOOLS_PREFIX))
+                .map(p -> PropertiesTransformer.removeKeyPrefix(p, Configuration.TOOLS_PREFIX))
                 .findFirst().get();
     }
 
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesModel.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesModel.java
deleted file mode 100644
index 45fa479..0000000
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesModel.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.apache.freemarker.generator.cli.model;
-
-import freemarker.ext.beans.BeanModel;
-import freemarker.ext.beans.BeansWrapper;
-import org.apache.freemarker.generator.base.datasource.DataSource;
-import org.apache.freemarker.generator.base.datasource.DataSources;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Wraps an instance of <code>DataSources</code> into a more user-friendly <code>BeanModel</code>
- * so the user can use FreeMarker directives and features instead of using the exposed methods.
- */
-public class DataSourcesModel extends BeanModel {
-
-    public DataSourcesModel(DataSources dataSources, BeansWrapper objectWrapper) {
-        super(new SimpleDataSourcesAdapter(dataSources), requireNonNull(objectWrapper));
-    }
-
-    private static final class SimpleDataSourcesAdapter {
-
-        private final DataSources dataSources;
-
-        public SimpleDataSourcesAdapter(DataSources dataSources) {
-            this.dataSources = dataSources;
-        }
-
-        public DataSource get(int index) {
-            return dataSources.get(index);
-        }
-
-        public DataSource get(String name) {
-            return dataSources.get(name);
-        }
-
-        public int size() {
-            return dataSources.size();
-        }
-    }
-}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
index 57b9867..289611f 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
@@ -1,3 +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.
+ */
 package org.apache.freemarker.generator.cli.model;
 
 import freemarker.template.DefaultMapAdapter;
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
similarity index 64%
copy from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
copy to freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
index ef06672..0c4ffcf 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataModelDefinition.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,9 +14,17 @@
  * 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.picocli;
 
-import java.util.function.Supplier;
+import picocli.CommandLine.Option;
 
-public interface TemplateTransformationsSupplier extends Supplier<TemplateTransformations> {
+import java.util.List;
+
+/**
+ * User-supplied list of data sources directly exposed in the data model
+ */
+public class DataModelDefinition {
+
+    @Option(names = { "-m", "--data-model" }, description = "data model used for rendering")
+    public List<String> dataModels;
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
similarity index 65%
rename from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
rename to freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
index ef06672..61e9924 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/DataSourceDefinition.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,9 +14,17 @@
  * 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.picocli;
 
-import java.util.function.Supplier;
+import picocli.CommandLine.Option;
 
-public interface TemplateTransformationsSupplier extends Supplier<TemplateTransformations> {
+import java.util.List;
+
+/**
+ * User-supplied list of data sources or directories
+ */
+public class DataSourceDefinition {
+
+    @Option(names = { "-s", "--data-source" }, description = "data source used for rendering")
+    public List<String> dataSources;
 }
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
new file mode 100644
index 0000000..78352ec
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.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.picocli;
+
+import picocli.CommandLine;
+import picocli.CommandLine.ArgGroup;
+import picocli.CommandLine.ParameterException;
+
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+
+public class OutputGeneratorDefinition {
+
+    @ArgGroup(multiplicity = "1")
+    public TemplateSourceDefinition templateSourceDefinition;
+
+    @ArgGroup(exclusive = false)
+    public TemplateSourceFilterDefinition templateSourceFilterDefinition;
+
+    @ArgGroup(exclusive = false)
+    public TemplateOutputDefinition templateOutputDefinition;
+
+    @ArgGroup(exclusive = false)
+    public DataSourceDefinition dataSourceDefinition;
+
+    @ArgGroup(exclusive = false)
+    public DataModelDefinition dataModelDefinition;
+
+    public void validate(CommandLine commandLine) {
+        if (templateSourceDefinition == null) {
+            throw new ParameterException(commandLine, "No template defined to be rendered");
+        }
+
+        if (templateOutputDefinition != null && templateOutputDefinition.outputs.size() > 1) {
+            throw new ParameterException(commandLine, "More than one output defined for a template");
+        }
+
+        if (dataSourceDefinition != null && dataSourceDefinition.dataSources != null) {
+            for (String source : dataSourceDefinition.dataSources) {
+                if (isFileSource(source) && (source.contains("*") || source.contains("?"))) {
+                    throw new ParameterException(commandLine, "No wildcards supported for data source: " + source);
+                }
+            }
+        }
+    }
+
+    public List<String> getDataSources() {
+        if (dataSourceDefinition != null && dataSourceDefinition.dataSources != null) {
+            return dataSourceDefinition.dataSources;
+        } else {
+            return emptyList();
+        }
+    }
+
+    public List<String> getDataModels() {
+        if (dataModelDefinition != null && dataModelDefinition.dataModels != null) {
+            return dataModelDefinition.dataModels;
+        } else {
+            return emptyList();
+        }
+
+    }
+
+    public TemplateSourceDefinition getTemplateSourceDefinition() {
+        return templateSourceDefinition;
+    }
+
+    public TemplateSourceFilterDefinition getTemplateSourceFilterDefinition() {
+        return templateSourceFilterDefinition;
+    }
+
+    public TemplateOutputDefinition getTemplateOutputDefinition() {
+        return templateOutputDefinition;
+    }
+
+    public boolean hasTemplateSourceIncludes() {
+        return getTemplateSourceFilterDefinition() != null &&
+                getTemplateSourceFilterDefinition().templateIncludePatterns != null &&
+                !getTemplateSourceFilterDefinition().templateIncludePatterns.isEmpty();
+    }
+
+    public boolean hasTemplateSourceExcludes() {
+        return getTemplateSourceFilterDefinition() != null &&
+                getTemplateSourceFilterDefinition().templateExcludePatterns != null &&
+                !getTemplateSourceFilterDefinition().templateExcludePatterns.isEmpty();
+    }
+
+    private static boolean isFileSource(String source) {
+        if (source.contains("file://")) {
+            return true;
+        } else if (source.contains("://")) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
similarity index 63%
copy from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
copy to freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
index ef06672..59627e3 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateOutputDefinition.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,9 +14,18 @@
  * 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.picocli;
 
-import java.util.function.Supplier;
+import picocli.CommandLine.Option;
 
-public interface TemplateTransformationsSupplier extends Supplier<TemplateTransformations> {
+import java.util.List;
+
+public class TemplateOutputDefinition {
+
+    @Option(names = { "-o", "--output" }, description = "output files or directories")
+    public List<String> outputs;
+
+    public boolean hasOutput() {
+        return outputs != null && !outputs.isEmpty();
+    }
 }
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
new file mode 100644
index 0000000..6193c7b
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceDefinition.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 TemplateSourceDefinition {
+
+    @Option(names = { "-t", "--template" }, description = "templates to process")
+    public String template;
+
+    @Option(names = { "-i", "--interactive" }, description = "interactive template to process")
+    public String interactiveTemplate;
+
+    public boolean isInteractiveTemplate() {
+        return interactiveTemplate != null && !interactiveTemplate.isEmpty();
+    }
+}
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
new file mode 100644
index 0000000..68cce4e
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/TemplateSourceFilterDefinition.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+import java.util.List;
+
+/** Include/exclude pattern when processing template directories */
+public class TemplateSourceFilterDefinition {
+
+    @Option(names = { "--template-include" }, description = "template include pattern")
+    public List<String> templateIncludePatterns;
+
+    @Option(names = { "--template-exclude" }, description = "template exclude pattern")
+    public List<String> 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 3842508..1fe6f57 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
@@ -20,42 +20,30 @@
 import freemarker.template.Template;
 import freemarker.template.TemplateException;
 import org.apache.commons.io.FileUtils;
-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.DataSources;
+import org.apache.freemarker.generator.base.output.OutputGenerator;
 import org.apache.freemarker.generator.base.template.TemplateOutput;
 import org.apache.freemarker.generator.base.template.TemplateSource;
-import org.apache.freemarker.generator.base.template.TemplateTransformation;
-import org.apache.freemarker.generator.base.template.TemplateTransformations;
-import org.apache.freemarker.generator.base.util.UriUtils;
-import org.apache.freemarker.generator.cli.config.Settings;
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.Writer;
-import java.net.URI;
-import java.util.ArrayList;
+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.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.FreeMarkerConstants.Model.DATASOURCES;
-import static org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_PLAIN;
-import static org.apache.freemarker.generator.cli.config.Suppliers.configurationSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.dataModelSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.dataSourcesSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.parameterSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.templateTransformationsSupplier;
-import static org.apache.freemarker.generator.cli.config.Suppliers.toolsSupplier;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
 
 /**
  * Renders a FreeMarker template.
@@ -64,105 +52,83 @@
 
     private static final int SUCCESS = 0;
 
-    private final Settings settings;
-    private final Supplier<Map<String, Object>> toolsSupplier;
-    private final Supplier<List<DataSource>> dataSourcesSupplier;
-    private final Supplier<Map<String, Object>> dataModelsSupplier;
-    private final Supplier<Map<String, Object>> parameterModelSupplier;
     private final Supplier<Configuration> configurationSupplier;
-    private final Supplier<TemplateTransformations> templateTransformationsSupplier;
+    private final Supplier<List<OutputGenerator>> outputGeneratorsSupplier;
+    private final Supplier<Map<String, Object>> sharedDataModelSupplier;
+    private final Supplier<List<DataSource>> sharedDataSourcesSupplier;
+    private final Supplier<Map<String, Object>> sharedParametersSupplier;
 
-
-    public FreeMarkerTask(Settings settings) {
-        this(settings,
-                configurationSupplier(settings),
-                templateTransformationsSupplier(settings),
-                dataSourcesSupplier(settings),
-                dataModelSupplier(settings),
-                parameterSupplier(settings),
-                toolsSupplier(settings)
-        );
-    }
-
-    public FreeMarkerTask(Settings settings,
-                          Supplier<Configuration> configurationSupplier,
-                          Supplier<TemplateTransformations> templateTransformationsSupplier,
-                          Supplier<List<DataSource>> dataSourcesSupplier,
-                          Supplier<Map<String, Object>> dataModelsSupplier,
-                          Supplier<Map<String, Object>> parameterModelSupplier,
-                          Supplier<Map<String, Object>> toolsSupplier) {
-        this.settings = requireNonNull(settings);
-        this.toolsSupplier = requireNonNull(toolsSupplier);
-        this.dataSourcesSupplier = requireNonNull(dataSourcesSupplier);
-        this.dataModelsSupplier = requireNonNull(dataModelsSupplier);
-        this.parameterModelSupplier = requireNonNull(parameterModelSupplier);
-        this.configurationSupplier = requireNonNull(configurationSupplier);
-        this.templateTransformationsSupplier = requireNonNull(templateTransformationsSupplier);
+    public FreeMarkerTask(Supplier<Configuration> configurationSupplier,
+                          Supplier<List<OutputGenerator>> outputGeneratorsSupplier,
+                          Supplier<Map<String, Object>> sharedDataModelSupplier,
+                          Supplier<List<DataSource>> sharedDataSourcesSupplier,
+                          Supplier<Map<String, Object>> sharedParametersSupplier) {
+        this.configurationSupplier = requireNonNull(configurationSupplier, "configurationSupplier");
+        this.outputGeneratorsSupplier = requireNonNull(outputGeneratorsSupplier, "outputGeneratorsSupplier");
+        this.sharedDataModelSupplier = requireNonNull(sharedDataModelSupplier, "sharedDataModelSupplier");
+        this.sharedDataSourcesSupplier = requireNonNull(sharedDataSourcesSupplier, "sharedDataSourcesSupplier");
+        this.sharedParametersSupplier = requireNonNull(sharedParametersSupplier, "parametersSupplier");
     }
 
     @Override
     public Integer call() {
-        try {
-            final Configuration configuration = configurationSupplier.get();
-            final TemplateTransformations templateTransformations = templateTransformationsSupplier.get();
-            final DataSources dataSources = dataSources(settings, dataSourcesSupplier);
-            final Map<String, Object> dataModel = dataModel(dataSources, parameterModelSupplier, dataModelsSupplier, toolsSupplier);
-            templateTransformations.getList().forEach(t -> process(configuration, t, dataModel));
-            return SUCCESS;
-        } catch (RuntimeException e) {
-            throw new RuntimeException("Failed to process templates", e);
-        }
+        final Configuration configuration = configurationSupplier.get();
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final Map<String, Object> sharedDataModel = sharedDataModelSupplier.get();
+        final List<DataSource> sharedDataSources = sharedDataSourcesSupplier.get();
+        final Map<String, Object> sharedParameters = sharedParametersSupplier.get();
+
+        outputGenerators.forEach(outputGenerator -> process(
+                configuration,
+                outputGenerator,
+                sharedDataModel,
+                sharedDataSources,
+                sharedParameters));
+
+        return SUCCESS;
     }
 
     private void process(Configuration configuration,
-                         TemplateTransformation templateTransformation,
-                         Map<String, Object> dataModel) {
-        final TemplateSource templateSource = templateTransformation.getTemplateSource();
-        final TemplateOutput templateOutput = templateTransformation.getTemplateOutput();
+                         OutputGenerator outputGenerator,
+                         Map<String, Object> sharedDataModelMap,
+                         List<DataSource> sharedDataSources,
+                         Map<String, Object> sharedParameters) {
+        final TemplateSource templateSource = outputGenerator.getTemplateSource();
+        final TemplateOutput templateOutput = outputGenerator.getTemplateOutput();
+        final DataSources dataSources = toDataSources(outputGenerator, sharedDataSources);
+        final Map<String, Object> variables = outputGenerator.getVariables();
+        final Map<String, Object> templateDataModel = toTemplateDataModel(dataSources, variables, sharedDataModelMap, sharedParameters);
+
         try (Writer writer = writer(templateOutput)) {
             final Template template = template(configuration, templateSource);
-            template.process(dataModel, writer);
+            template.process(templateDataModel, writer);
         } catch (TemplateException | IOException e) {
             throw new RuntimeException("Failed to process template: " + templateSource.getName(), e);
         }
     }
 
-    private static DataSources dataSources(Settings settings, Supplier<List<DataSource>> dataSourcesSupplier) {
-        final List<DataSource> dataSources = new ArrayList<>(dataSourcesSupplier.get());
-
-        // Add optional data source from STDIN at the start of the list since
-        // this allows easy sequence slicing in FreeMarker.
-        if (settings.isReadFromStdin()) {
-            final URI uri = UriUtils.toUri(Location.SYSTEM, "in");
-            dataSources.add(0, DataSourceFactory.fromInputStream(STDIN, DEFAULT_GROUP, uri, System.in, MIME_TEXT_PLAIN, UTF_8));
-        }
-
-        return new DataSources(dataSources);
+    private static DataSources toDataSources(OutputGenerator outputGenerator, List<DataSource> sharedDataSources) {
+        return new DataSources(Stream.of(outputGenerator.getDataSources(), sharedDataSources)
+                .flatMap(Collection::stream).collect(Collectors.toList()));
     }
 
-    private static Map<String, Object> dataModel(
-            DataSources dataSources,
-            Supplier<Map<String, Object>> parameterModelSupplier,
-            Supplier<Map<String, Object>> dataModelsSupplier,
-            Supplier<Map<String, Object>> tools) {
+    @SafeVarargs
+    private static Map<String, Object> toTemplateDataModel(DataSources dataSources, Map<String, Object>... maps) {
         final Map<String, Object> result = new HashMap<>();
-        result.putAll(dataModelsSupplier.get());
-        result.put(DATASOURCES, dataSources);
-        result.putAll(parameterModelSupplier.get());
-        result.putAll(tools.get());
+        Arrays.stream(maps).forEach(result::putAll);
+        result.put(Model.DATASOURCES, dataSources);
         return result;
     }
 
-    // ==============================================================
-
     private static Writer writer(TemplateOutput templateOutput) throws IOException {
-        if (templateOutput.getWriter() != null) {
+        if (templateOutput.hasWriter()) {
             return templateOutput.getWriter();
+        } else {
+            final File file = templateOutput.getFile();
+            FileUtils.forceMkdirParent(file);
+            // We need to explicitly set our output encoding here - see https://freemarker.apache.org/docs/pgui_misc_charset.html
+            return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), templateOutput.getCharset()));
         }
-
-        final File file = templateOutput.getFile();
-        FileUtils.forceMkdirParent(file);
-        return new BufferedWriter(new FileWriter(file));
     }
 
     /**
@@ -177,12 +143,12 @@
      */
     private static Template template(Configuration configuration, TemplateSource templateSource) {
         switch (templateSource.getOrigin()) {
-            case PATH:
+            case TEMPLATE_LOADER:
                 return fromTemplatePath(configuration, templateSource);
-            case CODE:
+            case TEMPLATE_CODE:
                 return fromTemplateCode(configuration, templateSource);
             default:
-                throw new IllegalArgumentException("Don't know how to handle: " + templateSource.getOrigin());
+                throw new IllegalArgumentException("Don't know how to create a template: " + templateSource.getOrigin());
         }
     }
 
@@ -197,7 +163,6 @@
 
     private static Template fromTemplateCode(Configuration configuration, TemplateSource templateSource) {
         final String name = templateSource.getName();
-
         try {
             return new Template(name, templateSource.getCode(), configuration);
         } catch (IOException e) {
diff --git a/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md b/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
index bb8b8ca..092a5cb 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/advanced/cli-configuration.md
@@ -16,7 +16,6 @@
 # See https://freemarker.apache.org/docs/api/freemarker/template/Configuration.html#setSetting-java.lang.String-java.lang.String-
 #############################################################################
 # freemarker.configuration.setting.locale=JVM default
-
 #############################################################################
 # Configure FreeMarker Tools (name -> implementation class)
 #############################################################################
@@ -27,7 +26,8 @@
 freemarker.tools.freemarker=org.apache.freemarker.generator.tools.freemarker.FreeMarkerTool
 freemarker.tools.grok=org.apache.freemarker.generator.tools.grok.GrokTool
 freemarker.tools.gson=org.apache.freemarker.generator.tools.gson.GsonTool
-freemarker.tools.jsonpath=org.apache.freemarker.generator.tools.json.JsonPathTool
+freemarker.tools.javafaker=org.apache.freemarker.generator.tools.javafaker.JavaFakerTool
+freemarker.tools.jsonpath=org.apache.freemarker.generator.tools.jsonpath.JsonPathTool
 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
@@ -71,7 +71,6 @@
 
 FreeMarker Generator Template Loader Directories
 ------------------------------------------------------------------------------
-[#1] /Users/sgoeschl
-[#2] /Users/sgoeschl/.freemarker-generator
-[#3] /Applications/Java/freemarker-generator-2.0.0
+[#1] /Users/sgoeschl/.freemarker-generator/templates
+[#2] /Applications/Java/freemarker-generator/templates
 ``` 
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 53dd6bf..e6e4b3e 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
@@ -83,6 +83,17 @@
 URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/json/swagger-spec.json```
 ```
 
+Access to `stdin` is implemented as `DataSource` - please not that `stdin` is read lazy to cater for arbitrary large input data
+
+```
+cat examples/data/csv/contract.csv | bin/freemarker-generator -t freemarker-generator/info.ftl --stdin
+
+FreeMarker Generator DataSources
+------------------------------------------------------------------------------
+[#1]: name=stdin, group=default, fileName=stdin mimeType=text/plain, charset=UTF-8, length=-1 Bytes
+URI : system:///stdin
+```
+
 ### Selecting A DataSource
 
 After loading one or more `DataSource` they are accessible as `dataSource` map in the FreeMarker model
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/design-goals.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/design-goals.md
index 0d2e07c..1cf4634 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/design-goals.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/design-goals.md
@@ -13,5 +13,6 @@
 * XML & XPath is supported by FreeMarker [out-of-the-box](http://freemarker.org/docs/xgui.html)
 * Support for reading a data source content from STDIN to integrate with command line tools
 * Support execution of arbitrary commands using [Apache Commons Exec](https://commons.apache.org/proper/commons-exec/)
+* Support creation of test data using [JavaFaker](https://github.com/DiUS/java-faker/)
 * Add some commonly useful information such as `System Properties`, `Enviroment Variables`
 * Support embedding the code in existing applications
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/passing-data.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/passing-data.md
index d181c01..3ff42cc 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/passing-data.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/passing-data.md
@@ -5,7 +5,7 @@
 * System properties
 * Parameters 
 
-### User-Supplied System Poperties
+### User-Supplied System Properties
 
 User-supplied system properties are added to the JVM's system properties
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
index 9de70b6..5b1ce99 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/template-loading.md
@@ -10,7 +10,6 @@
 
 `Apache FreeMarker Generator CLI` uses a `MultiTemplateLoader` searching for templates in the following directories
 
-* Current working directory
 * Optional `~/.freemarker-generator` directory
 * `Apache FreeMarker Generator` installation directory
 
@@ -21,9 +20,8 @@
 
 FreeMarker Generator Template Loader Directories
 ------------------------------------------------------------------------------
-[#1] /Users/sgoeschl/work/github/apache/freemarker-generator
-[#2] /Users/sgoeschl/.freemarker-generator
-[#3] /Applications/Java/freemarker-generator-2.0.0
+[#1] /Users/sgoeschl/.freemarker-generator/templates
+[#2] /Applications/Java/freemarker-generator/templates
 ```
 
 The main benefit of `MultiTemplateLoader` is the use of abstract template paths finding a template in the template loader directories
@@ -38,16 +36,28 @@
 <#import "/lib/commons-csv.ftl" as csv />
 ```  
 
+You can add a template directory, e.g. the current working directory
+
+```
+freemarker-generator -t freemarker-generator/info.ftl --template-dir=./
+
+FreeMarker Generator Template Loader Directories
+------------------------------------------------------------------------------
+[#1] /Users/sgoeschl
+[#2] /Users/sgoeschl/.freemarker-generator/templates
+[#3] /Applications/Java/freemarker-generator/templates
+```
+
 ### Free-Style Template Loading
 
-The previously described `Template Loaders` do not support absolute template files or arbitrary URLS - this behaviour 
+The previously described `Template Loaders` do not support absolute template files or arbitrary URLs - this behaviour 
 stems from security aspects when running `Apache FreeMarker` on the server side. For a command-line tool this is mostly
 irrelevant therefore any template file outside of the template loader directories can be loaded 
 
 This example loads the `info.ftl` directly from a GitHub URL
 
 ```
-freemarker-generator -t https://raw.githubusercontent.com/apache/freemarker-generator/master/freemarker-generator-cli/templates/info.ftl
+freemarker-generator -t https://raw.githubusercontent.com/apache/freemarker-generator/master/freemarker-generator-cli/src/app/templates/freemarker-generator/info.ftl
 ```
 
 ### Interactive Template Loading
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/tools.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/tools.md
index c5bb4da..e4a34c1 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/tools.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/tools.md
@@ -4,19 +4,20 @@
 
 The following tools are currently implemented
 
-| Tool                  | Description                                                                                               |
-|-----------------------|-----------------------------------------------------------------------------------------------------------|
-| CSVTool               | Process CSV files using [Apache Commons CSV](https://commons.apache.org/proper/commons-csv/)              |
-| DataFrameTool         | Bridge to [nRo/DataFrame](https://github.com/nRo/DataFrame)                                               |
-| ExecTool              | Execute command line tools using [Apache Commons Exec](https://commons.apache.org/proper/commons-exec/)   |
-| ExcelTool             | Process Excels files (XLS, XLSX) using [Apache POI](https://poi.apache.org)                               |
-| FreeMarkerTool        | Expose useful FreeMarker classes                                                                          |
-| GrokTool              | Process text files using [Grok](https://github.com/thekrakken/java-grok) instead of regular expressions   |
-| GsonTool              | Process JSON files using [GSON](https://github.com/google/gson)                                           |
-| JsonPathTool          | Process JSON file using [Java JSON Path](https://github.com/json-path/JsonPath)                           |
-| JsoupTool             | Processing HTML files using [Jsoup](https://jsoup.org)                                                    |
-| PropertiesTool        | Process JDK properties files                                                                              |
-| SystemTool            | System-related utility methods                                                                            |
-| UUIDTool              | Create UUIDs                                                                                              |
-| XmlTool               | Process XML files using [Apache FreeMarker](https://freemarker.apache.org/docs/xgui.html)                 |
-| YamlTool              | Process YAML files using [SnakeYAML](https://bitbucket.org/asomov/snakeyaml/wiki/Home)                    |
+| Implementation Class  | Name          | Description                                                                                               |
+|-----------------------|---------------|-----------------------------------------------------------------------------------------------------------|
+| CSVTool               | csv           | Process CSV files using [Apache Commons CSV](https://commons.apache.org/proper/commons-csv/)              |
+| DataFrameTool         | dataframe     | Bridge to [nRo/DataFrame](https://github.com/nRo/DataFrame)                                               |
+| ExcelTool             | excel         | Process Excels files (XLS, XLSX) using [Apache POI](https://poi.apache.org)                               |
+| ExecTool              | exec          | Execute command line tools using [Apache Commons Exec](https://commons.apache.org/proper/commons-exec/)   |
+| FreeMarkerTool        | freemarker    | Expose useful FreeMarker classes                                                                          |
+| GrokTool              | grok          | Process text files using [Grok](https://github.com/thekrakken/java-grok) instead of regular expressions   |
+| GsonTool              | gson          | Process JSON files using [GSON](https://github.com/google/gson)                                           |
+| JavaFakerTool         | javafaker     | Generate test data using Java Faker [JavaFaker](https://github.com/DiUS/java-faker)
+| JsonPathTool          | jsonpath      | Process JSON file using [Java JSON Path](https://github.com/json-path/JsonPath)                           |
+| JsoupTool             | jsoup         | Processing HTML files using [Jsoup](https://jsoup.org)                                                    |
+| PropertiesTool        | properties    | Process JDK properties files                                                                              |
+| SystemTool            | system        | System-related utility methods                                                                            |
+| UUIDTool              | uuid          | Create UUIDs                                                                                              |
+| XmlTool               | xaml          | Process XML files using [Apache FreeMarker](https://freemarker.apache.org/docs/xgui.html)                 |
+| YamlTool              | yaml          | Process YAML files using [SnakeYAML](https://bitbucket.org/asomov/snakeyaml/wiki/Home)                    |
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 03a9b3a..11b1323 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
@@ -3,30 +3,29 @@
 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 mapped to a series of `template transformations`
-* The `transformation` consists of exactly one `template`, 0..n `data sources` / `data models` written to an `output`
-* The `output` is either written to
+* A command line invocation is internally mapped to a list of `output generators`
+* 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`
-    * one or more output files
-    * one or output directories
+    * an output file
+    * an output directory
 * When the output is written to a directory
     * the structure of the input directory is preserved
-    * a `ftl` file extension is removed
+    * any `ftl` file extension is removed
+* Positional command line arguments are interpreted as `data sources` (or directories) and accessible by a `output generators`   
 
 ### Examples
 
-Transforming a single template to a single output file 
+Transform a single template to a single output file 
+
+```
+freemarker-generator -t freemarker-generator/csv/md/transform.ftl -o target/contract.md examples/data/csv/contract.csv 
+```
+
+Transform multiple templates to multiple output files (1:1 mapping between templates and outputs)
 
 ```
 freemarker-generator \
--t freemarker-generator/csv/md/transform.ftl examples/data/csv/contract.csv \
--o target/contract.md
-```
-
-Transforming multiple templates to multiple output files (1:1 mapping between templates and outputs)
-
-```
-> 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
@@ -37,7 +36,7 @@
 `-- contract.md
 ```
 
-Transforming single template directory to single output directory
+Transform a single template directory to single output directory
 
 ```
 > freemarker-generator \
@@ -49,8 +48,6 @@
     |-- application.properties
     `-- nginx
         `-- nginx.conf
-
-
 ```
 
 Transforming multiple template directories to multiple output directories
@@ -70,5 +67,16 @@
     |-- application.properties
     `-- nginx
         `-- nginx.conf
+```
+
+Transforming multiple templates and data sources to multiple output files
 
 ```
+freemarker-generator \
+-t freemarker-generator/yaml/json/transform.ftl -s examples/data/yaml/customer.yaml -o customer.json \
+-t freemarker-generator/yaml/json/transform.ftl -s examples/data/yaml/swagger-spec.yaml -o swagger-spec.json
+
+> ls -l *.json
+-rw-r--r--  1 sgoeschl  staff   332B Jan  5 21:30 customer.json
+-rw-r--r--  1 sgoeschl  staff    25K Jan  5 21:30 swagger-spec.json
+```  
diff --git a/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md b/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
index f1bc312..833fd37 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/introduction/getting-started.md
@@ -12,7 +12,7 @@
 On my local box (Mac OS 10.15.5) I use the following setup
 
 ```
-export FREEMARKER_CLI_HOME=/Applications/Java/freemarker-generator-2.0.0
+export FREEMARKER_CLI_HOME=/Applications/Java/freemarker-generator
 export PATH=$PATH:$FREEMARKER_CLI_HOME/bin
 ```
 
@@ -20,14 +20,14 @@
 
 ```
 > which freemarker-generator
-/Applications/Java/freemarker-generator-2.0.0/bin/freemarker-generator
+/Applications/Java/freemarker-generator/bin/freemarker-generator
 ```
 
 and check the version of `Apache FreeMarker Generator`
 
 ```
 > freemarker-generator -V
-version=0.1.0-SNAPSHOT, time=2020-06-25T21:48:02+0200, commit=b320d00094be8789086ad6153d9d3fcaf4b8c75f
+version=0.1.0-SNAPSHOT, time=2021-01-09T10:41:01+0100, commit=d308ede197f1c2972e3b328b9a37fa233cae101a
 ```
 
 ### Command Line Options
@@ -36,27 +36,32 @@
 
 ```
 > freemarker-generator -h
-Usage: freemarker-generator (-t=<templates> [-t=<templates>]... |
-                            -i=<interactiveTemplate>) [-hV] [--stdin]
-                            [--config=<configFile>]
+Usage: freemarker-generator [-hV] [--stdin] [--config=<configFile>]
                             [--data-source-exclude=<dataSourceExcludePattern>]
                             [--data-source-include=<dataSourceIncludePattern>]
                             [-e=<inputEncoding>] [-l=<locale>]
                             [--output-encoding=<outputEncoding>]
-                            [--template-dir=<templateDir>] [--times=<times>]
-                            [-D=<String=String>]... [-m=<dataModels>]...
-                            [-o=<outputs>]... [-P=<String=String>]...
-                            [-s=<dataSources>]... [<sources>...]
+                            [--template-dir=<templateDir>]
+                            [--template-encoding=<templateEncoding>]
+                            [--times=<times>] [-D=<String=String>]...
+                            [-P=<String=String>]...
+                            [--shared-data-model=<sharedDataModels>]...
+                            ((-t=<template> | -i=<interactiveTemplate>)
+                            [[--template-include=<templateIncludePatterns>]...
+                            [--template-exclude=<templateExcludePatterns>]...]
+                            [[-o=<outputs>]...] [[-s=<dataSources>]...]
+                            [[-m=<dataModels>]...])... [<sharedDataSources>...]
 Apache FreeMarker Generator
-      [<sources>...]       data source files and/or directories
+      [<sharedDataSources>...]
+                           shared data source files and/or directories
       --config=<configFile>
                            FreeMarker Generator configuration file
   -D, --system-property=<String=String>
                            set system property
       --data-source-exclude=<dataSourceExcludePattern>
-                           file exclude pattern for data sources
+                           data source exclude pattern
       --data-source-include=<dataSourceIncludePattern>
-                           file include pattern for data sources
+                           data source include pattern
   -e, --input-encoding=<inputEncoding>
                            encoding of data source
   -h, --help               Show this help message and exit.
@@ -72,11 +77,19 @@
                            set parameter
   -s, --data-source=<dataSources>
                            data source used for rendering
+      --shared-data-model=<sharedDataModels>
+                           shared data models used for rendering
       --stdin              read data source from stdin
-  -t, --template=<templates>
+  -t, --template=<template>
                            templates to process
       --template-dir=<templateDir>
                            additional template directory
+      --template-encoding=<templateEncoding>
+                           template encoding
+      --template-exclude=<templateExcludePatterns>
+                           template exclude pattern
+      --template-include=<templateIncludePatterns>
+                           template include pattern
       --times=<times>      re-run X times for profiling
   -V, --version            Print version information and exit.
 ```
@@ -87,26 +100,25 @@
 to better understand `Apache FreeMarker Generator`
 
 ```
-> freemarker-generator -t freemarker-generator/info.ftl
+> freemarker-generator -t freemarker-generator/info.ftl 
 FreeMarker Generator Information
 ------------------------------------------------------------------------------
 FreeMarker version     : 2.3.30
-Template name          : info.ftl
+Template name          : freemarker-generator/info.ftl
 Language               : en
 Locale                 : en_US
-Timestamp              : Nov 9, 2020 1:13:21 PM
+Timestamp              : Jan 7, 2021 11:36:26 PM
 Output encoding        : UTF-8
 Output format          : plainText
 
 FreeMarker Generator Template Loader Directories
 ------------------------------------------------------------------------------
 [#1] /Users/sgoeschl/.freemarker-generator/templates
-[#2] /Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/templates
+[#2] /Applications/Java/freemarker-generator/templates
 
 FreeMarker Generator Data Model
 ---------------------------------------------------------------------------
 - dataSources
-- tools
 
 FreeMarker Generator Tools
 ------------------------------------------------------------------------------
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/generating-testdata.md b/freemarker-generator-cli/src/site/markdown/cli/usage/generating-testdata.md
index f19714d..4019eba 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/generating-testdata.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/generating-testdata.md
@@ -5,30 +5,32 @@
 Let's assume that you need to populate a user table based on a CSV file with random data
 
 ```
-freemarker-generator -DNR_OF_RECORDS=10 -t examples/templates/javafaker/csv/testdata.ftl
+freemarker-generator -DNR_OF_RECORDS=10 -t examples/templates/javafaker/csv/testdata.ftl; echo
 ```  
 
 will generate
 
 ```
 ID,CUSTOMER_ID,FIRST_NAME,LAST_NAME,EMAIL,IBAN,CREATED_AT
-5c3dbf2b-2957-41fe-8566-e16c91d6bba7,e6044780,Audrey,Ryan,audrey.ryan@gmail.com,DE69137185182464804325,2016-07-25T03:06:54+02:00
-34671167-92f8-46e3-874b-d488960eb320,z8394366,Herb,Lehner,herb.lehner@gmail.com,DE32993443443552974345,2019-10-11T23:27:32+02:00
-479855f6-cc98-4c46-99e4-b1d38d35a7b2,x8937857,Kirby,Wilkinson,kirby.wilkinson@gmail.com,DE12566901129220359287,2020-04-12T04:49:00+02:00
-1a3c51b9-d168-4c0a-84ae-05e79cd181b1,t3486957,Charmaine,Bergstrom,charmaine.bergstrom@gmail.com,DE98964063811726229158,2015-07-24T23:19:19+02:00
-43b9f7ad-1aec-44ff-b3c1-de7688b5a729,z9190225,Sterling,Glover,sterling.glover@gmail.com,DE47207633748672977993,2019-11-04T04:45:06+01:00
-34ce2c9f-e5bb-44f4-a71f-40b0dfa8d0bf,a4406167,George,Marquardt,george.marquardt@gmail.com,DE79342449317255392445,2016-05-15T16:33:05+02:00
-1f9bbc16-8b17-4947-ab50-4abf6aa4cc46,s6438445,Arnoldo,Herzog,arnoldo.herzog@gmail.com,DE20421444995381411375,2013-10-12T07:01:01+02:00
-30e3f7a2-7fe8-4ebf-b46b-4f59ab62ba45,o9507275,Nickie,Predovic,nickie.predovic@gmail.com,DE06666930299990216198,2019-08-01T10:51:51+02:00
-f703e93e-7bc3-42c9-a7f5-f1db84d32fd1,z8385157,Clinton,Murphy,clinton.murphy@gmail.com,DE27305002168865903990,2018-04-01T19:03:55+02:00
-7f6a8d29-2dfc-4467-b366-25b46aa5bc32,x5244747,Johnson,Blanda,johnson.blanda@gmail.com,DE83757301199253406795,2012-06-23T18:04:38+02:00
+ec48407f-b74b-3255-aa23-c478b97fe6a0,k1627756,Finnja,Huebel,finnja.huebel@server.invalid,DE34649925979537623502,2019-02-02T06:19:32+01:00
+4386bbd1-f410-310f-9961-3e8b2bdfb158,k2697194,Catharina,Ruckdeschel,catharina.ruckdeschel@server.invalid,DE20946378663346781874,2016-01-08T21:31:30+01:00
+5225a437-a981-33f8-be17-4520d1656357,i2477693,Efe,Grün,efe.grün@server.invalid,DE72530958450307406958,2014-01-27T19:33:54+01:00
+f3d7cbd6-b7ba-3709-97e3-a0f8c66f3a61,h1789606,Adriano,Walz,adriano.walz@server.invalid,DE91943313948716057559,2019-01-06T18:57:24+01:00
+3b43d134-8342-322d-8814-2efb8d2525af,x9067951,Denny,Kleininger,denny.kleininger@server.invalid,DE60996500459835447795,2014-10-30T19:07:59+01:00
+0f145471-293f-32c4-8dd2-9be95540e5f6,k9415677,Silas,Bönisch,silas.bönisch@server.invalid,DE53857572315572131803,2014-07-15T15:33:06+02:00
+ec0a21b7-106b-3983-8b19-03bf91f5fa33,a0190000,Ceyda,Schäffel,ceyda.schäffel@server.invalid,DE83184389823488369676,2020-07-28T21:16:33+02:00
+9c0d0feb-dd08-3cbd-875e-ea9f072d1a96,w7084669,Wilhelm,Burmeister,wilhelm.burmeister@server.invalid,DE89066581983817613534,2017-09-26T04:01:56+02:00
+76f770a9-4008-317e-800f-6c203dd363d8,f7683995,Madlen,Knobel,madlen.knobel@server.invalid,DE82012476109671707669,2018-07-08T05:52:25+02:00
+6ecf87f6-6a46-3f06-9482-ab2ffe696c41,y8814536,Jessica,Koubaa,jessica.koubaa@server.invalid,DE79396597066674926625,2019-10-12T23:55:02+02:00
+4a72d322-55db-35ee-b47e-c484d29b5760,v1849194,Jesper,Hildenbrand,jesper.hildenbrand@server.invalid,DE60616233632021309556,2015-03-09T01:37:18+01:00
 ```
 
 using the following FreeMarker template
 
 ```
-<#assign faker = tools.javafaker.faker>
-<#assign nrOfRecords = tools.system.getString("NR_OF_RECORDS","10")>
+<#-- Get a localized JavaFaker instance -->
+<#assign faker = tools.javafaker.getFaker("de_DE")>
+<#assign nrOfRecords = tools.system.getString("NR_OF_RECORDS","100")>
 <#assign days = tools.javafaker.timeUnits["DAYS"]>
 <#assign csvTargetFormat = tools.csv.formats["DEFAULT"].withFirstRecordAsHeader()>
 <#assign csvPrinter = tools.csv.printer(csvTargetFormat)>
@@ -37,15 +39,18 @@
     <#if csvTargetFormat.getSkipHeaderRecord()>
         ${csvPrinter.printRecord(csvHeaders)}<#t>
     </#if>
-    <#list 1..nrOfRecords?number as i>
-        <#assign id = tools.uuid.randomUUID()>
+    <#list 0..nrOfRecords?number as i>
+        <#-- Generate a reproducable id to allow re-importing of test data -->
+        <#assign id = tools.uuid.namedUUID("trxid-" + i?string)>
         <#assign customerId = faker.bothify("?#######")>
         <#assign firstName = faker.name().firstName()>
         <#assign lastName = faker.name().lastName()>
-        <#assign email = firstName + "." + lastName + "@gmail.com">
+        <#assign email = firstName + "." + lastName + "@server.invalid">
+        <#-- JavaFakers IBAN generation is really slow -->
         <#assign iban = faker.finance().iban("DE")>
-
+        <#-- Distribute the creation date up to 10 years in the past -->
         <#assign createAt = faker.date().past(3650, days)>
+        <#-- Use a CSV Printer to properly escape the output -->
         ${csvPrinter.printRecord(
             id,
             customerId,
@@ -61,6 +66,7 @@
 Some thoughts along the line
 
 * [Java Faker](https://github.com/DiUS/java-faker) does not create coherent test data, e.g. each invocation of "name" creates a new random name - hence we create the email address ourselves
-* The created IBAN does not use a valid bank code but structure and checksum is correct
+* The created IBAN does not use a valid bank code but structure and checksum is correct (albeit slow)
 * The "createdAt" generates a creation date from the last 10 years to have some proper distribution
-* See [A Guide to JavaFaker](https://www.baeldung.com/java-faker) for a quick overview
\ No newline at end of file
+* See [A Guide to JavaFaker](https://www.baeldung.com/java-faker) for a quick overview
+* A CSV Printer is used to properly escape the generated fields (in case they contain a CSV separator)
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md b/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
index 2aa4e7f..247d9a8 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
@@ -34,12 +34,20 @@
   index index.htm;
 ```
 
-### Transform Template Directory To STDOUT
+### Transform Template Directory To Output Directory
 
-If no output directory is provided all output is written to `stdout`
+The transformed templates are written to an `out` directory
+
+* `nginx.conf.ftl` was changed to `nginx.conf" during the transformation
 
 ```
-freemarker-generator -t examples/data/template/
+freemarker-generator -t examples/data/template/ -o out; tree out; cat out/application.properties out/nginx/nginx.conf
+out
+|-- application.properties
+`-- nginx
+    `-- nginx.conf
+
+1 directory, 2 files    
 # == application.properties ==================================================
 server.name=127.0.0.1
 server.logs=/var/log/nginx
@@ -53,35 +61,21 @@
 }
 ```
 
-### Transform Template Directory To Output Directory
-
-The transformed templates are written to an `out` directory
-
-* `nginx.conf.ftl` was changed to `nginx.conf" during the transformation
-
-```
-freemarker-generator -t examples/data/template/ -o out; tree out
-out
-|-- application.properties
-`-- nginx
-    `-- nginx.conf
-
-1 directory, 2 files
-```
-
 ### Use Command Line Parameters
 
 A user-supplied parameter `NGINX_HOSTNAME` is used to render the templates
 
 ```
-freemarker-generator -t examples/data/template/ -P NGINX_HOSTNAME=localhost
+freemarker-generator -t examples/data/template/ -P NGINX_HOSTNAME=some.host.invalid -o out; \
+cat out/application.properties out/nginx/nginx.conf
+
 # == application.properties ==================================================
-server.name=localhost
+server.name=some.host.invalid
 server.logs=/var/log/nginx
 # == nginx-conf ==============================================================
 server {
   listen 80;
-  server_name localhost;
+  server_name some.host.invalid;
 
   root /usr/share/nginx/www;
   index index.htm;
@@ -136,15 +130,15 @@
 Another option is passing the information as JSON file
 
 ```
-echo '{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}' > nginx.json
+echo '{"NGINX_PORT":"8443","NGINX_HOSTNAME":"some.host.invalid"}' > nginx.json
 freemarker-generator -t examples/data/template/ -m nginx.json 
 # == application.properties ==================================================
-server.name=localhost
+server.name=some.host.invalid
 server.logs=/var/log/nginx
 # == nginx-conf ==============================================================
 server {
   listen 8443;
-  server_name localhost;
+  server_name some.host.invalid;
 
   root /usr/share/nginx/www;
   index index.htm;
@@ -156,7 +150,7 @@
 Yet another option is using a YAML file
 
 ```
-echo -e "- NGINX_PORT": "\"8443\"\n- NGINX_HOSTNAME": "localhost" > nginx.yaml
+echo -e "- NGINX_PORT": "\"8443\"\n- NGINX_HOSTNAME": "some.host.invalid" > nginx.yaml
 freemarker-generator -t examples/data/template/ -m nginx.yaml 
 # == application.properties ==================================================
 server.name=localhost
@@ -179,15 +173,15 @@
 * `#mimeType=application/json` defines that JSON content is parsed
 
 ```
-export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
+export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"some.host.invalid"}'
 freemarker-generator -t examples/data/template/ -m env:///NGINX_CONF#mimeType=application/json
 # == application.properties ==================================================
-server.name=localhost
+server.name=some.host.invalid
 server.logs=/var/log/nginx
 # == nginx-conf ==============================================================
 server {
   listen 8443;
-  server_name localhost;
+  server_name some.host.invalid;
 
   root /usr/share/nginx/www;
   index index.htm;
@@ -199,7 +193,7 @@
 For testing purpose it is useful to override certain settings
 
 ```
-export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
+export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"some.host.invalid"}'
 freemarker-generator -t examples/data/template/ -PNGINX_HOSTNAME=www.mydomain.com -m env:///NGINX_CONF#mimeType=application/json
 # == application.properties ==================================================
 server.name=www.mydomain.com
diff --git a/freemarker-generator-cli/src/site/markdown/index.md b/freemarker-generator-cli/src/site/markdown/index.md
index 9a6ecc4..e6cde71 100644
--- a/freemarker-generator-cli/src/site/markdown/index.md
+++ b/freemarker-generator-cli/src/site/markdown/index.md
@@ -9,9 +9,9 @@
 * Transform a directory with a single command-line invocation
 * Made for the heavy lifting of data by using lazy-loading and streaming 
 
-### Getting Started
+### First Seps
 
-* [Installation](cli/introduction/getting-started.html)
+* [Getting Started](cli/introduction/getting-started.html)
 * [Running Examples](cli/usage/running-examples.html)
 
 ### Concepts
diff --git a/freemarker-generator-cli/src/test/data/encoding/utf16.txt b/freemarker-generator-cli/src/test/data/encoding/utf16.txt
index 4d8e1f2..6dfd2e5 100755
--- a/freemarker-generator-cli/src/test/data/encoding/utf16.txt
+++ b/freemarker-generator-cli/src/test/data/encoding/utf16.txt
Binary files differ
diff --git a/freemarker-generator-cli/src/test/data/encoding/utf8.txt b/freemarker-generator-cli/src/test/data/encoding/utf8.txt
index c43eb00..25f1e5c 100755
--- a/freemarker-generator-cli/src/test/data/encoding/utf8.txt
+++ b/freemarker-generator-cli/src/test/data/encoding/utf8.txt
@@ -1,3 +1,5 @@
+UTF-8 encoding
+=============================================================================
 première is first
 première is slightly different
 Кириллица is Cyrillic
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 045bb42..5c5ebd5 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
@@ -148,6 +148,16 @@
     }
 
     @Test
+    public void shouldTransformMultipleTemplatesAndDataSources() throws IOException {
+        final String output = execute(
+                "-t freemarker-generator/yaml/json/transform.ftl -s src/app/examples/data/yaml/swagger-spec.yaml -o swagger-spec.json " +
+                        "-t freemarker-generator/yaml/json/transform.ftl -s src/app/examples/data/yaml/customer.yaml -o customer.json");
+
+        assertTrue("Swagger file content is missing", output.contains("This is a sample server Petstore server"));
+        assertTrue("Customer data is missing", output.contains("Xyz, DEF Street"));
+    }
+
+    @Test
     public void shouldSupportDataSourcesAccessInFTL() throws IOException {
         final String args = "users=src/app/examples/data/json/github-users.json contract=src/app/examples/data/csv/contract.csv";
 
@@ -161,22 +171,6 @@
         assertEquals("application/json", execute(args + " -i ${dataSources[\"users\"].mimeType}"));
     }
 
-    /**
-     * @Test public void shouldNotShadowDataSourcesInFTL() throws IOException {
-     * final String args = "empty=examples/data/json/github-users.json";
-     * <p>
-     * // check shadowing of "isEmpty"
-     * assertEquals("false", execute("empty=examples/data/json/github-users.json -i ${dataSources.empty?c}"));
-     * // DataSources#isEmpty shadows the data source "empty"
-     * // assertEquals("false", execute("empty=examples/data/json/github-users.json -i ${DataSources[\"empty\"]}"));
-     * assertEquals("empty", execute("empty=examples/data/json/github-users.json -i ${dataSources.get(\"empty\").name}"));
-     * <p>
-     * // check shadowing of "find"
-     * // assertEquals("find", execute("find=examples/data/json/github-users.json -i ${dataSources.find.name}"));
-     * // assertEquals("find", execute("find=examples/data/json/github-users.json -i ${DataSources[\"find\"].name}"));
-     * }
-     */
-
     @Test
     @Ignore("Manual test to check memory consumption and resource handling")
     public void shouldCloseAllResources() 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 34077d5..4a3a5b8 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
@@ -16,17 +16,19 @@
  */
 package org.apache.freemarker.generator.cli;
 
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
 import org.junit.Test;
 import picocli.CommandLine;
-import picocli.CommandLine.ParameterException;
+
+import java.util.List;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 public class PicocliTest {
 
     private static final String ANY_TEMPLATE = "any.ftl";
-    private static final String OTHER_TEMPLATE = "other.ftl";
     private static final String INTERACTIVE_TEMPLATE = "interactive-template";
     private static final String ANY_FILE = "users.csv";
     private static final String OTHER_FILE = "transctions.csv";
@@ -35,52 +37,64 @@
 
     @Test
     public void shouldParseSinglePositionalParameter() {
-        assertEquals(ANY_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE_URI).sources.get(0));
-        assertNull(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE_URI).dataSources);
+        final Main main = parse("-t", ANY_TEMPLATE, ANY_FILE_URI);
+
+        assertEquals(1, main.outputGeneratorDefinitions.size());
+        assertEquals(ANY_FILE_URI, main.sharedDataSources.get(0));
     }
 
     @Test
     public void shouldParseMultiplePositionalParameter() {
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE).sources.get(0));
-        assertEquals(OTHER_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE).sources.get(1));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE).sharedDataSources.get(0));
+        assertEquals(OTHER_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE).sharedDataSources.get(1));
 
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE_URI).sources.get(0));
-        assertEquals(OTHER_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE_URI).sources.get(1));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE_URI).sharedDataSources.get(0));
+        assertEquals(OTHER_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE, OTHER_FILE_URI).sharedDataSources.get(1));
 
-        assertEquals(ANY_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sources.get(0));
-        assertEquals(OTHER_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sources.get(1));
+        assertEquals(ANY_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sharedDataSources.get(0));
+        assertEquals(OTHER_FILE_URI, parse("-t", ANY_TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sharedDataSources.get(1));
     }
 
     @Test
     public void shouldParseSingleNamedDataSource() {
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, ANY_FILE).sources.get(0));
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "-s", ANY_FILE).dataSources.get(0));
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "--data-source", ANY_FILE).dataSources.get(0));
-        assertEquals(ANY_FILE_URI, parse("-t", ANY_TEMPLATE, "--data-source", ANY_FILE_URI).dataSources.get(0));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "-s", ANY_FILE).outputGeneratorDefinitions.get(0)
+                .getDataSources()
+                .get(0));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "--data-source", ANY_FILE).outputGeneratorDefinitions.get(0)
+                .getDataSources()
+                .get(0));
+        assertEquals(ANY_FILE_URI, parse("-t", ANY_TEMPLATE, "--data-source", ANY_FILE_URI).outputGeneratorDefinitions.get(0)
+                .getDataSources()
+                .get(0));
     }
 
     @Test
-    public void shouldParseMultipleNamedDataSource() {
+    public void shouldParseMultipleNamedDataSources() {
         final Main main = parse("-t", ANY_TEMPLATE, "-s", ANY_FILE, "--data-source", OTHER_FILE_URI);
 
-        assertEquals(ANY_FILE, main.dataSources.get(0));
-        assertEquals(OTHER_FILE_URI, main.dataSources.get(1));
-        assertNull(main.sources);
+        assertEquals(ANY_FILE, main.outputGeneratorDefinitions.get(0).getDataSources().get(0));
+        assertEquals(OTHER_FILE_URI, main.outputGeneratorDefinitions.get(0).getDataSources().get(1));
+        assertNull(main.sharedDataSources);
     }
 
     @Test
     public void shouldParseSingleDataModel() {
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "-m", ANY_FILE).dataModels.get(0));
-        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "--data-model", ANY_FILE).dataModels.get(0));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "-m", ANY_FILE).outputGeneratorDefinitions.get(0)
+                .getDataModels()
+                .get(0));
+        assertEquals(ANY_FILE, parse("-t", ANY_TEMPLATE, "--data-model", ANY_FILE).outputGeneratorDefinitions.get(0)
+                .getDataModels()
+                .get(0));
     }
 
     @Test
     public void shouldParseMultipleDataModels() {
         final Main main = parse("-t", ANY_TEMPLATE, "-m", ANY_FILE, "--data-model", OTHER_FILE_URI);
+        final OutputGeneratorDefinition outputGeneratorDefinition = main.outputGeneratorDefinitions.get(0);
 
-        assertEquals(ANY_FILE, main.dataModels.get(0));
-        assertEquals(OTHER_FILE_URI, main.dataModels.get(1));
-        assertNull(main.sources);
+        assertEquals(ANY_FILE, outputGeneratorDefinition.getDataModels().get(0));
+        assertEquals(OTHER_FILE_URI, outputGeneratorDefinition.getDataModels().get(1));
+        assertNull(main.sharedDataSources);
     }
 
     @Test
@@ -102,25 +116,64 @@
     public void shouldParseSingleTemplate() {
         final Main main = parse("-t", ANY_TEMPLATE);
 
-        assertEquals(ANY_TEMPLATE, main.templateSourceOptions.templates.get(0));
+        assertEquals(ANY_TEMPLATE, main.outputGeneratorDefinitions.get(0).templateSourceDefinition.template);
     }
 
     @Test
     public void shouldParseInteractiveTemplate() {
         final Main main = parse("-i", INTERACTIVE_TEMPLATE);
 
-        assertEquals(INTERACTIVE_TEMPLATE, main.templateSourceOptions.interactiveTemplate);
+        assertEquals(INTERACTIVE_TEMPLATE, main.outputGeneratorDefinitions.get(0).templateSourceDefinition.interactiveTemplate);
     }
 
-    @Test(expected = ParameterException.class)
-    public void shouldThrowParameterExceptionForMismatchedTemplateOutput() {
-        final Main main = parse("-t", "foo.ftl", "-t", "bar.ftl", "-o", "foo.out");
+    @Test
+    public void shouldParseMultipleTemplates() {
+        final Main main = parse("-t", ANY_TEMPLATE, "--template", ANY_TEMPLATE);
+
+        assertEquals(2, main.outputGeneratorDefinitions.size());
+    }
+
+    @Test
+    public void shouldParseStdin() {
+        final Main main = parse("-t", ANY_TEMPLATE, "--stdin");
+
+        assertTrue(main.readFromStdin);
+    }
+
+    @Test
+    public void shouldParseComplexCommandLine01() {
+        final Main main = parse(
+                "--template", "template01.ftl", "--data-source", "datasource10.csv",
+                "-t", "template02.ftl", "-s", "datasource20.csv", "-s", "datasource21.csv",
+                "-i", "some-interactive-template01", "-s", "datasource30.csv", "-o", "out.txt",
+                "-i", "some-interactive-template02");
 
         main.validate();
+
+        final List<OutputGeneratorDefinition> defs = main.outputGeneratorDefinitions;
+        assertEquals(4, defs.size());
+
+        assertEquals("template01.ftl", defs.get(0).templateSourceDefinition.template);
+        assertEquals(1, defs.get(0).dataSourceDefinition.dataSources.size());
+        assertEquals("datasource10.csv", defs.get(0).dataSourceDefinition.dataSources.get(0));
+        assertNull(defs.get(0).templateOutputDefinition);
+
+        assertEquals("template02.ftl", defs.get(1).templateSourceDefinition.template);
+        assertEquals(2, defs.get(1).dataSourceDefinition.dataSources.size());
+        assertEquals("datasource20.csv", defs.get(1).dataSourceDefinition.dataSources.get(0));
+        assertEquals("datasource21.csv", defs.get(1).dataSourceDefinition.dataSources.get(1));
+        assertNull(defs.get(0).templateOutputDefinition);
+
+        assertEquals("some-interactive-template01", defs.get(2).templateSourceDefinition.interactiveTemplate);
+        assertEquals(1, defs.get(2).dataSourceDefinition.dataSources.size());
+        assertEquals("datasource30.csv", defs.get(2).dataSourceDefinition.dataSources.get(0));
+        assertEquals("out.txt", defs.get(2).templateOutputDefinition.outputs.get(0));
+
+        assertEquals("some-interactive-template02", defs.get(3).templateSourceDefinition.interactiveTemplate);
     }
 
     private static Main parse(String... args) {
-        final Main main = new Main();
+        final Main main = new Main(args);
         new CommandLine(main).parseArgs(args);
         return main;
     }
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 93ddfa2..67721b9 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
@@ -51,7 +51,7 @@
     }
 
     private ConfigurationSupplier configurationSupplier(Settings settings) {
-        return new ConfigurationSupplier(settings, templateLoaderSupplier());
+        return new ConfigurationSupplier(settings, templateLoaderSupplier(), Suppliers.toolsSupplier(settings));
     }
 
     private TemplateLoaderSupplier templateLoaderSupplier() {
@@ -60,7 +60,6 @@
 
     private SettingsBuilder settingsBuilder() {
         return Settings.builder()
-                .setTemplateNames(singletonList(ANY_TEMPLATE_NAME))
-                .setWriter(new StringWriter());
+                .setCallerSuppliedWriter(new StringWriter());
     }
 }
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
deleted file mode 100644
index cbd838a..0000000
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.config.Settings.SettingsBuilder;
-import org.junit.Test;
-
-import java.io.StringWriter;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
-import static java.util.Collections.singletonList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-public class SettingsTest {
-
-    private static final String[] ANY_ARGS = singletonList("arg1 arg2").toArray(new String[0]);
-    private static final Properties ANY_CONFIGURATION = new Properties();
-    private static final String ANY_INCLUDE = "*.csv";
-    private static final String ANY_INPUT_ENCODING = "US-ASCII";
-    private static final String ANY_INTERACTIVE_TEMPLATE = "interactiveTemplate";
-    private static final String ANY_LOCALE = "th-TH";
-    private static final String ANY_OUTPUT_ENCODING = "UTF-16";
-    private static final String ANY_OUTPUT_FILE = "outputFile";
-    private static final List<String> ANY_SOURCES = singletonList("sources");
-    private static final String ANY_TEMPLATE_NAME = "templateName";
-    private static final Map<String, Object> ANY_USER_PARAMETERS = new HashMap<>();
-    private static final Properties ANY_SYSTEM_PROPERTIES = new Properties();
-
-    @Test
-    public void shouldProvideAllExpectedSettings() {
-        final Settings settings = allSettingsBuilder().build();
-
-        assertEquals(1, settings.getArgs().size());
-        assertNotNull(settings.getConfiguration());
-        assertEquals(ANY_INCLUDE, settings.getDataSourceIncludePattern());
-        assertEquals(ANY_INPUT_ENCODING, settings.getInputEncoding().name());
-        assertEquals(ANY_OUTPUT_ENCODING, settings.getOutputEncoding().name());
-        assertEquals(ANY_OUTPUT_FILE, settings.getOutputs().get(0));
-        assertEquals(ANY_TEMPLATE_NAME, settings.getTemplates().get(0));
-        assertNotNull(settings.getDataSources());
-        assertNotNull(settings.getUserParameters());
-        assertNotNull(settings.getUserSystemProperties());
-        assertTrue(settings.isReadFromStdin());
-        assertTrue(settings.isInteractiveTemplate());
-        assertTrue(settings.isVerbose());
-    }
-
-    private SettingsBuilder allSettingsBuilder() {
-        return Settings.builder()
-                .isReadFromStdin(true)
-                .setArgs(ANY_ARGS)
-                .setConfiguration(ANY_CONFIGURATION)
-                .setDataSourceIncludePattern(ANY_INCLUDE)
-                .setInputEncoding(ANY_INPUT_ENCODING)
-                .setInteractiveTemplate(ANY_INTERACTIVE_TEMPLATE)
-                .setLocale(ANY_LOCALE)
-                .setOutputEncoding(ANY_OUTPUT_ENCODING)
-                .setOutputs(Collections.singletonList(ANY_OUTPUT_FILE))
-                .setParameters(ANY_USER_PARAMETERS)
-                .setDataSources(ANY_SOURCES)
-                .setSystemProperties(ANY_SYSTEM_PROPERTIES)
-                .setTemplateNames(singletonList(ANY_TEMPLATE_NAME))
-                .setWriter(new StringWriter())
-                .setVerbose(true);
-    }
-}
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
new file mode 100644
index 0000000..c59c955
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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 freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.freemarker.generator.base.FreeMarkerConstants.Model;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourcesSupplier;
+import org.apache.freemarker.generator.base.output.OutputGenerator;
+import org.apache.freemarker.generator.base.template.TemplateSource.Origin;
+import org.apache.freemarker.generator.base.util.OperatingSystem;
+import org.apache.freemarker.generator.cli.picocli.DataModelDefinition;
+import org.apache.freemarker.generator.cli.picocli.DataSourceDefinition;
+import org.apache.freemarker.generator.cli.picocli.OutputGeneratorDefinition;
+import org.apache.freemarker.generator.cli.picocli.TemplateSourceDefinition;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@SuppressWarnings("unchecked")
+public class SuppliersTest {
+
+    private static final String ANY_DATA_MODEL_NAME = "src/test/data/properties/test.properties";
+    private static final String ANY_DATA_SOURCE_NAME = "src/test/data/properties/test.properties";
+    private static final String ANY_INTERACTIVE_TEMPLATE = "Hello World";
+    private static final String ANY_TEMPLATE_DIRECTORY_NAME = "src/test/templates";
+    private static final String ANY_TEMPLATE_NAME = "echo.ftl";
+    private static final List<File> ANY_TEMPLATE_DIRECTORIES = singletonList(new File(ANY_TEMPLATE_DIRECTORY_NAME));
+
+    @Test
+    public void shouldCreateTemplateDirectorySupplier() {
+        final TemplateDirectorySupplier templateDirectorySupplier = Suppliers.templateDirectorySupplier(ANY_TEMPLATE_DIRECTORY_NAME);
+
+        final List<File> files = templateDirectorySupplier.get();
+
+        assertTrue(files.get(0).getAbsolutePath().endsWith(fixSeparators(ANY_TEMPLATE_DIRECTORY_NAME)));
+    }
+
+    @Test
+    public void shouldCreateTools() {
+        final Properties configuration = new Properties();
+        configuration.setProperty("freemarker.tools.system", "org.apache.freemarker.generator.tools.system.SystemTool");
+        final Settings settings = Settings.builder().setConfiguration(configuration).build();
+        final ToolsSupplier toolsSupplier = Suppliers.toolsSupplier(settings);
+
+        final Map<String, Object> tools = (Map<String, Object>) toolsSupplier.get().get(Model.TOOLS);
+
+        assertEquals(1, tools.size());
+        assertNotNull(tools.get("system"));
+    }
+
+    @Test
+    public void shouldCreateTemplateLoaderSupplier() throws IOException {
+        final Settings settings = Settings.builder().setTemplateDirectories(ANY_TEMPLATE_DIRECTORIES).build();
+        final TemplateLoaderSupplier templateLoaderSupplier = Suppliers.templateLoaderSupplier(settings);
+
+        final TemplateLoader templateLoader = templateLoaderSupplier.get();
+
+        assertNotNull(templateLoader.findTemplateSource(ANY_TEMPLATE_NAME));
+    }
+
+    @Test
+    public void shouldCreateConfiguration() {
+        final Settings settings = Settings.builder().build();
+        final ConfigurationSupplier configurationSupplier = Suppliers.configurationSupplier(settings);
+
+        final Configuration configuration = configurationSupplier.get();
+
+        assertNotNull(configuration.getSharedVariable(Model.TOOLS));
+        assertTrue(configuration.isTemplateLoaderExplicitlySet());
+        assertTrue(configuration.isObjectWrapperExplicitlySet());
+    }
+
+    @Test
+    public void shouldCreateSharedDataSources() {
+        final Settings settings = Settings.builder().setSharedDataSources(singletonList(ANY_DATA_SOURCE_NAME)).build();
+        final DataSourcesSupplier dataSourcesSupplier = Suppliers.sharedDataSourcesSupplier(settings);
+
+        final List<DataSource> dataSourceList = dataSourcesSupplier.get();
+
+        assertEquals(1, dataSourceList.size());
+        assertTrue(dataSourceList.get(0).getName().endsWith(ANY_DATA_SOURCE_NAME));
+    }
+
+    @Test
+    public void shouldCreateSharedDataModel() {
+        final Settings settings = Settings.builder().setSharedDataModels(singletonList(ANY_DATA_MODEL_NAME)).build();
+        final DataModelSupplier sharedDataModelSupplier = Suppliers.sharedDataModelSupplier(settings);
+
+        final Map<String, Object> map = sharedDataModelSupplier.get();
+
+        assertEquals(2, map.size());
+    }
+
+    @Test
+    public void shouldCreateOutputGenerator() {
+        final OutputGeneratorDefinition def = new OutputGeneratorDefinition();
+        def.templateSourceDefinition = new TemplateSourceDefinition();
+        def.templateSourceDefinition.interactiveTemplate = ANY_INTERACTIVE_TEMPLATE;
+        def.dataModelDefinition = new DataModelDefinition();
+        def.dataModelDefinition.dataModels = singletonList(ANY_DATA_MODEL_NAME);
+        def.dataSourceDefinition = new DataSourceDefinition();
+        def.dataSourceDefinition.dataSources = singletonList(ANY_DATA_SOURCE_NAME);
+        final Settings settings = Settings.builder().setOutputGeneratorDefinitions(singletonList(def)).build();
+        final OutputGeneratorsSupplier outputGeneratorsSupplier = Suppliers.outputGeneratorsSupplier(settings);
+
+        final List<OutputGenerator> outputGenerators = outputGeneratorsSupplier.get();
+        final OutputGenerator outputGenerator = outputGenerators.get(0);
+
+        assertEquals(1, outputGenerators.size());
+        assertEquals(Origin.TEMPLATE_CODE, outputGenerator.getTemplateSource().getOrigin());
+        assertEquals(1, outputGenerator.getDataSources().size());
+        assertEquals(2, outputGenerator.getVariables().size());
+        assertEquals("foo", outputGenerator.getVariables().get("FOO"));
+        assertNotNull(outputGenerator.getTemplateOutput().getWriter());
+        assertNull(outputGenerator.getTemplateOutput().getFile());
+    }
+    private static String fixSeparators(String str) {
+        if (OperatingSystem.isWindows()) {
+            return FilenameUtils.separatorsToWindows(str);
+        } else {
+            return str;
+        }
+    }
+
+}
diff --git a/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OperatingSystem.java b/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OperatingSystem.java
index 9c974c2..4575238 100644
--- a/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OperatingSystem.java
+++ b/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OperatingSystem.java
@@ -1,3 +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.
+ */
 package org.apache.freemarker.generator.maven;
 
 import java.util.Locale;
diff --git a/freemarker-generator-tools/pom.xml b/freemarker-generator-tools/pom.xml
index 97a2dfd..635b9c1 100644
--- a/freemarker-generator-tools/pom.xml
+++ b/freemarker-generator-tools/pom.xml
@@ -109,7 +109,7 @@
         <dependency>
             <groupId>com.jayway.jsonpath</groupId>
             <artifactId>json-path</artifactId>
-            <version>2.4.0</version>
+            <version>2.5.0</version>
         </dependency>
         <!-- JsoupTool -->
         <dependency>
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 552ae16..3490ee8 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
@@ -50,7 +50,7 @@
     public List<CSVParser> parse(Collection<DataSource> dataSources) {
         return dataSources.stream()
                 .map(this::parse)
-                .collect(Collectors.toList());
+                .collect(toList());
     }
 
     public CSVParser parse(DataSource dataSource, CSVFormat format) {
diff --git a/licences/LICENCE_javafaker.txt b/licences/LICENCE_javafaker.txt
new file mode 100644
index 0000000..896c78c
--- /dev/null
+++ b/licences/LICENCE_javafaker.txt
@@ -0,0 +1,13 @@
+Copyright 2014 DiUS Computing
+
+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