FREEMARKER-142 Support Transformation Of Directories (#13)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2846d0a..b610dbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,9 @@
 ## 0.1.0-SNAPSHOT
 
 ### Added
+* [FREEMARKER-142] Support Transformation Of Directories
 * [FREEMARKER-139] freemarker-cli: Provide GsonTool to align with Maven plugin
-* An environment variable can bes passed as `DataSource`
+* Environment variables can bes passed as `DataSource`
 * [FREEMARKER-135] Support user-supplied names for `DataSource` on the command line
 * [FREEMARKER-129] Support `DataSource` exclude pattern in addition to include pattern
 * [FREEMARKER-129] User-defined parameters are passed as `-Pkey=value` instead of using system properties
@@ -36,4 +37,5 @@
 [FREEMARKER-135]: https://issues.apache.org/jira/browse/FREEMARKER-135
 [FREEMARKER-136]: https://issues.apache.org/jira/browse/FREEMARKER-136
 [FREEMARKER-138]: https://issues.apache.org/jira/browse/FREEMARKER-138
-[FREEMARKER-139]: https://issues.apache.org/jira/browse/FREEMARKER-139
\ No newline at end of file
+[FREEMARKER-139]: https://issues.apache.org/jira/browse/FREEMARKER-139
+[FREEMARKER-142]: https://issues.apache.org/jira/browse/FREEMARKER-142
\ No newline at end of file
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/ByteArrayDataSource.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/ByteArrayDataSource.java
index 196b254..c5c0a9d 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/ByteArrayDataSource.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/ByteArrayDataSource.java
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.freemarker.generator.base.activation;
 
 import javax.activation.DataSource;
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
index 016a394..df33351 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceFactory.java
@@ -49,7 +49,7 @@
 /**
  * Creates a FreeMarker data source from various sources.
  */
-public class DataSourceFactory {
+public abstract class DataSourceFactory {
 
     private static final String NO_MIME_TYPE = null;
     private static final Charset NO_CHARSET = null;
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 097e17d..ef60fe7 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
@@ -71,6 +71,7 @@
         return sources.stream()
                 .map(this::resolve)
                 .flatMap(Collection::stream)
+                .sorted()
                 .collect(toList());
     }
 
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
new file mode 100644
index 0000000..1e5cb50
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateOutput.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util.Validate;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Information about where to write the ouput of a template. Initially we
+ * wanted to use a <code>FileWriter</code> but it requires actually an
+ * existing output file (otherwise a FileNotFound exception is thrown).
+ */
+public class TemplateOutput {
+
+    private final Writer writer;
+    private final File file;
+
+    private TemplateOutput(File file) {
+        this.writer = null;
+        this.file = requireNonNull(file);
+    }
+
+    private TemplateOutput(Writer writer) {
+        this.writer = requireNonNull(writer);
+        this.file = null;
+    }
+
+    public static TemplateOutput fromWriter(Writer writer) {
+        return new TemplateOutput(writer);
+    }
+
+    public static TemplateOutput fromFile(File file) {
+        return new TemplateOutput(file);
+    }
+
+    public Writer getWriter() {
+        return writer;
+    }
+
+    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();
+    }
+
+    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
new file mode 100644
index 0000000..42c36b6
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSource.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.base.template;
+
+import org.apache.freemarker.generator.base.util.Validate;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Information how to load a template. The template is either
+ * provided as source code or as template path resolved
+ * by FreeMarker's template loader.
+ */
+public class TemplateSource {
+
+    public enum Origin {
+        PATH,
+        CODE
+    }
+
+    /** Name of template for diagnostics */
+    private final String name;
+
+    /** Origin of template, e.g. loaded via FreeMarker's template loader */
+    private final Origin origin;
+
+    /** Code of the template */
+    private final String code;
+
+    /** Template path */
+    private final String path;
+
+    /** Template encoding */
+    private final Charset encoding;
+
+    private TemplateSource(String name, String code) {
+        this.name = name;
+        this.origin = Origin.CODE;
+        this.code = code;
+        this.path = null;
+        this.encoding = StandardCharsets.UTF_8;
+    }
+
+    private TemplateSource(String name, String path, Charset encoding) {
+        this.name = name;
+        this.origin = Origin.PATH;
+        this.code = null;
+        this.path = path;
+        this.encoding = encoding;
+    }
+
+    public static TemplateSource fromPath(String path) {
+        Validate.notEmpty(path, "Template path is empty");
+        return new TemplateSource(path, path, StandardCharsets.UTF_8);
+    }
+
+    public static TemplateSource fromPath(String path, Charset encoding) {
+        Validate.notEmpty(path, "Template path is empty");
+        Validate.notNull(encoding, "Template encoding is null");
+        return new TemplateSource(path, path, encoding);
+    }
+
+    public static TemplateSource fromCode(String name, String code) {
+        Validate.notEmpty(name, "Template name is empty");
+        Validate.notEmpty(code, "Template code is empty");
+        return new TemplateSource(name, code);
+    }
+
+    public Origin getOrigin() {
+        return origin;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Charset getEncoding() {
+        return encoding;
+    }
+
+    @Override
+    public String toString() {
+        return "TemplateSource{" +
+                "name='" + name + '\'' +
+                ", origin=" + origin +
+                ", encoding=" + encoding +
+                '}';
+    }
+}
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
new file mode 100644
index 0000000..ab53ee3
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateSourceFactory.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.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
new file mode 100644
index 0000000..b13a799
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformation.java
@@ -0,0 +1,44 @@
+/*
+ * 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 static java.util.Objects.requireNonNull;
+
+/**
+ * Information about loading templates and writing their output.
+ */
+public class TemplateTransformation {
+
+    /** Source of template */
+    private final TemplateSource templateSource;
+
+    /** Output of template */
+    private final TemplateOutput templateOutput;
+
+    public TemplateTransformation(TemplateSource templateSource, TemplateOutput templateOutput) {
+        this.templateSource = requireNonNull(templateSource);
+        this.templateOutput = requireNonNull(templateOutput);
+    }
+
+    public TemplateSource getTemplateSource() {
+        return templateSource;
+    }
+
+    public TemplateOutput getTemplateOutput() {
+        return 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
new file mode 100644
index 0000000..ed65111
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformations.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+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
new file mode 100644
index 0000000..37100aa
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
@@ -0,0 +1,303 @@
+/*
+ * 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.file.RecursiveFileSupplier;
+import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
+import org.apache.freemarker.generator.base.util.StringUtils;
+import org.apache.freemarker.generator.base.util.Validate;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+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.
+ */
+public class TemplateTransformationsBuilder {
+
+    /** Interactive template */
+    private TemplateSource template;
+
+    /** List of templates and/or template directories to be rendered */
+    private final List<String> sources;
+
+    /** Optional include patterns for resolving source templates or template directories */
+    private final List<String> includes;
+
+    /** Optional exclude patterns for resolving source templates or template directories */
+    private final List<String> excludes;
+
+    /** Optional output file or directory */
+    private final List<File> outputs;
+
+    /** Optional user-supplied writer */
+    private Writer writer;
+
+    private TemplateTransformationsBuilder() {
+        this.sources = new ArrayList<>();
+        this.includes = new ArrayList<>();
+        this.excludes = new ArrayList<>();
+        this.outputs = new ArrayList<>();
+        this.writer = null;
+    }
+
+    public static TemplateTransformationsBuilder builder() {
+        return new TemplateTransformationsBuilder();
+    }
+
+    public TemplateTransformations build() {
+        validate();
+
+        final List<TemplateTransformation> result = new ArrayList<>();
+
+        if (template != null) {
+            final File outputFile = outputs.isEmpty() ? null : outputs.get(0);
+            result.add(resolveInteractiveTemplate(outputFile));
+        } else {
+            for (int i = 0; i < sources.size(); i++) {
+                final String source = sources.get(i);
+                final File output = i < outputs.size() ? outputs.get(i) : null;
+                result.addAll(resolve(source, output));
+            }
+        }
+
+        return new TemplateTransformations(result);
+    }
+
+    public TemplateTransformationsBuilder setTemplate(String name, String code) {
+        if (StringUtils.isNotEmpty(code)) {
+            this.template = TemplateSource.fromCode(name, 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);
+        return this;
+    }
+
+    public TemplateTransformationsBuilder addInclude(String include) {
+        if (StringUtils.isNotEmpty(include)) {
+            this.includes.add(include);
+        }
+        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);
+        }
+        return this;
+    }
+
+    public TemplateTransformationsBuilder addExcludes(Collection<String> excludes) {
+        if (excludes != null) {
+            this.excludes.addAll(excludes);
+        }
+        return this;
+    }
+
+    public TemplateTransformationsBuilder addOutput(String output) {
+        if (StringUtils.isNotEmpty(output)) {
+            this.outputs.add(new File(output));
+        }
+        return this;
+    }
+
+    public TemplateTransformationsBuilder addOutput(File output) {
+        if (output != null) {
+            this.outputs.add(output);
+        }
+        return this;
+    }
+
+    public TemplateTransformationsBuilder addOutputs(Collection<String> outputs) {
+        if (outputs != null) {
+            outputs.forEach(this::addOutput);
+        }
+        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)));
+        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");
+    }
+
+    /**
+     * Resolve a <code>source</code> to a list of <code>TemplateTransformation</code>.
+     *
+     * @param source the source being a file name, an URI or <code>NamedUri</code>
+     * @param output Optional output file or directory
+     * @return list of <code>TemplateTransformation</code>
+     */
+    private List<TemplateTransformation> resolve(String source, File output) {
+        if (isTemplateFile(source)) {
+            return resolveTemplateFile(source, output);
+        } else if (isTemplateDirectory(source)) {
+            return resolveTemplateDirectory(source, output);
+        } else if (isTemplatePath(source)) {
+            return resolveTemplatePath(source, output);
+        } else {
+            return resolveTemplateCode(source, output);
+        }
+    }
+
+    private List<TemplateTransformation> resolveTemplateFile(String source, File outputFile) {
+        final TemplateSource templateSource = templateSource(source);
+        final TemplateOutput templateOutput = templateOutput(outputFile);
+        return singletonList(new TemplateTransformation(templateSource, templateOutput));
+    }
+
+    private List<TemplateTransformation> resolveTemplateDirectory(String source, File outputDirectory) {
+        Validate.fileExists(new File(source), "Template directory does not exist: " + source);
+
+        final File templateDirectory = new File(source);
+        final List<File> templateFiles = templateFilesSupplier(source, getInclude(), getExclude()).get();
+        final List<TemplateTransformation> templateTransformations = new ArrayList<>();
+
+        for (File templateFile : templateFiles) {
+            final TemplateSource templateSource = templateSource(templateFile.getAbsolutePath());
+            final File outputFile = getTemplateOutputFile(templateDirectory, templateFile, outputDirectory);
+            final TemplateOutput templateOutput = templateOutput(outputFile);
+            templateTransformations.add(new TemplateTransformation(templateSource, templateOutput));
+        }
+
+        return templateTransformations;
+    }
+
+    private List<TemplateTransformation> resolveTemplatePath(String source, File out) {
+        final TemplateSource templateSource = templateSource(source);
+        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("interactive", source);
+        final TemplateOutput templateOutput = templateOutput(out);
+        return singletonList(new TemplateTransformation(templateSource, templateOutput));
+    }
+
+    private TemplateOutput templateOutput(File templateOutputFile) {
+        if (writer == null && templateOutputFile != null) {
+            return TemplateOutput.fromFile(templateOutputFile);
+        } else {
+            return TemplateOutput.fromWriter(writer);
+        }
+    }
+
+    private TemplateSource templateSource(String source) {
+        return TemplateSourceFactory.create(source);
+    }
+
+    private String getInclude() {
+        return includes.isEmpty() ? null : includes.get(0);
+    }
+
+    private String getExclude() {
+        return excludes.isEmpty() ? null : excludes.get(0);
+    }
+
+    private Writer writer(String outputFile, String outputEncoding) {
+        try {
+            if (writer != null) {
+                return writer;
+            } else if (!StringUtils.isEmpty(outputFile)) {
+                return new BufferedWriter(new FileWriter(outputFile));
+            } else {
+                return new BufferedWriter(new OutputStreamWriter(System.out, outputEncoding));
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Unable to create writer", e);
+        }
+    }
+
+    private static File getTemplateOutputFile(File templateDirectory, File templateFile, File outputDirectory) {
+        final String relativePath = relativePath(templateDirectory, templateFile);
+        final String relativeOutputFileName = mapExtension(relativePath);
+        return new File(outputDirectory, relativeOutputFileName);
+    }
+
+    private static boolean isTemplateFile(String source) {
+        final File file = new File(source);
+        return file.exists() && file.isFile();
+    }
+
+    private static boolean isTemplateDirectory(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 RecursiveFileSupplier templateFilesSupplier(String source, String include, String exclude) {
+        return new RecursiveFileSupplier(singletonList(source), singletonList(include), singletonList(exclude));
+    }
+
+    private static String relativePath(File directory, File file) {
+        return file.getAbsolutePath()
+                .substring(directory.getAbsolutePath().length())
+                .substring(1);
+    }
+
+    private static String mapExtension(String fileName) {
+        if (fileName.toLowerCase().endsWith(".ftl")) {
+            return fileName.substring(0, fileName.length() - 4);
+        } else {
+            return fileName;
+        }
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
new file mode 100644
index 0000000..ef06672
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsSupplier.java
@@ -0,0 +1,22 @@
+/*
+ * 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.function.Supplier;
+
+public interface TemplateTransformationsSupplier extends Supplier<TemplateTransformations> {
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ClosableUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ClosableUtils.java
index dfefc01..0d04517 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ClosableUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ClosableUtils.java
@@ -26,8 +26,8 @@
             if (closeable != null) {
                 closeable.close();
             }
-        } catch (final IOException e) {
-            // e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
         }
     }
 }
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
index 3800add..80f5dd3 100644
--- 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
@@ -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.base.util;
 
 import java.util.Iterator;
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java
index 71bd28a..af2b497 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java
@@ -16,6 +16,8 @@
  */
 package org.apache.freemarker.generator.base.util;
 
+import java.io.File;
+
 /**
  * Simple validation methods designed for interal use.
  */
@@ -140,6 +142,18 @@
     }
 
     /**
+     * Validates that the file exists
+     *
+     * @param file the file to test
+     * @param msg  message to output if validation fails
+     */
+    public static void fileExists(File file, String msg) {
+        if (file == null || !file.exists()) {
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
      * Cause a failure.
      *
      * @param msg message to output.
@@ -147,4 +161,5 @@
     public static void fail(String msg) {
         throw new IllegalArgumentException(msg);
     }
+
 }
diff --git a/freemarker-generator-base/src/test/data/env/nginx.env b/freemarker-generator-base/src/test/data/env/nginx.env
new file mode 100644
index 0000000..111461e
--- /dev/null
+++ b/freemarker-generator-base/src/test/data/env/nginx.env
@@ -0,0 +1,19 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+NGINX_HOSTNAME=localhost
+NGINX_WEBROOT=/var/www/project
+NGINX_LOGS=/var/log/nginx/
\ No newline at end of file
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesSupplierTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesSupplierTest.java
index 8089c48..aacfd75 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesSupplierTest.java
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesSupplierTest.java
@@ -44,7 +44,8 @@
         assertEquals(1, supplier("pom=./pom.xml", "*", NO_EXCLUDE).get().size());
         assertEquals(1, supplier("pom=./pom.xml#mimetype=application/xml", "*", NO_EXCLUDE).get().size());
         assertEquals(1, supplier("pom=" + PWD + "/pom.xml", "*", NO_EXCLUDE).get().size());
-        assertEquals(1, supplier("pom=file:///" + PWD + "/pom.xml#mimetype=application/xml", "*", NO_EXCLUDE).get().size());
+        assertEquals(1, supplier("pom=file:///" + PWD + "/pom.xml#mimetype=application/xml", "*", NO_EXCLUDE).get()
+                .size());
     }
 
     @Test
@@ -62,10 +63,10 @@
 
     @Test
     public void shouldResolveDirectory() {
-        assertEquals(4, supplier(DATA_DIRECTORY, null, null).get().size());
-        assertEquals(4, supplier(DATA_DIRECTORY, "", null).get().size());
-        assertEquals(4, supplier(DATA_DIRECTORY, "*", null).get().size());
-        assertEquals(4, supplier(DATA_DIRECTORY, "*.*", null).get().size());
+        assertEquals(5, supplier(DATA_DIRECTORY, null, null).get().size());
+        assertEquals(5, supplier(DATA_DIRECTORY, "", null).get().size());
+        assertEquals(5, supplier(DATA_DIRECTORY, "*", null).get().size());
+        assertEquals(5, supplier(DATA_DIRECTORY, "*.*", null).get().size());
         assertEquals(2, supplier(DATA_DIRECTORY, "*.csv", null).get().size());
         assertEquals(1, supplier(DATA_DIRECTORY, "*.t*", null).get().size());
         assertEquals(0, supplier(DATA_DIRECTORY, "*.bin", null).get().size());
@@ -75,10 +76,10 @@
     public void shouldResolveFilesAndDirectory() {
         final List<String> sources = Arrays.asList("pom.xml", "README.md", DATA_DIRECTORY);
 
-        assertEquals(6, supplier(sources, null, null).get().size());
-        assertEquals(6, supplier(sources, "", null).get().size());
-        assertEquals(6, supplier(sources, "*", null).get().size());
-        assertEquals(6, supplier(sources, "*.*", null).get().size());
+        assertEquals(7, supplier(sources, null, null).get().size());
+        assertEquals(7, supplier(sources, "", null).get().size());
+        assertEquals(7, supplier(sources, "*", null).get().size());
+        assertEquals(7, supplier(sources, "*.*", null).get().size());
         assertEquals(2, supplier(sources, "*.csv", null).get().size());
         assertEquals(1, supplier(sources, "*.t*", null).get().size());
         assertEquals(1, supplier(sources, "*.xml", null).get().size());
@@ -87,8 +88,9 @@
         assertEquals(0, supplier(sources, null, "*").get().size());
         assertEquals(0, supplier(sources, null, "*.*").get().size());
         assertEquals(0, supplier(sources, "*", "*").get().size());
-        assertEquals(5, supplier(sources, "*", "*.md").get().size());
-        assertEquals(3, supplier(sources, "*", "file*.*").get().size());
+
+        assertEquals(6, supplier(sources, "*", "*.md").get().size());
+        assertEquals(4, supplier(sources, "*", "file*.*").get().size());
     }
 
     @Test
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileResolverTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
similarity index 72%
rename from freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileResolverTest.java
rename to freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
index 7c20b49..06d5d17 100644
--- a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileResolverTest.java
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
@@ -27,7 +27,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-public class RecursiveFileResolverTest {
+public class RecursiveFileSupplierTest {
 
     private static final String ANY_DIRECTORY = "./src/test/data";
     private static final String ANY_FILE_NAME = "file_01.csv";
@@ -35,13 +35,13 @@
 
     @Test
     public void shouldResolveAllFilesOfDirectory() {
-        assertEquals(4, fileResolver(ANY_DIRECTORY, null, null).get().size());
-        assertTrue(fileResolver(ANY_DIRECTORY, UNKNOWN_FILE_NAME, null).get().isEmpty());
+        assertEquals(5, fileSupplier(ANY_DIRECTORY, null, null).get().size());
+        assertTrue(fileSupplier(ANY_DIRECTORY, UNKNOWN_FILE_NAME, null).get().isEmpty());
     }
 
     @Test
     public void shouldResolveSingleMatchingFile() {
-        final List<File> files = fileResolver(ANY_DIRECTORY, ANY_FILE_NAME, null).get();
+        final List<File> files = fileSupplier(ANY_DIRECTORY, ANY_FILE_NAME, null).get();
 
         assertEquals(1, files.size());
         assertEquals(ANY_FILE_NAME, files.get(0).getName());
@@ -50,17 +50,17 @@
     @Test
     public void shouldResolveMultipleFiles() {
         final List<String> sources = Arrays.asList("pom.xml", "README.md");
-        final List<File> files = fileResolver(sources, "*", null).get();
+        final List<File> files = fileSupplier(sources, "*", null).get();
 
         assertEquals(2, files.size());
-        assertEquals("pom.xml", files.get(0).getName());
-        assertEquals("README.md", files.get(1).getName());
+        assertEquals("README.md", files.get(0).getName());
+        assertEquals("pom.xml", files.get(1).getName());
     }
 
     @Test
     public void shouldResolveMultipleFilesWithIncludeFilter() {
         final List<String> sources = Arrays.asList("pom.xml", "README.md");
-        final List<File> files = fileResolver(sources, "*.xml", null).get();
+        final List<File> files = fileSupplier(sources, "*.xml", null).get();
 
         assertEquals(1, files.size());
         assertEquals("pom.xml", files.get(0).getName());
@@ -68,23 +68,24 @@
 
     @Test
     public void shouldExcludeAllFiles() {
-        final List<File> files = fileResolver(ANY_DIRECTORY, null, "*").get();
+        final List<File> files = fileSupplier(ANY_DIRECTORY, null, "*").get();
 
         assertEquals(0, files.size());
     }
 
     @Test
     public void shouldExcludeFiles() {
-        final List<File> files = fileResolver(ANY_DIRECTORY, null, "*.csv").get();
+        final List<File> files = fileSupplier(ANY_DIRECTORY, null, "*.csv").get();
 
-        assertEquals(2, files.size());
-        assertEquals("file_01.txt", files.get(0).getName());
+        assertEquals(3, files.size());
+        assertEquals("nginx.env", files.get(0).getName());
         assertEquals("test.properties", files.get(1).getName());
+        assertEquals("file_01.txt", files.get(2).getName());
     }
 
     @Test
     public void shouldIncludeAndExcludeFiles() {
-        final List<File> files = fileResolver(ANY_DIRECTORY, "file*.*", "*.csv").get();
+        final List<File> files = fileSupplier(ANY_DIRECTORY, "file*.*", "*.csv").get();
 
         assertEquals(1, files.size());
         assertEquals("file_01.txt", files.get(0).getName());
@@ -92,17 +93,16 @@
 
     @Test
     public void shouldResolveMultipleFilesRecursivelyWithIncludes() {
-        final List<File> files = fileResolver(ANY_DIRECTORY, "*.csv", null).get();
+        final List<File> files = fileSupplier(ANY_DIRECTORY, "*.csv", null).get();
 
         assertEquals(2, files.size());
     }
 
-    private static RecursiveFileSupplier fileResolver(String source, String include, String exclude) {
-        return fileResolver(singletonList(source), include, exclude);
+    private static RecursiveFileSupplier fileSupplier(String source, String include, String exclude) {
+        return fileSupplier(singletonList(source), include, exclude);
     }
 
-    private static RecursiveFileSupplier fileResolver(List<String> sources, String include, String exclude) {
+    private static RecursiveFileSupplier fileSupplier(List<String> sources, String include, String exclude) {
         return new RecursiveFileSupplier(sources, singletonList(include), singletonList(exclude));
     }
-
 }
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
new file mode 100644
index 0000000..1d84c90
--- /dev/null
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateSourceFactoryTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Ignore;
+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://jsonplaceholder.typicode.com/posts/2";
+    private static final String ANY_ENVIRONMENT_VARIABLE = "env:///PWD";
+    private static final String ANY_NAMED_URI = "content:www=https://www.google.com?foo=bar#contenttype=application/json";
+
+    @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_ENVIRONMENT_VARIABLE);
+
+        assertEquals("PWD", 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(ANY_NAMED_URI);
+
+        assertNotNull(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
new file mode 100644
index 0000000..3cc0545
--- /dev/null
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/template/TemplateTransformationsBuilderTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.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.TemplateTransformationsBuilder;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class TemplateTransformationsBuilderTest {
+
+    private static final String ANY_TEMPLATE_FILE_NAME = "src/test/template/application.properties";
+    private static final String OTHER_TEMPLATE_FILE_NAME = "src/test/template/nginx/nginx.conf.ftl";
+    private static final String ANY_TEMPLATE_PATH = "template/info.ftl";
+    private static final String ANY_TEMPLATE_DIRECTORY_NAME = "src/test/template";
+
+    // === Interactive Template =============================================
+
+    @Test
+    public void shouldCreateFromInteractiveTemplate() {
+        final TemplateTransformations transformations = builder()
+                .setTemplate("interactive", "Hello World")
+                .setStdOut()
+                .build();
+
+        assertEquals(1, transformations.size());
+
+        final TemplateSource templateSource = transformations.get(0).getTemplateSource();
+        final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
+
+        assertEquals("interactive", templateSource.getName());
+        assertEquals(Origin.CODE, templateSource.getOrigin());
+        assertEquals("Hello World", templateSource.getCode());
+        assertNull(templateSource.getPath());
+        assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
+
+        assertNotNull(templateOutput.getWriter());
+        assertNull(templateOutput.getFile());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shouldThrowIllegalArgumentExceptionWheMixingInteractiveTemplateWithSources() {
+        builder()
+                .setTemplate("interactive", "Hello World")
+                .addSource(ANY_TEMPLATE_FILE_NAME)
+                .setStdOut()
+                .build();
+    }
+
+
+    // === Template File ====================================================
+
+    @Test
+    public void shouldCreateFromTemplateFile() {
+        final TemplateTransformations transformations = builder()
+                .addSource(ANY_TEMPLATE_FILE_NAME)
+                .setStdOut()
+                .build();
+
+        assertEquals(1, transformations.size());
+
+        final TemplateSource templateSource = transformations.get(0).getTemplateSource();
+        final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
+
+        assertNotNull(templateSource.getName());
+        assertEquals(Origin.CODE, templateSource.getOrigin());
+        assertNotNull(templateSource.getCode());
+        assertNull(templateSource.getPath());
+        assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
+
+        assertNotNull(templateOutput.getWriter());
+        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()
+                .build();
+
+        assertEquals(1, transformations.size());
+
+        final TemplateSource templateSource = transformations.get(0).getTemplateSource();
+        final TemplateOutput templateOutput = transformations.get(0).getTemplateOutput();
+
+        assertNotNull(templateSource.getName());
+        assertEquals(Origin.PATH, templateSource.getOrigin());
+        assertNull(templateSource.getCode());
+        assertNotNull(templateSource.getPath());
+        assertEquals(StandardCharsets.UTF_8, templateSource.getEncoding());
+
+        assertNotNull(templateOutput.getWriter());
+        assertNull(templateOutput.getFile());
+    }
+
+    // === Template Directory ===============================================
+
+    @Test
+    public void shouldCreateFromTemplateDirectory() {
+        final TemplateTransformations transformations = builder()
+                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .setStdOut()
+                .build();
+
+        assertEquals(2, transformations.size());
+        assertEquals("application.properties", transformations.get(0).getTemplateSource().getName());
+        assertEquals("nginx.conf.ftl", transformations.get(1).getTemplateSource().getName());
+    }
+
+    @Test
+    public void shouldCreateFromTemplateDirectoryWithOutputDirectory() {
+        final TemplateTransformations transformations = builder()
+                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .addOutput("/foo")
+                .build();
+
+        assertEquals(2, transformations.size());
+        assertEquals("application.properties", transformations.get(0).getTemplateSource().getName());
+        assertEquals("nginx.conf.ftl", transformations.get(1).getTemplateSource().getName());
+    }
+
+    @Test
+    public void shouldCreateFromTemplateDirectoryWithInclude() {
+        final TemplateTransformations transformations = builder()
+                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .addInclude("*.properties")
+                .setStdOut()
+                .build();
+
+        assertEquals(1, transformations.size());
+        assertEquals("application.properties", transformations.get(0).getTemplateSource().getName());
+    }
+
+    @Test
+    public void shouldCreateFromTemplateDirectoryWithExclude() {
+        final TemplateTransformations transformations = builder()
+                .addSource(ANY_TEMPLATE_DIRECTORY_NAME)
+                .addExclude("*.ftl")
+                .setStdOut()
+                .build();
+
+        assertEquals(1, transformations.size());
+        assertEquals("application.properties", transformations.get(0).getTemplateSource().getName());
+    }
+
+    private TemplateTransformationsBuilder builder() {
+        return TemplateTransformationsBuilder
+                .builder();
+    }
+}
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
index 882ffe5..92bd90c 100644
--- 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
@@ -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.util;
 
 import org.apache.freemarker.generator.base.util.MapFlattener;
diff --git a/freemarker-generator-base/src/test/template/application.properties b/freemarker-generator-base/src/test/template/application.properties
new file mode 100644
index 0000000..0488360
--- /dev/null
+++ b/freemarker-generator-base/src/test/template/application.properties
@@ -0,0 +1,19 @@
+<#--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+# == application.properties ==================================================
+server.name=${NGINX_HOSTNAME!"somehost"}
+server.logs=${NGINX_LOGS!"/var/log/nginx"}
diff --git a/freemarker-generator-base/src/test/template/nginx/nginx.conf.ftl b/freemarker-generator-base/src/test/template/nginx/nginx.conf.ftl
new file mode 100644
index 0000000..ac57019
--- /dev/null
+++ b/freemarker-generator-base/src/test/template/nginx/nginx.conf.ftl
@@ -0,0 +1,24 @@
+<#--
+  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.
+-->
+# == nginx-conf =============================================================
+server {
+  listen ${NGINX_PORT!"80"};
+  server_name ${NGINX_HOSTNAME!"somehost"};
+
+  root ${NGINX_WEBROOT!"/usr/share/nginx/www"};
+  index index.htm;
+}
diff --git a/freemarker-generator-cli/run-samples.sh b/freemarker-generator-cli/run-samples.sh
index 01e5085..d760145 100755
--- a/freemarker-generator-cli/run-samples.sh
+++ b/freemarker-generator-cli/run-samples.sh
@@ -155,6 +155,13 @@
 $FREEMARKER_CMD -t templates/properties/csv/locker-test-users.ftl site/sample/properties > target/out/locker-test-users.csv || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
+# Template Directory
+#############################################################################
+
+echo "site/template"
+$FREEMARKER_CMD -t site/template -PNGINX_HOSTNAME=localhost -o target/out/template  || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # YAML
 #############################################################################
 
diff --git a/freemarker-generator-cli/site/template/application.properties b/freemarker-generator-cli/site/template/application.properties
new file mode 100644
index 0000000..d5f2114
--- /dev/null
+++ b/freemarker-generator-cli/site/template/application.properties
@@ -0,0 +1,19 @@
+<#--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+# == application.properties ==================================================
+server.name=${NGINX_HOSTNAME!"somehost"}
+server.logs=${NGINX_LOGS!"/var/log/nginx"}
diff --git a/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl b/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl
new file mode 100644
index 0000000..ac57019
--- /dev/null
+++ b/freemarker-generator-cli/site/template/nginx/nginx.conf.ftl
@@ -0,0 +1,24 @@
+<#--
+  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.
+-->
+# == nginx-conf =============================================================
+server {
+  listen ${NGINX_PORT!"80"};
+  server_name ${NGINX_HOSTNAME!"somehost"};
+
+  root ${NGINX_WEBROOT!"/usr/share/nginx/www"};
+  index index.htm;
+}
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 c3c319a..6315aa0 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
@@ -33,7 +33,6 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
@@ -60,56 +59,56 @@
     TemplateSourceOptions templateSourceOptions;
 
     public static final class TemplateSourceOptions {
-        @Option(names = { "-t", "--template" }, description = "FreeMarker template to render")
-        public String template;
+        @Option(names = { "-t", "--template" }, description = "template to process")
+        public List<String> templates;
 
-        @Option(names = { "-i", "--interactive" }, description = "Interactive FreeMarker template")
+        @Option(names = { "-i", "--interactive" }, description = "interactive template to process")
         public String interactiveTemplate;
     }
 
-    @Option(names = { "-b", "--basedir" }, description = "Optional template base directory")
+    @Option(names = { "-b", "--basedir" }, description = "optional template base directory")
     String baseDir;
 
-    @Option(names = { "-D", "--system-property" }, description = "Set system property")
+    @Option(names = { "-D", "--system-property" }, description = "set system property")
     Properties systemProperties;
 
-    @Option(names = { "-e", "--input-encoding" }, description = "Encoding of data source", defaultValue = "UTF-8")
+    @Option(names = { "-e", "--input-encoding" }, description = "encoding of data source", defaultValue = "UTF-8")
     String inputEncoding;
 
-    @Option(names = { "-l", "--locale" }, description = "Locale being used for the output, e.g. 'en_US'")
+    @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")
+    @Option(names = { "-m", "--data-model" }, description = "data model used for rendering")
     List<String> dataModels;
 
-    @Option(names = { "-o", "--output" }, description = "Output file")
+    @Option(names = { "-o", "--output" }, description = "output file or directory")
     String outputFile;
 
-    @Option(names = { "-P", "--param" }, description = "Set parameter")
+    @Option(names = { "-P", "--param" }, description = "set parameter")
     Map<String, String> parameters;
 
-    @Option(names = { "-s", "--data-source" }, description = "Data source used for rendering")
+    @Option(names = { "-s", "--data-source" }, description = "data source used for redering")
     List<String> dataSources;
 
     @Option(names = { "--config" }, defaultValue = FREEMARKER_CLI_PROPERTY_FILE, description = "FreeMarker CLI configuration file")
     String configFile;
 
-    @Option(names = { "--include" }, description = "File pattern for data source input directory")
+    @Option(names = { "--data-source-include" }, description = "file include pattern for data sources")
     String include;
 
-    @Option(names = { "--exclude" }, description = "File pattern for data source input directory")
+    @Option(names = { "--data-source-exclude" }, description = "file exclude pattern for data sources")
     String exclude;
 
-    @Option(names = { "--output-encoding" }, description = "Encoding of output, e.g. UTF-8", defaultValue = "UTF-8")
+    @Option(names = { "--output-encoding" }, description = "encoding of output, e.g. UTF-8", defaultValue = "UTF-8")
     String outputEncoding;
 
-    @Option(names = { "--stdin" }, description = "Read data  source from stdin")
+    @Option(names = { "--stdin" }, description = "read data source from stdin")
     boolean readFromStdin;
 
-    @Option(names = { "--times" }, defaultValue = "1", description = "Re-run X times for profiling")
+    @Option(names = { "--times" }, defaultValue = "1", description = "re-run X times for profiling")
     int times;
 
-    @Parameters(description = "List of input files and/or input directories")
+    @Parameters(description = "data source files and/or directories")
     List<String> sources;
 
     /** User-supplied command line parameters */
@@ -118,7 +117,7 @@
     /** User-supplied writer (used mainly for unit testing) */
     Writer userSuppliedWriter;
 
-    /** Injected by Picolci */
+    /** Injected by Picocli */
     @Spec private CommandSpec spec;
 
     Main() {
@@ -190,14 +189,6 @@
                 }
             }
         }
-
-        // "-t" or "--template" parameter shall not contain wildcard characters
-        if (StringUtils.isNotEmpty(templateSourceOptions.template)) {
-            final String source = templateSourceOptions.template;
-            if (isFileSource(source) && (source.contains("*") || source.contains("?"))) {
-                throw new ParameterException(spec.commandLine(), "No wildcards supported for template: " + source);
-            }
-        }
     }
 
     private Settings settings(Properties configuration, List<File> templateDirectories) {
@@ -207,31 +198,31 @@
                 .isReadFromStdin(readFromStdin)
                 .setArgs(args)
                 .setConfiguration(configuration)
-                .setInclude(include)
-                .setExclude(exclude)
+                .setDataSourceIncludePattern(include)
+                .setDataSourceExcludePattern(exclude)
                 .setInputEncoding(inputEncoding)
                 .setInteractiveTemplate(templateSourceOptions.interactiveTemplate)
                 .setLocale(locale)
                 .setOutputEncoding(outputEncoding)
                 .setOutputFile(outputFile)
                 .setParameters(parameterModelSupplier.get())
-                .setDataSources(getCombindedDataSources())
+                .setDataSources(getCombinedDataSources())
                 .setDataModels(dataModels)
                 .setSystemProperties(systemProperties != null ? systemProperties : new Properties())
                 .setTemplateDirectories(templateDirectories)
-                .setTemplateName(templateSourceOptions.template)
+                .setTemplateNames(templateSourceOptions.templates)
                 .setWriter(writer(outputFile, outputEncoding))
                 .build();
     }
 
-    private Writer writer(String outputFile, String ouputEncoding) {
+    private Writer writer(String outputFile, String outputEncoding) {
         try {
             if (userSuppliedWriter != null) {
                 return userSuppliedWriter;
-            } else if (!StringUtils.isEmpty(outputFile)) {
-                return new BufferedWriter(new FileWriter(outputFile));
+            } else if (StringUtils.isEmpty(outputFile)) {
+                return new BufferedWriter(new OutputStreamWriter(System.out, outputEncoding));
             } else {
-                return new BufferedWriter(new OutputStreamWriter(System.out, ouputEncoding));
+                return null;
             }
         } catch (IOException e) {
             throw new RuntimeException("Unable to create writer", e);
@@ -250,7 +241,7 @@
      *
      * @return List of data sources
      */
-    private List<String> getCombindedDataSources() {
+    private List<String> getCombinedDataSources() {
         return Stream.of(dataSources, sources)
                 .filter(Objects::nonNull)
                 .flatMap(Collection::stream)
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 3b53e71..89e48bf 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
@@ -23,6 +23,7 @@
 import java.io.File;
 import java.io.Writer;
 import java.nio.charset.Charset;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -52,12 +53,18 @@
     /** List of FreeMarker template directories to be passed to FreeMarker <code>TemplateLoader</code> */
     private final List<File> templateDirectories;
 
-    /** Name of the template to be loaded and rendered */
-    private final String templateName;
+    /** List of template to be loaded and rendered */
+    private final List<String> templates;
 
-    /** Template provided by the user interactivly */
+    /** Template provided by the user interactively */
     private final String interactiveTemplate;
 
+    /** Optional include pattern for recursive directly search of template files */
+    private final String templateFileIncludePattern;
+
+    /** Optional exclude pattern for recursive directly search of data source files */
+    private final String templateFileExcludePattern;
+
     /** Encoding of input files */
     private final Charset inputEncoding;
 
@@ -67,14 +74,14 @@
     /** Enable verbose mode (currently not used) **/
     private final boolean verbose;
 
-    /** Optional output file if not written to stdout */
-    private final File outputFile;
+    /** Optional output file or directory if not written to stdout */
+    private final File output;
 
-    /** Optional include pattern for recursice directly search of source files */
-    private final String include;
+    /** Optional include pattern for recursive directly search of data source files */
+    private final String dataSourceIncludePattern;
 
-    /** Optional exclude pattern for recursice directly search of source files */
-    private final String exclude;
+    /** 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;
@@ -92,7 +99,7 @@
     private final Map<String, Object> parameters;
 
     /** User-supplied system properties */
-    private final Properties sytemProperties;
+    private final Properties systemProperties;
 
     /** The writer used for rendering templates, e.g. stdout or a file writer */
     private final Writer writer;
@@ -101,43 +108,47 @@
             Properties configuration,
             List<String> args,
             List<File> templateDirectories,
-            String template,
+            List<String> templates,
             String interactiveTemplate,
+            String templateFileIncludePattern,
+            String templateFileExcludePattern,
             Charset inputEncoding,
             Charset outputEncoding,
             boolean verbose,
-            File outputFile,
-            String include,
-            String exclude,
+            File output,
+            String dataSourceIncludePattern,
+            String dataSourceExcludePattern,
             Locale locale,
             boolean isReadFromStdin,
             List<String> dataSources,
             List<String> dataModels,
             Map<String, Object> parameters,
-            Properties sytemProperties,
+            Properties systemProperties,
             Writer writer) {
-        if (isEmpty(template) && isEmpty(interactiveTemplate)) {
+        if ((templates == null || templates.isEmpty()) && isEmpty(interactiveTemplate)) {
             throw new IllegalArgumentException("Either 'template' or 'interactiveTemplate' must be provided");
         }
 
         this.args = requireNonNull(args);
         this.templateDirectories = requireNonNull(templateDirectories);
-        this.templateName = template;
+        this.templates = requireNonNull(templates);
         this.interactiveTemplate = interactiveTemplate;
+        this.templateFileIncludePattern = templateFileIncludePattern;
+        this.templateFileExcludePattern = templateFileExcludePattern;
         this.inputEncoding = inputEncoding;
         this.outputEncoding = outputEncoding;
         this.verbose = verbose;
-        this.outputFile = outputFile;
-        this.include = include;
-        this.exclude = exclude;
+        this.output = output;
+        this.dataSourceIncludePattern = dataSourceIncludePattern;
+        this.dataSourceExcludePattern = dataSourceExcludePattern;
         this.locale = requireNonNull(locale);
         this.isReadFromStdin = isReadFromStdin;
         this.dataSources = requireNonNull(dataSources);
         this.dataModels = requireNonNull(dataModels);
         this.parameters = requireNonNull(parameters);
-        this.sytemProperties = requireNonNull(sytemProperties);
+        this.systemProperties = requireNonNull(systemProperties);
         this.configuration = requireNonNull(configuration);
-        this.writer = new NonClosableWriterWrapper(requireNonNull(writer));
+        this.writer = writer != null ? new NonClosableWriterWrapper(writer) : null;
     }
 
     public static SettingsBuilder builder() {
@@ -156,14 +167,22 @@
         return templateDirectories;
     }
 
-    public String getTemplateName() {
-        return templateName;
+    public List<String> getTemplates() {
+        return templates;
     }
-
+    
     public String getInteractiveTemplate() {
         return interactiveTemplate;
     }
 
+    public String getTemplateFileIncludePattern() {
+        return templateFileIncludePattern;
+    }
+
+    public String getTemplateFileExcludePattern() {
+        return templateFileExcludePattern;
+    }
+
     public Charset getInputEncoding() {
         return inputEncoding;
     }
@@ -180,16 +199,16 @@
         return verbose;
     }
 
-    public File getOutputFile() {
-        return outputFile;
+    public File getOutput() {
+        return output;
     }
 
-    public String getInclude() {
-        return include;
+    public String getDataSourceIncludePattern() {
+        return dataSourceIncludePattern;
     }
 
-    public String getExclude() {
-        return exclude;
+    public String getDataSourceExcludePattern() {
+        return dataSourceExcludePattern;
     }
 
     public Locale getLocale() {
@@ -212,12 +231,12 @@
         return parameters;
     }
 
-    public Properties getSytemProperties() {
-        return sytemProperties;
+    public Properties getSystemProperties() {
+        return systemProperties;
     }
 
     public boolean hasOutputFile() {
-        return outputFile != null;
+        return output != null;
     }
 
     public Writer getWriter() {
@@ -236,7 +255,7 @@
         result.put(Model.FREEMARKER_LOCALE, getLocale());
         result.put(Model.FREEMARKER_TEMPLATE_DIRECTORIES, getTemplateDirectories());
         result.put(Model.FREEMARKER_USER_PARAMETERS, getParameters());
-        result.put(Model.FREEMARKER_USER_SYSTEM_PROPERTIES, getSytemProperties());
+        result.put(Model.FREEMARKER_USER_SYSTEM_PROPERTIES, getSystemProperties());
         result.put(Model.FREEMARKER_WRITER, getWriter());
         return result;
     }
@@ -251,33 +270,37 @@
                 "configuration=" + configuration +
                 ", args=" + args +
                 ", templateDirectories=" + templateDirectories +
-                ", templateName='" + templateName + '\'' +
+                ", templateName=s'" + templates + '\'' +
                 ", interactiveTemplate='" + interactiveTemplate + '\'' +
+                ", templateFileIncludePattern='" + templateFileIncludePattern + '\'' +
+                ", templateFileExcludePattern='" + templateFileExcludePattern + '\'' +
                 ", inputEncoding=" + inputEncoding +
                 ", outputEncoding=" + outputEncoding +
                 ", verbose=" + verbose +
-                ", outputFile=" + outputFile +
-                ", include='" + include + '\'' +
-                ", exclude='" + include + '\'' +
+                ", outputFile=" + output +
+                ", include='" + dataSourceIncludePattern + '\'' +
+                ", exclude='" + dataSourceExcludePattern + '\'' +
                 ", locale=" + locale +
                 ", isReadFromStdin=" + isReadFromStdin +
                 ", dataSources=" + dataSources +
                 ", properties=" + parameters +
-                ", sytemProperties=" + sytemProperties +
+                ", systemProperties=" + systemProperties +
                 '}';
     }
 
     public static class SettingsBuilder {
         private List<String> args;
         private List<File> templateDirectories;
-        private String templateName;
+        private List<String> templateNames;
         private String interactiveTemplate;
+        private String templateFileIncludePattern;
+        private String templateFileExcludePattern;
         private String inputEncoding;
         private String outputEncoding;
         private boolean verbose;
         private String outputFile;
-        private String include;
-        private String exclude;
+        private String dataSourceIncludePattern;
+        private String dataSourceExcludePattern;
         private String locale;
         private boolean isReadFromStdin;
         private List<String> dataSources;
@@ -295,6 +318,7 @@
             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();
@@ -315,8 +339,10 @@
             return this;
         }
 
-        public SettingsBuilder setTemplateName(String templateName) {
-            this.templateName = templateName;
+        public SettingsBuilder setTemplateNames(List<String> templateNames) {
+            if (templateNames != null) {
+                this.templateNames = templateNames;
+            }
             return this;
         }
 
@@ -325,6 +351,16 @@
             return this;
         }
 
+        public SettingsBuilder setTemplateFileIncludePattern(String templateFileIncludePattern) {
+            this.templateFileIncludePattern = templateFileIncludePattern;
+            return this;
+        }
+
+        public SettingsBuilder setTemplateFileExcludePattern(String templateFileExcludePattern) {
+            this.templateFileExcludePattern = templateFileExcludePattern;
+            return this;
+        }
+
         public SettingsBuilder setInputEncoding(String inputEncoding) {
             if (inputEncoding != null) {
                 this.inputEncoding = inputEncoding;
@@ -349,13 +385,13 @@
             return this;
         }
 
-        public SettingsBuilder setInclude(String include) {
-            this.include = include;
+        public SettingsBuilder setDataSourceIncludePattern(String dataSourceIncludePattern) {
+            this.dataSourceIncludePattern = dataSourceIncludePattern;
             return this;
         }
 
-        public SettingsBuilder setExclude(String exclude) {
-            this.exclude = exclude;
+        public SettingsBuilder setDataSourceExcludePattern(String dataSourceExcludePattern) {
+            this.dataSourceExcludePattern = dataSourceExcludePattern;
             return this;
         }
 
@@ -419,14 +455,16 @@
                     configuration,
                     args,
                     templateDirectories,
-                    templateName,
+                    templateNames,
                     interactiveTemplate,
+                    templateFileIncludePattern,
+                    templateFileExcludePattern,
                     inputEncoding,
                     outputEncoding,
                     verbose,
                     currOutputFile,
-                    include,
-                    exclude,
+                    dataSourceIncludePattern,
+                    dataSourceExcludePattern,
                     LocaleUtils.parseLocale(currLocale),
                     isReadFromStdin,
                     dataSources,
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 6a57e59..2c220b2 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
@@ -21,6 +21,8 @@
 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;
@@ -52,8 +54,8 @@
 
     public static DataSourcesSupplier dataSourcesSupplier(Settings settings) {
         return new DataSourcesSupplier(settings.getDataSources(),
-                settings.getInclude(),
-                settings.getExclude(),
+                settings.getDataSourceIncludePattern(),
+                settings.getDataSourceExcludePattern(),
                 settings.getInputEncoding());
     }
 
@@ -65,6 +67,17 @@
         return settings::getParameters;
     }
 
+    public static TemplateTransformationsSupplier templateTransformationsSupplier(Settings settings) {
+        return (() -> TemplateTransformationsBuilder.builder()
+                .setTemplate("interactive", settings.getInteractiveTemplate())
+                .addSources(settings.getTemplates())
+                .addInclude(settings.getTemplateFileIncludePattern())
+                .addExclude(settings.getTemplateFileExcludePattern())
+                .addOutput(settings.getOutput())
+                .setWriter(settings.getWriter())
+                .build());
+    }
+
     public static PropertiesSupplier propertiesSupplier(String fileName) {
         return new PropertiesSupplier(
                 new PropertiesFileSystemSupplier(fileName),
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 618fef9..7813a4e 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
@@ -18,15 +18,22 @@
 
 import freemarker.template.Configuration;
 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.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.IOException;
 import java.io.Writer;
 import java.net.URI;
@@ -47,6 +54,7 @@
 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;
 
 /**
@@ -62,41 +70,60 @@
     private final Supplier<Map<String, Object>> dataModelsSupplier;
     private final Supplier<Map<String, Object>> parameterModelSupplier;
     private final Supplier<Configuration> configurationSupplier;
+    private final Supplier<TemplateTransformations> templateTransformationsSupplier;
+
 
     public FreeMarkerTask(Settings settings) {
         this(settings,
-                toolsSupplier(settings),
+                configurationSupplier(settings),
+                templateTransformationsSupplier(settings),
                 dataSourcesSupplier(settings),
                 dataModelSupplier(settings),
                 parameterSupplier(settings),
-                configurationSupplier(settings));
+                toolsSupplier(settings)
+        );
     }
 
     public FreeMarkerTask(Settings settings,
-                          Supplier<Map<String, Object>> toolsSupplier,
+                          Supplier<Configuration> configurationSupplier,
+                          Supplier<TemplateTransformations> templateTransformationsSupplier,
                           Supplier<List<DataSource>> dataSourcesSupplier,
                           Supplier<Map<String, Object>> dataModelsSupplier,
                           Supplier<Map<String, Object>> parameterModelSupplier,
-                          Supplier<Configuration> configurationSupplier) {
+                          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);
     }
 
     @Override
     public Integer call() {
-        final Template template = template(settings, configurationSupplier);
-        try (Writer writer = settings.getWriter(); DataSources dataSources = dataSources(settings, dataSourcesSupplier)) {
+        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);
-            template.process(dataModel, writer);
+            templateTransformations.getList().forEach(t -> process(configuration, t, dataModel));
             return SUCCESS;
         } catch (RuntimeException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to render FreeMarker template: " + template.getName(), e);
+            throw new RuntimeException("Failed to process templates", e);
+        }
+    }
+
+    private void process(Configuration configuration,
+                         TemplateTransformation templateTransformation,
+                         Map<String, Object> dataModel) {
+        final TemplateSource templateSource = templateTransformation.getTemplateSource();
+        final TemplateOutput templateOutput = templateTransformation.getTemplateOutput();
+        try (Writer writer = writer(templateOutput)) {
+            final Template template = template(configuration, templateSource);
+            template.process(dataModel, writer);
+        } catch (TemplateException | IOException e) {
+            throw new RuntimeException("Failed to process template: " + templateSource.getName(), e);
         }
     }
 
@@ -113,33 +140,6 @@
         return new DataSources(dataSources);
     }
 
-    /**
-     * Loading FreeMarker templates from absolute paths is not encouraged due to security
-     * concern (see https://freemarker.apache.org/docs/pgui_config_templateloading.html#autoid_42)
-     * which are mostly irrelevant when running on the command line. So we resolve the absolute file
-     * instead of relying on existing template loaders.
-     *
-     * @param settings              Settings
-     * @param configurationSupplier Supplies FreeMarker configuration
-     * @return FreeMarker template
-     */
-    private static Template template(Settings settings, Supplier<Configuration> configurationSupplier) {
-        final String templateName = settings.getTemplateName();
-        final Configuration configuration = configurationSupplier.get();
-
-        try {
-            if (settings.isInteractiveTemplate()) {
-                return interactiveTemplate(settings, configuration);
-            } else if (isAbsoluteTemplateFile(settings)) {
-                return fileTemplate(settings, configuration);
-            } else {
-                return configuration.getTemplate(templateName);
-            }
-        } catch (IOException e) {
-            throw new RuntimeException("Failed to load template: " + templateName, e);
-        }
-    }
-
     private static Map<String, Object> dataModel(
             DataSources dataSources,
             Supplier<Map<String, Object>> parameterModelSupplier,
@@ -153,27 +153,55 @@
         return result;
     }
 
-    private static boolean isAbsoluteTemplateFile(Settings settings) {
-        final File file = new File(settings.getTemplateName());
-        return file.isAbsolute() && file.exists() & !file.isDirectory();
+    // ==============================================================
+
+    private static Writer writer(TemplateOutput templateOutput) throws IOException {
+        if (templateOutput.getWriter() != null) {
+            return templateOutput.getWriter();
+        }
+
+        final File file = templateOutput.getFile();
+        FileUtils.forceMkdirParent(file);
+        return new BufferedWriter(new FileWriter(file));
     }
 
-    private static Template fileTemplate(Settings settings, Configuration configuration) {
-        final String templateName = settings.getTemplateName();
-        final File templateFile = new File(templateName);
-        try {
-            final String content = FileUtils.readFileToString(templateFile, settings.getTemplateEncoding());
-            return new Template(templateName, content, configuration);
-        } catch (IOException e) {
-            throw new RuntimeException("Failed to load template: " + templateName, e);
+    /**
+     * Loading FreeMarker templates from absolute paths is not encouraged due to security
+     * concern (see https://freemarker.apache.org/docs/pgui_config_templateloading.html#autoid_42)
+     * which are mostly irrelevant when running on the command line. So we resolve the absolute file
+     * instead of relying on existing template loaders.
+     *
+     * @param configuration  FreeMarker configuration
+     * @param templateSource source template to load
+     * @return FreeMarker template
+     */
+    private static Template template(Configuration configuration, TemplateSource templateSource) {
+        switch (templateSource.getOrigin()) {
+            case PATH:
+                return fromTemplatePath(configuration, templateSource);
+            case CODE:
+                return fromTemplateCode(configuration, templateSource);
+            default:
+                throw new IllegalArgumentException("Don't know how to handle: " + templateSource.getOrigin());
         }
     }
 
-    private static Template interactiveTemplate(Settings settings, Configuration configuration) {
+    private static Template fromTemplatePath(Configuration configuration, TemplateSource templateSource) {
+        final String path = templateSource.getPath();
         try {
-            return new Template(Location.INTERACTIVE, settings.getInteractiveTemplate(), configuration);
+            return configuration.getTemplate(path);
         } catch (IOException e) {
-            throw new RuntimeException("Failed to load interactive template", e);
+            throw new RuntimeException("Failed to load template from path: " + path, e);
+        }
+    }
+
+    private static Template fromTemplateCode(Configuration configuration, TemplateSource templateSource) {
+        final String name = templateSource.getName();
+
+        try {
+            return new Template(name, templateSource.getCode(), configuration);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to load template code: " + name, e);
         }
     }
 }
diff --git a/freemarker-generator-cli/src/main/scripts/run-samples.sh b/freemarker-generator-cli/src/main/scripts/run-samples.sh
index c9f335e..c005f09 100755
--- a/freemarker-generator-cli/src/main/scripts/run-samples.sh
+++ b/freemarker-generator-cli/src/main/scripts/run-samples.sh
@@ -155,6 +155,13 @@
 $FREEMARKER_CMD -t templates/properties/csv/locker-test-users.ftl site/sample/properties > target/out/locker-test-users.csv || { echo >&2 "Test failed.  Aborting."; exit 1; }
 
 #############################################################################
+# Template Directory
+#############################################################################
+
+echo "site/template"
+$FREEMARKER_CMD -t site/template -PNGINX_HOSTNAME=localhost -o target/out/template  || { echo >&2 "Test failed.  Aborting."; exit 1; }
+
+#############################################################################
 # YAML
 #############################################################################
 
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts.md b/freemarker-generator-cli/src/site/markdown/cli/concepts.md
deleted file mode 100644
index dc95664..0000000
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts.md
+++ /dev/null
@@ -1,124 +0,0 @@
-## The Mental Model
-
-* A command line invocation requires 1..n `templates` and 0..n `datasources`
-* A command line invocation is mapped to a series of `transformations`
-* The mapping strategy is either
-    * `aggregating`: each `template` yields one output file based on 0..n `datasources`
-    * `generating`: each `datasources` yields one output per `template`
-* The `transformation` consists of exactly one `template`, 0..n `datasources` and an `output`
-* An `output` is either written to 
-    * `stdout`
-    * an output file
-    * an output directory
-* When the output is written to a directory
-    * the structure of the input directory is preserved
-    * the file names can be customized using an output mapping
-
-## Aggregation Versus Generation
-
-This distiniction is only relevant if you have more than one `datasource` for a given `template`
-
-* aggregation will invoke the `template` once with all `datasources`
-* generation will invoke the `template` multiple times with only one `datasources`
-
-### Aggregation
-
-A nice example for `aggregation` is the generation of test user documentation with the following directory layout
-
-```text
-.
-|-- data
-|   |-- user-products.csv
-|   |-- user-transactions.csv
-|   `-- users.csv
-`-- templates
-    |-- test-user-documentation.html.ftl
-    `-- test-user-documentation.md.ftl
-```
-
-We want to transform those three CSV input files into a `Markdown` and `HTML` document copied into an `out` directory
-
-```text
-# Using short options
-> freemarker-cli -t templates -d data -output-dir out
-
-# Using long options
-> freemarker-cli -m aggregate -t templates -d data -output-dir out
-> freemarker-cli --mode aggregate --template templates --datasource data -output-dir out
-```
-
-The invocation would generate the following files
-
-```text
-.
-`-- out
-    |-- test-user-documentation.html
-    `-- test-user-documentation.md
-```
-
-### Generation
-
-#### Source Code Generation
-
-A good example for `generation` is the creation of souce code, e.g. as provided by the `maven-generator-plugin`
-
-```text
-.
-|-- data
-|   |-- ProductDao.java.json
-|   |-- TransactionDao.java.json
-|   `-- UserDao.java.json
-`-- templates
-    `-- generate-dao.ftl
-```
-
-We want to generate three Java files based on JSON files and a single template
-
-```text
-# Using short options
-> freemarker-cli -m generate -t templates -d data -output-dir out
-
-# Using long options
-> freemarker-cli --mode generate --template templates --datasource data -output-dir out
-```
-
-The invocation would generate the following files
-
-```text
-.
-`-- out
-    |-- ProductDao.java
-    |-- TransactionDao.java
-    `-- UserDao.java
-```
-
-#### Configuration File Generations
-
-Another example for `generation` comes from cloud computing, i.e. the generation of configuration files using the following directory layout
-
-```text
-.
-|-- config
-|   |-- application.yml
-|   `-- nginx.conf
-`-- config.json
-```
-
-We want to generate a set of expanded configuration files
-
-```text
-# Using short options
-> freemarker-cli -m generate -t config -d config.json -output-dir out
-
-# Using long options
-> freemarker-cli --mode generate --template templates --datasource data -output-dir out
-```
-
-which results in the following directory structure
-
-```text
-.
-`-- out
-|   |-- application.yml
-|   `-- nginx.conf
-```
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
new file mode 100644
index 0000000..84647e0
--- /dev/null
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
@@ -0,0 +1,86 @@
+## DataSources
+
+A `DataSource` consists of lazy-loaded data available in Apache FreeMarker's model (context) - it provides
+
+* a `charset` for reading textual content
+* a `content type`
+* a `name` and a `group`
+* access to textual content directly or using a line iterator
+* access to the data input stream
+
+### Loading A DataSource
+
+A `DataSource` can be loaded from the file system, e.g. as positional command line argument
+
+```
+bin/freemarker-cli -t templates/info.ftl README.md
+
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=README.md, group=default, contentType=text/markdown, charset=UTF-8, length=57,188 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/README.md
+```
+ 
+from an URL
+
+```
+bin/freemarker-cli --data-source xkcd=https://xkcd.com/info.0.json -t templates/info.ftl
+
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=xkcd, group=default, contentType=application/json, charset=UTF-8, length=-1 Bytes
+    URI : https://xkcd.com/info.0.json 
+```
+
+or from an environment variable, e.g. `NGINX_CONF` having a JSON payload
+
+```
+export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
+bin/freemarker-cli -t templates/info.ftl -s conf=env:///NGINX_CONF#mimetype=application/json
+
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=conf, group=default, contentType=application/json, charset=UTF-8, length=50 Bytes
+    URI : env:///NGINX_CONF
+```
+
+Of course you can load multiple `DataSources` directly
+
+```
+bin/freemarker-cli -t templates/info.ftl README.md xkcd=https://xkcd.com/info.0.json
+ 
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=README.md, group=default, contentType=text/markdown, charset=UTF-8, length=57,188 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/README.md
+    [#2], name=xkcd, group=default, contentType=application/json, charset=UTF-8, length=-1 Bytes
+    URI : https://xkcd.com/info.0.json
+```
+
+or load them from a directory
+
+```
+bin/freemarker-cli -t templates/info.ftl -s site/sample/
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=combined-access.log, group=default, contentType=text/plain, charset=UTF-8, length=2,068 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/site/sample/accesslog/combined-access.log
+    ...
+    [#22], name=swagger-spec.yaml, group=default, contentType=text/yaml, charset=UTF-8, length=17,555 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/site/sample/yaml/swagger-spec.yaml
+
+```
+
+which can be combined with `include` and `exclude` filters
+
+```
+bin/freemarker-cli -t templates/info.ftl -s site/sample --data-source-include=*.json
+
+FreeMarker CLI DataSources
+------------------------------------------------------------------------------
+    [#1], name=github-users.json, group=default, contentType=application/json, charset=UTF-8, length=7,168 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/site/sample/json/github-users.json
+    [#2], name=swagger-spec.json, group=default, contentType=application/json, charset=UTF-8, length=24,948 Bytes
+    URI : file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/site/sample/json/swagger-spec.json
+
+```
diff --git a/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
new file mode 100644
index 0000000..966743b
--- /dev/null
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/transformation.md
@@ -0,0 +1,12 @@
+## Transformation
+
+* 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 `transformations`
+* The `transformation` consists of exactly one `template`, 0..n `data sources` / `data models` and an `output`
+* An `output` is either written to 
+    * `stdout`
+    * 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 externsion is removed
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
new file mode 100644
index 0000000..3181907
--- /dev/null
+++ b/freemarker-generator-cli/src/site/markdown/cli/usage/transforming-directories.md
@@ -0,0 +1,126 @@
+## Transforming Directories
+
+FreeMarker CLI supports the transformation of directories
+
+* Transform an input directory recursively into an output directory
+* If a template has a ".ftl" extension this extension will be removed after processing
+* Only a single directory is support
+* Currently no inclusion / exclusion pattern for templates are supported
+
+### Transform Template Directory To STDOUT
+
+```
+bin/freemarker-cli -t site/template/
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 80;
+  server_name 127.0.0.1;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
+
+### Transform Template Directory To Output Directory
+
+```
+bin/freemarker-cli -t site/template/ -o out; ls -l out
+total 8
+-rw-r--r--  1 sgoeschl  staff  128 May 30 20:02 application.properties
+drwxr-xr-x  3 sgoeschl  staff   96 May 30 20:02 nginx
+```
+
+### Pass Parameter On The Command Line
+
+```
+bin/freemarker-cli -t site/template/ -P NGINX_HOSTNAME=localhost
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 80;
+  server_name localhost;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
+
+### Use Environment Variables
+
+```
+export NGINX_PORT=8080
+bin/freemarker-cli -t site/template/ -m env:///
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 8080;
+  server_name 127.0.0.1;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
+
+### Use Environment File
+
+```
+echo "NGINX_PORT=8080" > nginx.env
+bin/freemarker-cli -t site/template/ -m nginx.env 
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 8080;
+  server_name 127.0.0.1;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
+
+### Use JSON File
+
+```
+echo '{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}' > nginx.json
+bin/freemarker-cli -t site/template/ -m nginx.json 
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 8443;
+  server_name localhost;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+
+```
+
+### Use Environment Variable With JSON Payload
+
+```
+export NGINX_CONF='{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}'
+echo $NGINX_CONF
+{"NGINX_PORT":"8443","NGINX_HOSTNAME":"localhost"}
+bin/freemarker-cli -t site/template/ -m env:///NGINX_CONF#mimetype=application/json
+# == application.properties ==================================================
+server.name=localhost
+server.logs=/var/log/nginx
+# == nginx-conf =============================================================
+server {
+  listen 8443;
+  server_name localhost;
+
+  root /usr/share/nginx/www;
+  index index.htm;
+}
+```
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/site/markdown/index.md b/freemarker-generator-cli/src/site/markdown/index.md
index 791ab12..b8d50e1 100644
--- a/freemarker-generator-cli/src/site/markdown/index.md
+++ b/freemarker-generator-cli/src/site/markdown/index.md
@@ -2,6 +2,12 @@
 
 ### Concepts
 
-* [User-Supplied Parameters](cli/concepts/user-parameters.html)
 * [Named URIs](cli/concepts/named-uris.html)
-* [Data Models](cli/concepts/data-models.html)
\ No newline at end of file
+* [Data Sources](cli/concepts/data-sources.html)
+* [Data Models](cli/concepts/data-models.html)
+* [User-Supplied Parameters](cli/concepts/user-parameters.html)
+* [Transformation](cli/concepts/transformation.html)
+
+### Usage
+
+* [Transforming Directories](cli/usage/transforming-directories.html)
diff --git a/freemarker-generator-cli/src/site/site.xml b/freemarker-generator-cli/src/site/site.xml
index a2157bf..0c6d789 100644
--- a/freemarker-generator-cli/src/site/site.xml
+++ b/freemarker-generator-cli/src/site/site.xml
@@ -18,7 +18,6 @@
     <body>
         <menu name="Overview">
             <item name="Introduction" href="index.html" />
-            <item name="Concepts" href="cli/concepts.html" />        
         </menu>
     </body>
 </project>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
index fedc66c..9b2727b 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
@@ -114,9 +114,15 @@
     }
 
     @Test
+    public void shouldTransformTemplateDirectory() throws IOException {
+        assertTrue(execute("-t site/template").contains("server.name=somehost"));
+        assertTrue(execute("-t site/template -PNGINX_HOSTNAME=localhost").contains("server.name=localhost"));
+    }
+
+    @Test
     @Ignore("Manual test to check memory consumption and resource handling")
     public void shouldCloseAllResources() throws IOException {
-        for (int i = 0; i < 5000; i++) {
+        for (int i = 0; i < 500; i++) {
             shouldRunInfo();
             shouldRunDemoExamples();
             shouldRunCsvExamples();
@@ -128,6 +134,7 @@
             shouldRunXmlExamples();
             shouldRunGrokExamples();
             shouldRunInteractiveTemplateExamples();
+            shouldTransformTemplateDirectory();
             shouldRunWithExposedEnvironmentVariableExamples();
         }
     }
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
index 210f32f..2dbaf2c 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
@@ -48,7 +48,8 @@
     // private static final String CMD = "-b ./src/test -t templates/demo.ftl -m env=./site/sample/properties/user_0001/user.properties";
     // private static final String CMD = "-b ./src/test -t templates/demo.ftl -m ./site/sample/properties/user_0001/user.properties";
     // private static final String CMD = "-b ./src/test --data-model post=https://jsonplaceholder.typicode.com/posts/2 -t templates/info.ftl";
-    private static final String CMD = "-b ./src/test -t templates/info.ftl -P name=value";
+    // private static final String CMD = "-b ./src/test -t templates/info.ftl -P name=value";
+    private static final String CMD = "-P NGINX_PORT=8080 -t ../freemarker-generator-base/src/test/template -t templates/info.ftl";
 
 
     public static void main(String[] args) {
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 57f2844..800e8be 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
@@ -101,7 +101,7 @@
     public void shouldParseSingleTemplate() {
         final Main main = parse("-t", ANY_TEMPLATE);
 
-        assertEquals(ANY_TEMPLATE, main.templateSourceOptions.template);
+        assertEquals(ANY_TEMPLATE, main.templateSourceOptions.templates.get(0));
     }
 
     @Test
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 ad24631..baae451 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
@@ -59,6 +59,8 @@
     }
 
     private SettingsBuilder settingsBuilder() {
-        return Settings.builder().setTemplateName(ANY_TEMPLATE_NAME).setWriter(new StringWriter());
+        return Settings.builder()
+                .setTemplateNames(singletonList(ANY_TEMPLATE_NAME))
+                .setWriter(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
index 16032e4..5aa6681 100644
--- 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
@@ -51,14 +51,14 @@
 
         assertEquals(1, settings.getArgs().size());
         assertNotNull(settings.getConfiguration());
-        assertEquals(ANY_INCLUDE, settings.getInclude());
+        assertEquals(ANY_INCLUDE, settings.getDataSourceIncludePattern());
         assertEquals(ANY_INPUT_ENCODING, settings.getInputEncoding().name());
         assertEquals(ANY_OUTPUT_ENCODING, settings.getOutputEncoding().name());
-        assertEquals(ANY_OUTPUT_FILE, settings.getOutputFile().getName());
-        assertEquals(ANY_TEMPLATE_NAME, settings.getTemplateName());
+        assertEquals(ANY_OUTPUT_FILE, settings.getOutput().getName());
+        assertEquals(ANY_TEMPLATE_NAME, settings.getTemplates().get(0));
         assertNotNull(settings.getDataSources());
         assertNotNull(settings.getParameters());
-        assertNotNull(settings.getSytemProperties());
+        assertNotNull(settings.getSystemProperties());
         assertTrue(settings.isReadFromStdin());
         assertTrue(settings.isInteractiveTemplate());
         assertTrue(settings.isVerbose());
@@ -69,7 +69,7 @@
                 .isReadFromStdin(true)
                 .setArgs(ANY_ARGS)
                 .setConfiguration(ANY_CONFIGURATION)
-                .setInclude(ANY_INCLUDE)
+                .setDataSourceIncludePattern(ANY_INCLUDE)
                 .setInputEncoding(ANY_INPUT_ENCODING)
                 .setInteractiveTemplate(ANY_INTERACTIVE_TEMPLATE)
                 .setLocale(ANY_LOCALE)
@@ -78,7 +78,7 @@
                 .setParameters(ANY_USER_PARAMETERS)
                 .setDataSources(ANY_SOURCES)
                 .setSystemProperties(ANY_SYSTEM_PROPERTIES)
-                .setTemplateName(ANY_TEMPLATE_NAME)
+                .setTemplateNames(singletonList(ANY_TEMPLATE_NAME))
                 .setWriter(new StringWriter())
                 .setVerbose(true);
     }
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index f530805..455ebb0 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -1,8 +1,8 @@
 The Apache FreeMarker Generator projects provides additional tools to generate textual ouptut using [Apache FreeMarker](https://freemarker.apache.org).
 
-| Name                  | Description                                                       |
-| --------------------- | ----------------------------------------------------------------- |
-| Base                  | Common functionality independent from Apache FreeMarker           |
-| Tools                 | Data source processing tools for Apache FreeMarker Generator      |
-| CLI                   | Command-line client for Apache FreeMarker                         |
-| Maven Plugin          | Maven plugin for Apache FreeMarker                                |
\ No newline at end of file
+| Name                                                          | Description                                                       |
+| ------------------------------------------------------------- | ----------------------------------------------------------------- |
+| [Base](freemarker-generator-base/index.html)                  | Common functionality independent from Apache FreeMarker           |
+| [Tools](freemarker-generator-tools/index.html)                | Data source processing tools for Apache FreeMarker Generator      |
+| [CLI](freemarker-generator-cli/index.html)                    | Command-line client for Apache FreeMarker                         |
+| [Maven Plugin](freemarker-generator-maven-plugin/index.html)  | Maven plugin for Apache FreeMarker                                |
\ No newline at end of file
diff --git a/travis.sh b/travis.sh
new file mode 100755
index 0000000..e9c3f62
--- /dev/null
+++ b/travis.sh
@@ -0,0 +1,5 @@
+mvn clean install
+cd ./freemarker-generator-cli
+sh ./run-samples.sh
+cd ../freemarker-generator-maven-plugin-sample
+mvn clean package
\ No newline at end of file