This is an automated email from the ASF dual-hosted git repository.

sgoeschl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/freemarker-generator.git


The following commit(s) were added to refs/heads/master by this push:
     new f0bae9a  FREEMARKER-172 [freemarker-generator] Refactor 
DataSourceFactory (#27)
f0bae9a is described below

commit f0bae9a8870f325fa828afcd5968169516648944
Author: Siegfried Goeschl <[email protected]>
AuthorDate: Sat Feb 6 18:34:46 2021 +0100

    FREEMARKER-172 [freemarker-generator] Refactor DataSourceFactory (#27)
---
 .../generator/base/datasource/DataSource.java      |  90 ++++++++--
 .../base/datasource/DataSourceFactory.java         | 131 +++------------
 .../DataSourceLoader.java}                         |  43 +++--
 .../base/datasource/DataSourceLoaderFactory.java   |  44 +++++
 .../generator/base/datasource/DataSources.java     |  16 +-
 .../base/datasource/DataSourcesSupplier.java       |  16 +-
 .../datasource/loader/DefaultDataSourceLoader.java |  62 +++++++
 .../loader/EnvironmentDataSourceLoader.java        |  77 +++++++++
 .../datasource/loader/FileDataSourceLoader.java    |  59 +++++++
 .../datasource/loader/HttpDataSourceLoader.java    |  67 ++++++++
 .../base/mime/MimetypesFileTypeMapFactory.java     |   4 +-
 .../template/TemplateTransformationsBuilder.java   |   8 +-
 .../generator/base/tools/ToolsFactory.java         |   3 +-
 .../freemarker/generator/base/uri/NamedUri.java    |  21 +++
 .../freemarker/generator/base/util/ListUtils.java  |  21 ---
 .../freemarker/generator/base/util/MapBuilder.java |   2 +
 .../generator/base/util/OperatingSystem.java       |   2 -
 .../generator/base/util/PropertiesTransformer.java |  12 --
 .../freemarker/generator/base/util/UriUtils.java   |  19 ---
 .../src/test/data/txt/utf16.txt                    | Bin 0 -> 362 bytes
 .../src/test/data/txt/utf8.txt                     |   6 +
 .../datasource/DataSourceFactoryTest.java          |  66 +-------
 .../generator/datasource/DataSourceLoaderTest.java | 187 +++++++++++++++++++++
 .../generator/datasource/DataSourceTest.java       |  39 +++--
 .../datasource/DataSourcesSupplierTest.java        |  24 +--
 .../generator/datasource/DataSourcesTest.java      |  94 ++++++-----
 .../generator/file/RecursiveFileSupplierTest.java  |   6 +-
 .../generator/uri/NamedUriStringParserTest.java    |   2 +
 freemarker-generator-cli/CHANGELOG.md              |  18 +-
 .../src/app/examples/templates/demo.ftl            |   2 +-
 .../cli/config/ConfigurationSupplier.java          |   2 +-
 .../generator/cli/config/DataModelSupplier.java    |  11 +-
 .../cli/model/GeneratorObjectWrapper.java          |   5 +
 .../cli/picocli/OutputGeneratorDefinition.java     |   4 +-
 .../src/site/markdown/cli/concepts/data-sources.md | 110 ++++++++++--
 .../freemarker/generator/cli/ManualTest.java       |   2 +-
 .../src/test/templates/manual.ftl                  |  47 +++---
 .../generator/maven/OutputGeneratorTest.java       |   2 +
 .../tools/dataframe/DataFrameToolTest.java         |   9 +-
 39 files changed, 938 insertions(+), 395 deletions(-)

diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
index be9934c..0bb4b57 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSource.java
@@ -34,7 +34,10 @@ import java.io.OutputStream;
 import java.io.StringWriter;
 import java.net.URI;
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
@@ -43,7 +46,7 @@ import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_APPLICATI
 
 /**
  * Data source which encapsulates data to be used for rendering
- * a template. When accessing content it is loaded on demand on not
+ * a template. When accessing content it is loaded on demand and not
  * kept in memory to allow processing of large volumes of data.
  * <br>
  * There is also special support of <code>UrlDataSource</code> since
@@ -55,14 +58,25 @@ import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_APPLICATI
  */
 public class DataSource implements Closeable, javax.activation.DataSource {
 
-    public static final String METADATA_BASE_NAME = "baseName";
+    public static final String METADATA_BASE_NAME = "basename";
     public static final String METADATA_EXTENSION = "extension";
-    public static final String METADATA_FILE_NAME = "fileName";
-    public static final String METADATA_FILE_PATH = "filePath";
+    public static final String METADATA_FILE_NAME = "filename";
+    public static final String METADATA_FILE_PATH = "filepath";
     public static final String METADATA_GROUP = "group";
     public static final String METADATA_NAME = "name";
     public static final String METADATA_URI = "uri";
-    public static final String METADATA_URI_PATH = "uriPath";
+    public static final String METADATA_MIME_TYPE = "mimetype";
+
+    public static final List<String> METADATA_KEYS = Arrays.asList(
+            METADATA_BASE_NAME,
+            METADATA_EXTENSION,
+            METADATA_FILE_NAME,
+            METADATA_FILE_PATH,
+            METADATA_GROUP,
+            METADATA_NAME,
+            METADATA_URI,
+            METADATA_MIME_TYPE
+    );
 
     /** Human-readable name of the data source */
     private final String name;
@@ -140,14 +154,32 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
         return group;
     }
 
+    /**
+     * Get the file name from the underlying "FileDataSource". All
+     * other data sources will return an empty string.
+     *
+     * @return file name or empty string
+     */
     public String getFileName() {
-        return FilenameUtils.getName(name);
+        return isFileDataSource() ? 
FilenameUtils.getName(dataSource.getName()) : "";
     }
 
+    /**
+     * Get the base name from the underlying "FileDataSource". All
+     * other data sources will return an empty string.
+     *
+     * @return base name or empty string
+     */
     public String getBaseName() {
         return FilenameUtils.getBaseName(getFileName());
     }
 
+    /**
+     * Get the extension from the underlying "FileDataSource". All
+     * other data sources will return an empty string.
+     *
+     * @return base name or empty string
+     */
     public String getExtension() {
         return FilenameUtils.getExtension(getFileName());
     }
@@ -180,11 +212,11 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
      * @return Length of data source or UNKNOWN_LENGTH
      */
     public long getLength() {
-        if (dataSource instanceof FileDataSource) {
+        if (isFileDataSource()) {
             return ((FileDataSource) dataSource).getFile().length();
-        } else if (dataSource instanceof StringDataSource) {
+        } else if (isStringDataSource()) {
             return ((StringDataSource) dataSource).length();
-        } else if (dataSource instanceof ByteArrayDataSource) {
+        } else if (isByteArrayDataSource()) {
             return ((ByteArrayDataSource) dataSource).length();
         } else {
             return DATASOURCE_UNKNOWN_LENGTH;
@@ -220,7 +252,7 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
     }
 
     /**
-     * Gets the contents of an <code>InputStream</code> as a list of Strings,
+     * Get the content of an <code>InputStream</code> as a list of Strings,
      * one entry per line, using the specified character encoding.
      *
      * @return the list of Strings, never null
@@ -246,7 +278,7 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
     }
 
     /**
-     * Returns an Iterator for the lines in an <code>InputStream</code>, using
+     * Returns an iterator for the lines in an <code>InputStream</code>, using
      * the default character encoding specified. The exposed iterator is closed
      * by the <code>DataSource</code>.
      *
@@ -282,7 +314,7 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
     }
 
     /**
-     * Expose various parts of the metadata as simple strings to cater for 
filtering in  a script.
+     * Expose various parts of the metadata as simple strings to cater for 
filtering in a script.
      *
      * @param key key part key
      * @return value
@@ -302,16 +334,26 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
                 return getGroup();
             case METADATA_NAME:
                 return getName();
-            case METADATA_URI_PATH:
-                return uri.getPath();
             case METADATA_URI:
                 return uri.toString();
+            case METADATA_MIME_TYPE:
+                return getMimeType();
             default:
                 throw new IllegalArgumentException("Unknown key: " + key);
         }
     }
 
     /**
+     * Get all metadata parts as map.
+     *
+     * @return Map of metadata parts
+     */
+    public Map<String, String> getMetadata() {
+        return METADATA_KEYS.stream()
+                .collect(Collectors.toMap(key -> key, this::getMetadata));
+    }
+
+    /**
      * Matches a metadata key with a wildcard expression.
      *
      * @param key      metadata key, e.g. "name", "fileName", "baseName", 
"extension", "uri", "group"
@@ -348,7 +390,8 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
 
     /**
      * If there is no content type we ask the underlying data source. E.g. for
-     * an URL data source this information is fetched from the remote server.
+     * an <code>URLDataSource</code> this information is fetched from the
+     * remote server.
      *
      * @return content type
      */
@@ -356,7 +399,20 @@ public class DataSource implements Closeable, 
javax.activation.DataSource {
         if (StringUtils.isNotEmpty(contentType)) {
             return contentType;
         } else {
-            return StringUtils.firstNonEmpty(dataSource.getContentType(), 
MIME_APPLICATION_OCTET_STREAM);
+            final String contentType = dataSource.getContentType();
+            return StringUtils.firstNonEmpty(contentType, 
MIME_APPLICATION_OCTET_STREAM);
         }
     }
-}
\ No newline at end of file
+
+    private boolean isFileDataSource() {
+        return dataSource instanceof FileDataSource;
+    }
+
+    private boolean isStringDataSource() {
+        return dataSource instanceof StringDataSource;
+    }
+
+    private boolean isByteArrayDataSource() {
+        return dataSource instanceof ByteArrayDataSource;
+    }
+}
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 0c9055c..c9467a8 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
@@ -22,10 +22,7 @@ import 
org.apache.freemarker.generator.base.activation.CachingUrlDataSource;
 import org.apache.freemarker.generator.base.activation.InputStreamDataSource;
 import org.apache.freemarker.generator.base.activation.StringDataSource;
 import org.apache.freemarker.generator.base.mime.MimetypesFileTypeMapFactory;
-import org.apache.freemarker.generator.base.uri.NamedUri;
-import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
 import org.apache.freemarker.generator.base.util.PropertiesFactory;
-import org.apache.freemarker.generator.base.util.StringUtils;
 import org.apache.freemarker.generator.base.util.UriUtils;
 import org.apache.freemarker.generator.base.util.Validate;
 
@@ -44,61 +41,26 @@ import java.util.UUID;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
-import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_PLAIN;
-import static 
org.apache.freemarker.generator.base.util.StringUtils.firstNonEmpty;
 
 /**
- * Creates a FreeMarker data source from various sources.
+ * Low-level factory to create FreeMarker data sources.
  */
 public abstract class DataSourceFactory {
 
-    private static final String NO_MIME_TYPE = null;
-    private static final Charset NO_CHARSET = null;
-    private static final String ROOT_DIR = "/";
-
     private DataSourceFactory() {
     }
 
-    // == NamedUri ==========================================================
-
-    public static DataSource fromNamedUri(String str) {
-        Validate.notNull(str, "namedUri is null");
-
-        return fromNamedUri(NamedUriStringParser.parse(str));
-    }
+    // == General ===========================================================
 
-    public static DataSource fromNamedUri(NamedUri namedUri) {
-        Validate.notNull(namedUri, "namedUri is null");
-
-        final URI uri = namedUri.getUri();
-        final String group = namedUri.getGroupOrElse(DEFAULT_GROUP);
-        final Charset charset = getCharsetOrElse(namedUri, NO_CHARSET);
-        final String mimeType = getMimeTypeOrElse(namedUri, NO_MIME_TYPE);
-
-        if (UriUtils.isHttpUri(uri)) {
-            final URL url = toUrl(uri);
-            final String name = 
namedUri.getNameOrElse(UriUtils.toStringWithoutFragment(uri));
-            return fromUrl(name, group, url, mimeType, charset);
-        } else if (UriUtils.isFileUri(uri)) {
-            final File file = namedUri.getFile();
-            final String name = 
namedUri.getNameOrElse(UriUtils.toStringWithoutFragment(file.toURI()));
-            return fromFile(name, group, file, charset);
-        } else if (UriUtils.isEnvUri(uri)) {
-            // environment variables come with a leading "/" to be removed
-            final String key = stripRootDir(uri.getPath());
-            final String contentType = getMimeTypeOrElse(namedUri, 
MIME_TEXT_PLAIN);
-            final String name = firstNonEmpty(namedUri.getName(), key, 
Location.ENVIRONMENT);
-            if (StringUtils.isEmpty(key)) {
-                return fromEnvironment(name, group, contentType);
-            } else {
-                return fromEnvironment(name, group, key, contentType);
-            }
-        } else {
-            // handle things such as "foo=some.file"
-            final File file = namedUri.getFile();
-            final String name = 
namedUri.getNameOrElse(UriUtils.toStringWithoutFragment(file.toURI()));
-            return fromFile(name, group, file, charset);
-        }
+    public static DataSource create(
+            String name,
+            String group,
+            URI uri,
+            javax.activation.DataSource dataSource,
+            String contentType,
+            Charset charset
+    ) {
+        return new DataSource(name, group, uri, dataSource, contentType, 
charset);
     }
 
     // == URL ===============================================================
@@ -120,7 +82,7 @@ public abstract class DataSourceFactory {
     // == File ==============================================================
 
     public static DataSource fromFile(File file, Charset charset) {
-        return fromFile(UriUtils.toStringWithoutFragment(file.toURI()), 
DEFAULT_GROUP, file, charset);
+        return fromFile(file.getName(), DEFAULT_GROUP, file, charset);
     }
 
     public static DataSource fromFile(String name, String group, File file, 
Charset charset) {
@@ -143,12 +105,18 @@ public abstract class DataSourceFactory {
     // == InputStream =======================================================
 
     public static DataSource fromInputStream(String name, String group, 
InputStream is, String contentType, Charset charset) {
-        final InputStreamDataSource dataSource = new 
InputStreamDataSource(name, is);
         final URI uri = UriUtils.toUri(Location.INPUTSTREAM + ":///");
-        return create(name, group, uri, dataSource, contentType, charset);
+        return fromInputStream(name, group, uri, is, contentType, charset);
     }
 
-    public static DataSource fromInputStream(String name, String group, URI 
uri, InputStream is, String contentType, Charset charset) {
+    public static DataSource fromInputStream(
+            String name,
+            String group,
+            URI uri,
+            InputStream is,
+            String contentType,
+            Charset charset
+    ) {
         final InputStreamDataSource dataSource = new 
InputStreamDataSource(name, is);
         return create(name, group, uri, dataSource, contentType, charset);
     }
@@ -164,7 +132,7 @@ public abstract class DataSourceFactory {
             final URI uri = UriUtils.toUri(Location.ENVIRONMENT, "");
             return create(name, group, uri, dataSource, contentType, UTF_8);
         } catch (IOException e) {
-            throw new RuntimeException(e);
+            throw new RuntimeException("Unable to load environment variables", 
e);
         }
     }
 
@@ -176,60 +144,11 @@ public abstract class DataSourceFactory {
         return create(name, group, uri, dataSource, contentType, UTF_8);
     }
 
-    // == General ===========================================================
-
-    /**
-     * Create a data source based on a
-     * <ul>
-     *  <li>URI</li>
-     *  <li>Named URI</li>
-     *  <li>file name</li>
-     * </ul>
-     *
-     * @param source source of the data source
-     * @return DataSource
-     */
-    public static DataSource create(String source) {
-        if (UriUtils.isUri(source)) {
-            return fromNamedUri(source);
-        } else {
-            final File file = new File(source);
-            return fromFile(file.getName(), DEFAULT_GROUP, file, UTF_8);
-        }
-    }
-
-    public static DataSource create(
-            String name,
-            String group,
-            URI uri,
-            javax.activation.DataSource dataSource,
-            String contentType,
-            Charset charset) {
-        return new DataSource(name, group, uri, dataSource, contentType, 
charset);
-    }
-
-    private static String getMimeTypeOrElse(NamedUri namedUri, String def) {
-        return namedUri.getParameter(NamedUri.MIMETYPE, def);
-    }
-
-    private static Charset getCharsetOrElse(NamedUri namedUri, Charset def) {
-        final String charsetName = namedUri.getParameter(NamedUri.CHARSET);
-        return StringUtils.isEmpty(charsetName) ? def : 
Charset.forName(charsetName);
-    }
-
-    private static URL toUrl(URI uri) {
+    public static URL toUrl(String url) {
         try {
-            return uri.toURL();
+            return new URL(url);
         } catch (MalformedURLException e) {
-            throw new IllegalArgumentException(uri.toString(), e);
-        }
-    }
-
-    private static String stripRootDir(String str) {
-        if (str.startsWith(ROOT_DIR)) {
-            return str.substring(ROOT_DIR.length());
-        } else {
-            return str;
+            throw new IllegalArgumentException(url, e);
         }
     }
 }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
similarity index 52%
copy from 
freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
copy to 
freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
index 2a4c84c..423b648 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoader.java
@@ -14,27 +14,34 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.freemarker.generator.base.util;
+package org.apache.freemarker.generator.base.datasource;
 
-import java.util.Locale;
+import java.nio.charset.Charset;
 
-/**
- * Helper class to detect the operting system (mostly Windows).
- *
- * TODO should be moved to "freemarker-generator-base"
- */
-public class OperatingSystem {
-    private static final String OS = System.getProperty("os.name", 
"unknown").toLowerCase(Locale.ROOT);
+public interface DataSourceLoader {
 
-    public static boolean isWindows() {
-        return OS.contains("win");
-    }
+    /**
+     * Check if the source would be accepted
+     *
+     * @param source source
+     * @return true if the instance wold be able to load a data source
+     */
+    boolean accept(String source);
 
-    public static boolean isMac() {
-        return OS.contains("mac");
-    }
+    /**
+     * Load a DataSource.
+     *
+     * @param source source of the data source
+     * @return DataSource
+     */
+    DataSource load(String source);
 
-    public static boolean isUnix() {
-        return OS.contains("nux");
-    }
+    /**
+     * Load a DataSource using the given charset.
+     *
+     * @param source source of the data source
+     * @param charset charset to use
+     * @return DataSource
+     */
+    DataSource load(String source, Charset charset);
 }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.java
new file mode 100644
index 0000000..b7c4a3e
--- /dev/null
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourceLoaderFactory.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.datasource;
+
+import 
org.apache.freemarker.generator.base.datasource.loader.DefaultDataSourceLoader;
+import 
org.apache.freemarker.generator.base.datasource.loader.EnvironmentDataSourceLoader;
+import 
org.apache.freemarker.generator.base.datasource.loader.FileDataSourceLoader;
+import 
org.apache.freemarker.generator.base.datasource.loader.HttpDataSourceLoader;
+
+import java.util.Arrays;
+
+/**
+ * Creates a FreeMarker data source from various sources.
+ */
+public abstract class DataSourceLoaderFactory {
+
+    private static DataSourceLoader dataSourceLoader;
+
+    public static synchronized DataSourceLoader create() {
+        if (dataSourceLoader == null) {
+            dataSourceLoader = new DefaultDataSourceLoader(
+                    Arrays.asList(
+                            new FileDataSourceLoader(),
+                            new HttpDataSourceLoader(),
+                            new EnvironmentDataSourceLoader()
+                    ));
+        }
+        return dataSourceLoader;
+    }
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
index 5af23bf..4df5c0c 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSources.java
@@ -76,7 +76,6 @@ public class DataSources implements Closeable {
         return dataSources.stream()
                 .map(DataSource::getGroup)
                 .filter(StringUtils::isNotEmpty)
-                .sorted()
                 .distinct()
                 .collect(Collectors.toList());
     }
@@ -100,13 +99,22 @@ public class DataSources implements Closeable {
 
     /**
      * Get a map representation of the underlying data sources.
+     * In <code>freemarker-cli</code> the map is also used to
+     * iterate over data source so we need to return a
+     * <code>LinkedHashMap</code>.
+     * <p>
+     * The implementation also throws as <code>IllegalStateException</code>
+     * when finding duplicate keys to avoid "losing" data sources.
      *
-     * @return map of data sources
+     * @return linked hasp map of data sources
      */
     public Map<String, DataSource> toMap() {
-        return 
dataSources.stream().collect(Collectors.toMap(DataSource::getName,
+        return dataSources.stream().collect(Collectors.toMap(
+                DataSource::getName,
                 identity(),
-                (v1, v2) -> v1,
+                (ds1, ds2) -> {
+                    throw new IllegalStateException("Duplicate key detected 
when generating map: " + ds1 + ", " + ds2);
+                },
                 LinkedHashMap::new));
     }
 
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
index cbf651b..c2604ba 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DataSourcesSupplier.java
@@ -40,6 +40,8 @@ import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_G
  */
 public class DataSourcesSupplier implements Supplier<List<DataSource>> {
 
+    private final DataSourceLoader dataSourceLoader;
+
     /** List of source files and/or directories */
     private final Collection<String> sources;
 
@@ -61,6 +63,7 @@ public class DataSourcesSupplier implements 
Supplier<List<DataSource>> {
      * @param charset The charset for loading text files
      */
     public DataSourcesSupplier(Collection<String> sources, String include, 
String exclude, Charset charset) {
+        this.dataSourceLoader = DataSourceLoaderFactory.create();
         this.sources = new ArrayList<>(sources);
         this.include = include;
         this.exclude = exclude;
@@ -97,8 +100,12 @@ public class DataSourcesSupplier implements 
Supplier<List<DataSource>> {
         }
     }
 
-    private static DataSource resolveHttpUrl(String source) {
-        return DataSourceFactory.create(source);
+    private DataSource resolveHttpUrl(String source) {
+        return dataSourceLoader.load(source);
+    }
+
+    private DataSource resolveEnvironment(String source) {
+        return dataSourceLoader.load(source);
     }
 
     private static List<DataSource> resolveFileOrDirectory(String source, 
String include, String exclude, Charset charset) {
@@ -111,11 +118,6 @@ public class DataSourcesSupplier implements 
Supplier<List<DataSource>> {
                 .collect(toList());
     }
 
-    private static DataSource resolveEnvironment(String source) {
-        final NamedUri namedUri = NamedUriStringParser.parse(source);
-        return DataSourceFactory.fromNamedUri(namedUri);
-    }
-
     private static RecursiveFileSupplier fileSupplier(String source, String 
include, String exclude) {
         return new RecursiveFileSupplier(singletonList(source), 
singletonList(include), singletonList(exclude));
     }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/DefaultDataSourceLoader.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/DefaultDataSourceLoader.java
new file mode 100644
index 0000000..467531c
--- /dev/null
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/DefaultDataSourceLoader.java
@@ -0,0 +1,62 @@
+/*
+ * 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.datasource.loader;
+
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Loads data source by delegating the loading to a list of "DataSourceLoader".
+ */
+public class DefaultDataSourceLoader implements DataSourceLoader {
+
+    private final List<DataSourceLoader> dataSourceLoaders;
+
+    public DefaultDataSourceLoader(List<DataSourceLoader> dataSourceLoaders) {
+        this.dataSourceLoaders = new 
ArrayList<>(requireNonNull(dataSourceLoaders));
+    }
+
+    @Override
+    public boolean accept(String source) {
+        return dataSourceLoaders.stream()
+                .anyMatch(loader -> loader.accept(source));
+    }
+
+    @Override
+    public DataSource load(String source) {
+        return get(source).load(source);
+    }
+
+    @Override
+    public DataSource load(String source, Charset charset) {
+        return get(source).load(source, charset);
+    }
+
+    private DataSourceLoader get(String source) {
+        return dataSourceLoaders.stream()
+                .filter(loader -> loader.accept(source))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("Don't know 
how to load: " + source));
+    }
+
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/EnvironmentDataSourceLoader.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/EnvironmentDataSourceLoader.java
new file mode 100644
index 0000000..41eeeef
--- /dev/null
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/EnvironmentDataSourceLoader.java
@@ -0,0 +1,77 @@
+/*
+ * 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.datasource.loader;
+
+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.DataSourceLoader;
+import org.apache.freemarker.generator.base.mime.Mimetypes;
+import org.apache.freemarker.generator.base.uri.NamedUri;
+import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
+import org.apache.freemarker.generator.base.util.StringUtils;
+
+import java.nio.charset.Charset;
+
+import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
+import static 
org.apache.freemarker.generator.base.util.StringUtils.firstNonEmpty;
+import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
+
+/**
+ * Load a DataSource based on a single environment variable or all 
environments variables.
+ */
+public class EnvironmentDataSourceLoader implements DataSourceLoader {
+
+    private static final String ROOT_DIR = "/";
+
+    @Override
+    public boolean accept(String source) {
+        return isNotEmpty(source) && source.contains("env://");
+    }
+
+    @Override
+    public DataSource load(String source) {
+        final NamedUri namedUri = NamedUriStringParser.parse(source);
+        final String key = stripRootDir(namedUri.getUri().getPath());
+        final String contentType = 
namedUri.getMimeTypeOrElse(Mimetypes.MIME_TEXT_PLAIN);
+        final String name = firstNonEmpty(namedUri.getName(), key, 
Location.ENVIRONMENT);
+        final String group = namedUri.getGroupOrElse(DEFAULT_GROUP);
+        if (StringUtils.isEmpty(key)) {
+            return DataSourceFactory.fromEnvironment(name, group, contentType);
+        } else {
+            return DataSourceFactory.fromEnvironment(name, group, key, 
contentType);
+        }
+    }
+
+    @Override
+    public DataSource load(String source, Charset charset) {
+        // We already habe internal strings so we can ignore the charset
+        return load(source);
+    }
+
+    /**
+     * Environment variables come with a leading "/" to be removed.
+     */
+    private static String stripRootDir(String value) {
+        if (value.startsWith(ROOT_DIR)) {
+            return value.substring(ROOT_DIR.length());
+        } else {
+            return value;
+        }
+    }
+
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/FileDataSourceLoader.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/FileDataSourceLoader.java
new file mode 100644
index 0000000..1ee8b66
--- /dev/null
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/FileDataSourceLoader.java
@@ -0,0 +1,59 @@
+/*
+ * 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.datasource.loader;
+
+import org.apache.freemarker.generator.base.FreeMarkerConstants;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.uri.NamedUri;
+import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
+import org.apache.freemarker.generator.base.util.UriUtils;
+
+import java.io.File;
+import java.nio.charset.Charset;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
+
+public class FileDataSourceLoader implements DataSourceLoader {
+
+    @Override
+    public boolean accept(String source) {
+        return isNotEmpty(source) && (!source.contains("://") || 
source.contains("file://"));
+    }
+
+    @Override
+    public DataSource load(String source) {
+        final NamedUri namedUri = NamedUriStringParser.parse(source);
+        final String group = 
namedUri.getGroupOrElse(FreeMarkerConstants.DEFAULT_GROUP);
+        final Charset charset = namedUri.getCharsetOrElse(UTF_8);
+        final File file = namedUri.getFile();
+        final String name = namedUri.getNameOrElse(file.getName());
+        return DataSourceFactory.fromFile(name, group, file, charset);
+    }
+
+    @Override
+    public DataSource load(String source, Charset charset) {
+        final NamedUri namedUri = NamedUriStringParser.parse(source);
+        final String group = 
namedUri.getGroupOrElse(FreeMarkerConstants.DEFAULT_GROUP);
+        final File file = namedUri.getFile();
+        final String name = 
namedUri.getNameOrElse(UriUtils.toStringWithoutFragment(file.toURI()));
+        return DataSourceFactory.fromFile(name, group, file, charset);
+    }
+
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/HttpDataSourceLoader.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/HttpDataSourceLoader.java
new file mode 100644
index 0000000..10755f9
--- /dev/null
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/loader/HttpDataSourceLoader.java
@@ -0,0 +1,67 @@
+/*
+ * 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.datasource.loader;
+
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.uri.NamedUri;
+import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
+import org.apache.freemarker.generator.base.util.UriUtils;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.Charset;
+
+import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
+import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
+
+public class HttpDataSourceLoader implements DataSourceLoader {
+
+    @Override
+    public boolean accept(String source) {
+        return isNotEmpty(source) && (source.contains("http://";) || 
source.contains("https://";));
+    }
+
+    @Override
+    public DataSource load(String source) {
+        final NamedUri namedUri = NamedUriStringParser.parse(source);
+        final URI uri = namedUri.getUri();
+        final String group = namedUri.getGroupOrElse(DEFAULT_GROUP);
+        final Charset charset = namedUri.getCharsetOrElse(null);
+        final String mimeType = namedUri.getMimeType();
+        final URL url = toUrl(uri);
+        final String name = 
namedUri.getNameOrElse(UriUtils.toStringWithoutFragment(uri));
+        return DataSourceFactory.fromUrl(name, group, url, mimeType, charset);
+    }
+
+    @Override
+    public DataSource load(String source, Charset charset) {
+        // We should pick up the charset from the HTTP server
+        return load(source);
+    }
+
+    private static URL toUrl(URI uri) {
+        try {
+            return uri.toURL();
+        } catch (MalformedURLException e) {
+            throw new IllegalArgumentException(uri.toString(), e);
+        }
+    }
+
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
index 56e9efa..f8bbf2a 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/mime/MimetypesFileTypeMapFactory.java
@@ -46,7 +46,7 @@ public class MimetypesFileTypeMapFactory {
             mimeTypes.addMimeTypes(MIME_APPLICATION_XML + " xml XML");
             mimeTypes.addMimeTypes(MIME_APPLICATION_XHTML + " xhtml XHTML");
             mimeTypes.addMimeTypes(MIME_TEXT_CSV + " csv CSV");
-            mimeTypes.addMimeTypes(MIME_TEXT_PLAIN + " adoc ADOC bat BAT env 
ENV ftl FTL ini INI log LOG properties txt TXT");
+            mimeTypes.addMimeTypes(MIME_TEXT_PLAIN + " adoc ADOC bat BAT env 
ENV ftl FTL ini INI log LOG properties PROPERTIES txt TXT");
             mimeTypes.addMimeTypes(MIME_TEXT_HTML + " htm HTM html HTML");
             mimeTypes.addMimeTypes(MIME_TEXT_MARKDOWM + " md MD");
             mimeTypes.addMimeTypes(MIME_TEXT_RTF + " rtf RTF");
@@ -56,4 +56,4 @@ public class MimetypesFileTypeMapFactory {
 
         return mimeTypes;
     }
-}
\ No newline at end of file
+}
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
index 6aaf494..c9b0814 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/template/TemplateTransformationsBuilder.java
@@ -18,7 +18,8 @@ package org.apache.freemarker.generator.base.template;
 
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
 import org.apache.freemarker.generator.base.datasource.DataSource;
-import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
 import org.apache.freemarker.generator.base.file.RecursiveFileSupplier;
 import org.apache.freemarker.generator.base.util.NonClosableWriterWrapper;
 import org.apache.freemarker.generator.base.util.StringUtils;
@@ -41,6 +42,8 @@ import static java.util.Collections.singletonList;
  */
 public class TemplateTransformationsBuilder {
 
+    private final DataSourceLoader dataSourceLoader;
+
     /** Interactive template */
     private TemplateSource interactiveTemplate;
 
@@ -66,6 +69,7 @@ public class TemplateTransformationsBuilder {
     private Writer callerSuppliedWriter;
 
     private TemplateTransformationsBuilder() {
+        this.dataSourceLoader = DataSourceLoaderFactory.create();
         this.templateSource = null;
         this.includes = new ArrayList<>();
         this.excludes = new ArrayList<>();
@@ -228,7 +232,7 @@ public class TemplateTransformationsBuilder {
     }
 
     private TemplateSource templateSource(String source) {
-        try (DataSource dataSource = DataSourceFactory.create(source)) {
+        try (DataSource dataSource = dataSourceLoader.load(source)) {
             return TemplateSource.fromCode(dataSource.getName(), 
dataSource.getText(templateEncoding.name()));
         }
     }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/tools/ToolsFactory.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/tools/ToolsFactory.java
index 00a202b..607a165 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/tools/ToolsFactory.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/tools/ToolsFactory.java
@@ -36,7 +36,8 @@ public class ToolsFactory {
         }
 
         try {
-            return forName(clazzName) != null;
+            forName(clazzName);
+            return true;
         } catch (NoClassDefFoundError | ClassNotFoundException e) {
             return false;
         }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
index 08b7253..6003d3c 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
@@ -16,8 +16,11 @@
  */
 package org.apache.freemarker.generator.base.uri;
 
+import org.apache.freemarker.generator.base.util.StringUtils;
+
 import java.io.File;
 import java.net.URI;
+import java.nio.charset.Charset;
 import java.util.Map;
 
 import static java.util.Objects.requireNonNull;
@@ -102,6 +105,24 @@ public class NamedUri {
         return new File(uri.getPath());
     }
 
+    public String getMimeType() {
+        return getParameter(NamedUri.MIMETYPE);
+    }
+
+    public String getMimeTypeOrElse(String def) {
+        return getParameter(NamedUri.MIMETYPE, def);
+    }
+
+    public Charset getCharset() {
+        final String charsetName = getParameter(NamedUri.CHARSET);
+        return Charset.forName(charsetName);
+    }
+
+    public Charset getCharsetOrElse(Charset def) {
+        final String charsetName = getParameter(NamedUri.CHARSET);
+        return StringUtils.isEmpty(charsetName) ? def : 
Charset.forName(charsetName);
+    }
+
     @Override
     public String toString() {
         return "NamedUri{" +
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
index 613b3e1..4ec6fbc 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/ListUtils.java
@@ -26,10 +26,6 @@ public class ListUtils {
         return list == null || list.isEmpty();
     }
 
-    public static <T> boolean isNotEmpty(final List<T> list) {
-        return !isNullOrEmpty(list);
-    }
-
     /**
      * Transposes the given tabular data, swapping rows with columns.
      *
@@ -67,21 +63,4 @@ public class ListUtils {
                 .findFirst()
                 .orElse(null);
     }
-
-    /**
-     * Copy an array to another array while casting to <code>R</code>.
-     *
-     * @param array array to copy
-     * @param <T>   the source type of the array
-     * @param <R>   the target type of the array
-     * @return copied array
-     */
-    @SuppressWarnings("unchecked")
-    public static <T, R> List<R> copy(final List<T> array) {
-        final List<R> result = new ArrayList<>();
-        for (int i = 0; i < array.size(); i++) {
-            result.set(i, (R) array.get(i));
-        }
-        return result;
-    }
 }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
index 561c13b..574ddb0 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/MapBuilder.java
@@ -45,6 +45,8 @@ public class MapBuilder {
                 case 1:
                     map.put(currKey, value);
                     break;
+                default:
+                    throw new RuntimeException("That should never happen");
             }
         }
 
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
index 2a4c84c..180b7ac 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/OperatingSystem.java
@@ -20,8 +20,6 @@ import java.util.Locale;
 
 /**
  * Helper class to detect the operting system (mostly Windows).
- *
- * TODO should be moved to "freemarker-generator-base"
  */
 public class OperatingSystem {
     private static final String OS = System.getProperty("os.name", 
"unknown").toLowerCase(Locale.ROOT);
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
index a863b8d..90b2f6a 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/PropertiesTransformer.java
@@ -51,16 +51,4 @@ public class PropertiesTransformer {
         properties.forEach((key, value) -> 
result.put(key.toString().substring(prefix.length()), value));
         return result;
     }
-
-    /**
-     * Copy a entries in a new <code>java.util.Properties</code> instance.
-     *
-     * @param properties the properties
-     * @return properties
-     */
-    public static Properties copy(Properties properties) {
-        final Properties result = new Properties();
-        properties.forEach((key, value) -> result.setProperty((String) key, 
(String) value));
-        return result;
-    }
 }
diff --git 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
index b8135bb..74d620c 100644
--- 
a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
+++ 
b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/UriUtils.java
@@ -21,7 +21,6 @@ import java.net.URISyntaxException;
 import java.net.URL;
 
 import static org.apache.commons.io.FilenameUtils.separatorsToUnix;
-import static org.apache.freemarker.generator.base.util.StringUtils.isNotEmpty;
 
 public class UriUtils {
 
@@ -53,24 +52,6 @@ public class UriUtils {
         return index > 0 ? str.substring(0, index) : str;
     }
 
-    public static boolean isUri(String str) {
-        return isNotEmpty(str) && str.contains("://");
-    }
-
-    public static boolean isHttpUri(URI uri) {
-        if (uri == null) {
-            return false;
-        }
-        return "http".equalsIgnoreCase(uri.getScheme()) || 
"https".equalsIgnoreCase(uri.getScheme());
-    }
-
-    public static boolean isFileUri(URI uri) {
-        if (uri == null) {
-            return false;
-        }
-        return "file".equalsIgnoreCase(uri.getScheme());
-    }
-
     public static boolean isEnvUri(URI uri) {
         if (uri == null) {
             return false;
diff --git a/freemarker-generator-base/src/test/data/txt/utf16.txt 
b/freemarker-generator-base/src/test/data/txt/utf16.txt
new file mode 100755
index 0000000..6dfd2e5
Binary files /dev/null and 
b/freemarker-generator-base/src/test/data/txt/utf16.txt differ
diff --git a/freemarker-generator-base/src/test/data/txt/utf8.txt 
b/freemarker-generator-base/src/test/data/txt/utf8.txt
new file mode 100755
index 0000000..25f1e5c
--- /dev/null
+++ b/freemarker-generator-base/src/test/data/txt/utf8.txt
@@ -0,0 +1,6 @@
+UTF-8 encoding
+=============================================================================
+première is first
+première is slightly different
+Кириллица is Cyrillic
+𐐀 am Deseret
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceFactoryTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceFactoryTest.java
index 0610e2d..37d1f92 100644
--- 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceFactoryTest.java
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceFactoryTest.java
@@ -16,11 +16,10 @@
  */
 package org.apache.freemarker.generator.datasource;
 
-import org.apache.commons.io.FilenameUtils;
+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.uri.NamedUri;
-import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
+import org.apache.freemarker.generator.base.util.UriUtils;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -28,10 +27,10 @@ import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
 import java.net.URL;
 import java.nio.charset.Charset;
 
-import static java.lang.String.format;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
@@ -40,14 +39,10 @@ import static org.junit.Assert.assertEquals;
 
 public class DataSourceFactoryTest {
 
-    private static final String PWD = FilenameUtils.separatorsToUnix(new 
File("").getAbsolutePath());
     private static final String ANY_TEXT = "Hello World";
     private static final String ANY_FILE_NAME = "pom.xml";
-    private static final String ANY_FILE_URI = format("file:///%s/pom.xml", 
PWD);
     private static final Charset ANY_CHAR_SET = UTF_8;
     private static final File ANY_FILE = new File(ANY_FILE_NAME);
-    private static final String ANY_ENV_VARIABLE = "JAVA_HOME";
-    private static final String ANY_NAMED_URL_STRING = 
"content:www=https://www.google.com?foo=bar#contenttype=application/json";;
 
     @Test
     public void shouldCreateDataSourceFromFile() {
@@ -61,17 +56,6 @@ public class DataSourceFactoryTest {
     }
 
     @Test
-    public void shouldCreateDataSourceFromFileUri() {
-        final DataSource dataSource = DataSourceFactory.create(ANY_FILE_URI);
-
-        assertEquals(ANY_FILE_NAME, dataSource.getFileName());
-        assertEquals(UTF_8, dataSource.getCharset());
-        assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
-        assertEquals(ANY_FILE.toURI(), dataSource.getUri());
-        assertTrue(!dataSource.getLines().isEmpty());
-    }
-
-    @Test
     public void shouldCreateDataSourceFromString() {
         final DataSource dataSource = DataSourceFactory.fromString("test.txt", 
"default", ANY_TEXT, "text/plain");
 
@@ -99,8 +83,9 @@ public class DataSourceFactoryTest {
 
     @Test
     public void shouldCreateDataSourceFromInputStream() {
+        final URI uri = UriUtils.toUri(Location.INPUTSTREAM + ":///");
         final InputStream is = new 
ByteArrayInputStream(ANY_TEXT.getBytes(UTF_8));
-        final DataSource dataSource = 
DataSourceFactory.fromInputStream("test.txt", "default", is, "text/plain", 
UTF_8);
+        final DataSource dataSource = 
DataSourceFactory.fromInputStream("test.txt", "default", uri, is, "text/plain", 
UTF_8);
 
         assertEquals("test.txt", dataSource.getName());
         assertEquals(UTF_8, dataSource.getCharset());
@@ -116,26 +101,14 @@ public class DataSourceFactoryTest {
         final DataSource dataSource = 
DataSourceFactory.fromUrl("jsonplaceholder.typicode.com", "default", url, null, 
null);
 
         assertEquals("jsonplaceholder.typicode.com", dataSource.getName());
+        assertEquals("jsonplaceholder.typicode.com", dataSource.getFileName());
         assertEquals("application/json", dataSource.getContentType());
         assertEquals(UTF_8, dataSource.getCharset());
     }
 
     @Test
-    @Ignore
-    public void shouldCreateDataSourceFromNamedURL() {
-        final NamedUri namedUri = 
NamedUriStringParser.parse(ANY_NAMED_URL_STRING);
-        final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
-
-        assertEquals(namedUri.getName(), dataSource.getName());
-        assertEquals(namedUri.getGroup(), dataSource.getGroup());
-        assertEquals("ISO-8859-1", dataSource.getCharset().toString());
-        assertEquals(namedUri.getUri().toString(), 
dataSource.getUri().toString());
-    }
-
-    @Test
     public void shouldCreateDataSourceFromEnvironment() {
-        final NamedUri namedUri = NamedUriStringParser.parse("env:///");
-        final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
+        final DataSource dataSource = DataSourceFactory.fromEnvironment("env", 
"default", "text/plain");
 
         assertEquals("env", dataSource.getName());
         assertEquals("default", dataSource.getGroup());
@@ -143,29 +116,4 @@ public class DataSourceFactoryTest {
         assertEquals("env:///", dataSource.getUri().toString());
         assertEquals("text/plain", dataSource.getContentType());
     }
-
-    @Test
-    public void shouldCreateDataSourceFromNamedEnvironment() {
-        final NamedUri namedUri = NamedUriStringParser.parse("config=env:///");
-        final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
-
-        assertEquals("config", dataSource.getName());
-        assertEquals("default", dataSource.getGroup());
-        assertEquals(UTF_8, dataSource.getCharset());
-        assertEquals("env:///", dataSource.getUri().toString());
-        assertEquals("text/plain", dataSource.getContentType());
-    }
-
-    @Test
-    public void shouldCreateDataSourceFromEnvironmentVariable() {
-        final String uri = "env:///" + ANY_ENV_VARIABLE;
-        final NamedUri namedUri = NamedUriStringParser.parse("myenv=" + uri);
-        final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
-
-        assertEquals("myenv", dataSource.getName());
-        assertEquals("default", dataSource.getGroup());
-        assertEquals(UTF_8, dataSource.getCharset());
-        assertEquals(uri, dataSource.getUri().toString());
-        assertEquals("text/plain", dataSource.getContentType());
-    }
 }
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceLoaderTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceLoaderTest.java
new file mode 100644
index 0000000..1567699
--- /dev/null
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceLoaderTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.datasource;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_16;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static junit.framework.TestCase.assertFalse;
+import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
+import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_APPLICATION_XML;
+import static org.junit.Assert.assertEquals;
+
+public class DataSourceLoaderTest {
+
+    private static final String PWD = FilenameUtils.separatorsToUnix(new 
File("").getAbsolutePath());
+    private static final String ANY_FILE_NAME = "pom.xml";
+    private static final String ANY_ABSOLUTE_FILE_NAME = format("%s/pom.xml", 
PWD);
+    private static final String ANY_FILE_URI = format("file:///%s/pom.xml", 
PWD);
+    private static final File ANY_FILE = new File(ANY_FILE_NAME);
+
+    @Test
+    public void shouldLoadDataSourceFromFileName() {
+        try (DataSource dataSource = dataSourceLoader().load(ANY_FILE_NAME)) {
+            assertEquals(ANY_FILE_NAME, dataSource.getName());
+            assertEquals(DEFAULT_GROUP, dataSource.getGroup());
+            assertEquals(ANY_FILE_NAME, dataSource.getFileName());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getMimeType());
+            assertEquals(ANY_FILE.toURI(), dataSource.getUri());
+            assertFalse(dataSource.getLines().isEmpty());
+        }
+    }
+
+    @Test
+    public void shouldLoadDataSourceFromAbsoluteFileName() {
+        try (DataSource dataSource = 
dataSourceLoader().load(ANY_ABSOLUTE_FILE_NAME)) {
+            assertEquals(ANY_FILE_NAME, dataSource.getName());
+            assertEquals(DEFAULT_GROUP, dataSource.getGroup());
+            assertEquals(ANY_FILE_NAME, dataSource.getFileName());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getMimeType());
+            assertEquals(ANY_FILE.toURI(), dataSource.getUri());
+            assertFalse(dataSource.getLines().isEmpty());
+        }
+    }
+
+    @Test
+    public void shouldLoadDataSourceFromFileUri() {
+        try (DataSource dataSource = dataSourceLoader().load(ANY_FILE_URI)) {
+            assertEquals(ANY_FILE_NAME, dataSource.getName());
+            assertEquals(DEFAULT_GROUP, dataSource.getGroup());
+            assertEquals(ANY_FILE_NAME, dataSource.getFileName());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getMimeType());
+            assertEquals(ANY_FILE.toURI(), dataSource.getUri());
+            assertFalse(dataSource.getLines().isEmpty());
+        }
+    }
+
+    @Test
+    public void shouldLoadDataSourceFromSimpleNameFileUri() {
+        try (DataSource dataSource = 
dataSourceLoader().load("source=pom.xml")) {
+            assertEquals("pom.xml", dataSource.getFileName());
+            assertEquals("source", dataSource.getName());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getMimeType());
+            assertEquals(ANY_FILE.toURI(), dataSource.getUri());
+            assertFalse(dataSource.getLines().isEmpty());
+        }
+    }
+
+    @Test
+    public void shouldLoadDataSourceFromComplexNameFileUri() {
+        try (DataSource dataSource = 
dataSourceLoader().load("source=pom.xml#charset=UTF-8&foo=bar")) {
+            assertEquals("pom.xml", dataSource.getFileName());
+            assertEquals("source", dataSource.getName());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals(MIME_APPLICATION_XML, dataSource.getContentType());
+            assertEquals(ANY_FILE.toURI(), dataSource.getUri());
+            assertFalse(dataSource.getLines().isEmpty());
+        }
+    }
+
+    @Test
+    @Ignore("Requires internet access")
+    public void shouldCreateDataSourceFromUrl() {
+        try (DataSource dataSource = 
dataSourceLoader().load("https://jsonplaceholder.typicode.com/posts/2";)) {
+            assertEquals("https://jsonplaceholder.typicode.com/posts/2";, 
dataSource.getName());
+            assertEquals("", dataSource.getFileName());
+            assertEquals("", dataSource.getBaseName());
+            assertEquals("", dataSource.getExtension());
+            assertEquals("application/json; charset=utf-8", 
dataSource.getContentType());
+            assertEquals("application/json", dataSource.getMimeType());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals("https://jsonplaceholder.typicode.com/posts/2";, 
dataSource.getUri().toString());
+        }
+    }
+
+    @Test
+    @Ignore("Requires internet access")
+    public void shouldCreateDataSourceFromNamedURL() {
+        try (DataSource dataSource = 
dataSourceLoader().load("content:www=https://www.google.com?foo=bar#contenttype=application/json";))
 {
+            assertEquals("content", dataSource.getName());
+            assertEquals("", dataSource.getFileName());
+            assertEquals("", dataSource.getBaseName());
+            assertEquals("", dataSource.getExtension());
+            assertEquals("www", dataSource.getGroup());
+            assertEquals("text/html; charset=ISO-8859-1", 
dataSource.getContentType());
+            assertEquals("text/html", dataSource.getMimeType());
+            assertEquals("ISO-8859-1", dataSource.getCharset().toString());
+            
assertEquals("https://www.google.com?foo=bar#contenttype=application/json";, 
dataSource.getUri().toString());
+        }
+    }
+
+    @Test
+    public void shouldCreateDataSourceFromEnvironment() {
+        try (DataSource dataSource = dataSourceLoader().load("env:///")) {
+            assertEquals("env", dataSource.getName());
+            assertEquals("default", dataSource.getGroup());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals("env:///", dataSource.getUri().toString());
+            assertEquals("text/plain", dataSource.getContentType());
+        }
+    }
+
+    @Test
+    public void shouldCreateDataSourceFromNamedEnvironment() {
+        try (DataSource dataSource = 
dataSourceLoader().load("config=env:///")) {
+            assertEquals("config", dataSource.getName());
+            assertEquals("default", dataSource.getGroup());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals("env:///", dataSource.getUri().toString());
+            assertEquals("text/plain", dataSource.getContentType());
+        }
+    }
+
+    @Test
+    public void shouldCreateDataSourceFromEnvironmentVariable() {
+        try (DataSource dataSource = 
dataSourceLoader().load("myenv=env:///HOME")) {
+            assertEquals("myenv", dataSource.getName());
+            assertEquals("default", dataSource.getGroup());
+            assertEquals(UTF_8, dataSource.getCharset());
+            assertEquals("env:///HOME", dataSource.getUri().toString());
+            assertEquals("text/plain", dataSource.getContentType());
+        }
+    }
+
+    @Test
+    public void shouldLoadDataSourceWithCharset() {
+        final DataSource utf8DataSource = 
dataSourceLoader().load("./src/test/data/txt/utf8.txt", UTF_8);
+        final DataSource utf16DataSource = 
dataSourceLoader().load("./src/test/data/txt/utf16.txt", UTF_16);
+
+        // skip the first line before comparing
+        assertEquals(utf8DataSource.getLines().subList(1, 5), 
utf16DataSource.getLines().subList(1, 5));
+    }
+
+    private DataSourceLoader dataSourceLoader() {
+        return DataSourceLoaderFactory.create();
+    }
+}
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
index 155b778..8961359 100644
--- 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourceTest.java
@@ -31,6 +31,7 @@ import java.util.Iterator;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
+import static 
org.apache.freemarker.generator.base.datasource.DataSourceFactory.toUrl;
 import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_HTML;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -49,7 +50,7 @@ public class DataSourceTest {
         try (DataSource dataSource = DataSourceFactory.fromString("stdin", 
ANY_GROUP, ANY_TEXT, Mimetypes.MIME_TEXT_PLAIN)) {
             assertEquals("stdin", dataSource.getName());
             assertEquals(ANY_GROUP, dataSource.getGroup());
-            assertEquals("stdin", dataSource.getBaseName());
+            assertEquals("", dataSource.getBaseName());
             assertEquals("", dataSource.getExtension());
             
assertTrue(dataSource.getUri().toString().startsWith("string:///"));
             assertEquals(UTF_8, dataSource.getCharset());
@@ -76,19 +77,20 @@ public class DataSourceTest {
             assertTrue(dataSource.match("name", "*" + ANY_FILE_NAME));
             assertTrue(dataSource.match("uri", "file:/*/pom.xml"));
             assertTrue(dataSource.match("extension", "xml"));
-            assertTrue(dataSource.match("baseName", "pom"));
+            assertTrue(dataSource.match("basename", "pom"));
         }
     }
 
-    @Ignore("Requires internet connection")
     @Test
+    @Ignore("Requires internet access")
     public void shouldSupportUrlDataSource() {
-        try (DataSource dataSource = 
DataSourceFactory.create("https://www.google.com/?foo=bar";)) {
+        try (DataSource dataSource = 
DataSourceFactory.fromUrl("www.google.com", DEFAULT_GROUP, 
toUrl("https://www.google.com/?foo=bar";), null, null)) {
             assertEquals("www.google.com", dataSource.getName());
             assertEquals(DEFAULT_GROUP, dataSource.getGroup());
-            assertEquals("www.google", dataSource.getBaseName());
-            assertEquals("com", dataSource.getExtension());
+            assertEquals("", dataSource.getBaseName());
+            assertEquals("", dataSource.getExtension());
             assertEquals("https://www.google.com/?foo=bar";, 
dataSource.getUri().toString());
+            assertEquals("text/html; charset=ISO-8859-1", 
dataSource.getContentType());
             assertEquals(MIME_TEXT_HTML, dataSource.getMimeType());
             assertEquals("ISO-8859-1", dataSource.getCharset().name());
             assertEquals(-1, dataSource.getLength());
@@ -98,7 +100,7 @@ public class DataSourceTest {
 
     @Test
     public void shouldSupportLineIterator() throws IOException {
-        try (DataSource dataSource = textDataSource()) {
+        try (DataSource dataSource = stringDataSource()) {
             try (LineIterator iterator = dataSource.getLineIterator()) {
                 assertEquals(1, count(iterator));
             }
@@ -107,7 +109,7 @@ public class DataSourceTest {
 
     @Test
     public void shouldReadLines() {
-        try (DataSource dataSource = textDataSource()) {
+        try (DataSource dataSource = stringDataSource()) {
             assertEquals(1, dataSource.getLines().size());
             assertEquals(ANY_TEXT, dataSource.getLines().get(0));
         }
@@ -115,14 +117,29 @@ public class DataSourceTest {
 
     @Test
     public void shouldGetBytes() {
-        try (DataSource dataSource = textDataSource()) {
+        try (DataSource dataSource = stringDataSource()) {
             assertEquals(11, dataSource.getBytes().length);
         }
     }
 
     @Test
+    public void shouldGetMetadata() {
+        try (DataSource dataSource = stringDataSource()) {
+            assertEquals(8, dataSource.getMetadata().size());
+            assertEquals("", dataSource.getMetadata().get("basename"));
+            assertEquals("", dataSource.getMetadata().get("extension"));
+            assertEquals("", dataSource.getMetadata().get("filename"));
+            assertEquals("/", dataSource.getMetadata().get("filepath"));
+            assertEquals("default", dataSource.getMetadata().get("group"));
+            assertEquals("stdin", dataSource.getMetadata().get("name"));
+            
assertTrue(dataSource.getMetadata().get("uri").startsWith("string://"));
+            assertEquals("text/plain", 
dataSource.getMetadata().get("mimetype"));
+        }
+    }
+
+    @Test
     public void shouldCloseDataSource() {
-        final DataSource dataSource = textDataSource();
+        final DataSource dataSource = stringDataSource();
         final TestClosable closable1 = dataSource.addClosable(new 
TestClosable());
         final TestClosable closable2 = dataSource.addClosable(new 
TestClosable());
 
@@ -141,7 +158,7 @@ public class DataSourceTest {
         return count;
     }
 
-    private static DataSource textDataSource() {
+    private static DataSource stringDataSource() {
         return DataSourceFactory.fromString("stdin", "default", ANY_TEXT, 
"text/plain");
     }
 
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 fddc597..779485c 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
@@ -64,12 +64,12 @@ public class DataSourcesSupplierTest {
 
     @Test
     public void shouldResolveDirectory() {
-        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(7, supplier(DATA_DIRECTORY, null, null).get().size());
+        assertEquals(7, supplier(DATA_DIRECTORY, "", null).get().size());
+        assertEquals(7, supplier(DATA_DIRECTORY, "*", null).get().size());
+        assertEquals(7, 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(3, supplier(DATA_DIRECTORY, "*.t*", null).get().size());
         assertEquals(0, supplier(DATA_DIRECTORY, "*.bin", null).get().size());
     }
 
@@ -77,12 +77,12 @@ public class DataSourcesSupplierTest {
     public void shouldResolveFilesAndDirectory() {
         final List<String> sources = Arrays.asList("pom.xml", "README.md", 
DATA_DIRECTORY);
 
-        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(9, supplier(sources, null, null).get().size());
+        assertEquals(9, supplier(sources, "", null).get().size());
+        assertEquals(9, supplier(sources, "*", null).get().size());
+        assertEquals(9, supplier(sources, "*.*", null).get().size());
         assertEquals(2, supplier(sources, "*.csv", null).get().size());
-        assertEquals(1, supplier(sources, "*.t*", null).get().size());
+        assertEquals(3, supplier(sources, "*.t*", null).get().size());
         assertEquals(1, supplier(sources, "*.xml", null).get().size());
         assertEquals(0, supplier(sources, "*.bin", null).get().size());
 
@@ -90,8 +90,8 @@ public class DataSourcesSupplierTest {
         assertEquals(0, supplier(sources, null, "*.*").get().size());
         assertEquals(0, supplier(sources, "*", "*").get().size());
 
-        assertEquals(6, supplier(sources, "*", "*.md").get().size());
-        assertEquals(4, supplier(sources, "*", "file*.*").get().size());
+        assertEquals(8, supplier(sources, "*", "*.md").get().size());
+        assertEquals(6, supplier(sources, "*", "file*.*").get().size());
     }
 
     @Test
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
index d3cdf45..3591e38 100644
--- 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/datasource/DataSourcesTest.java
@@ -22,6 +22,8 @@ import 
org.apache.freemarker.generator.base.datasource.DataSources;
 import org.junit.Test;
 
 import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
@@ -30,6 +32,7 @@ import static 
org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_G
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class DataSourcesTest {
 
@@ -43,66 +46,63 @@ public class DataSourcesTest {
 
     @Test
     public void shouldFindByName() {
-        final DataSources dataSources = dataSources();
-
-        assertEquals(0, dataSources.find(null).size());
-        assertEquals(0, dataSources.find("").size());
-        assertEquals(0, dataSources.find("*.bar").size());
-        assertEquals(0, dataSources.find("foo.*").size());
-        assertEquals(0, dataSources.find("foo.bar").size());
-
-        assertEquals(2, dataSources.find("*.*").size());
-        assertEquals(1, dataSources.find("*." + ANY_FILE_EXTENSION).size());
-        assertEquals(1, dataSources.find("*/*." + ANY_FILE_EXTENSION).size());
-        assertEquals(1, dataSources.find("*.???").size());
-        assertEquals(1, dataSources.find("*om*").size());
-        assertEquals(1, dataSources.find("*o*.xml").size());
-
-        assertEquals(3, dataSources.find("*").size());
+        try (DataSources dataSources = dataSources()) {
+            assertEquals(0, dataSources.find(null).size());
+            assertEquals(0, dataSources.find("").size());
+            assertEquals(0, dataSources.find("*.bar").size());
+            assertEquals(0, dataSources.find("foo.*").size());
+            assertEquals(0, dataSources.find("foo.bar").size());
+
+            assertEquals(2, dataSources.find("*.*").size());
+            assertEquals(1, dataSources.find("*." + 
ANY_FILE_EXTENSION).size());
+            assertEquals(1, dataSources.find("*.???").size());
+            assertEquals(1, dataSources.find("*om*").size());
+            assertEquals(1, dataSources.find("*o*.xml").size());
+
+            assertEquals(3, dataSources.find("*").size());
+        }
     }
 
     @Test
     public void shouldFindByGroupPart() {
-        final DataSources dataSources = dataSources();
+        try (DataSources dataSources = dataSources()) {
 
-        assertEquals(0, dataSources.find(GROUP_PART, null).size());
-        assertEquals(0, dataSources.find(GROUP_PART, "").size());
+            assertEquals(0, dataSources.find(GROUP_PART, null).size());
+            assertEquals(0, dataSources.find(GROUP_PART, "").size());
 
-        assertEquals(0, dataSources.find(GROUP_PART, "unknown").size());
-
-        assertEquals(3, dataSources.find(GROUP_PART, "*").size());
-        assertEquals(3, dataSources.find(GROUP_PART, "default").size());
-        assertEquals(3, dataSources.find(GROUP_PART, "d*").size());
-        assertEquals(3, dataSources.find(GROUP_PART, "d??????").size());
+            assertEquals(0, dataSources.find(GROUP_PART, "unknown").size());
 
+            assertEquals(3, dataSources.find(GROUP_PART, "*").size());
+            assertEquals(3, dataSources.find(GROUP_PART, "default").size());
+            assertEquals(3, dataSources.find(GROUP_PART, "d*").size());
+            assertEquals(3, dataSources.find(GROUP_PART, "d??????").size());
+        }
     }
 
     @Test
     public void shouldGetDataSource() {
-        assertNotNull(dataSources().get("*/" + ANY_FILE_NAME));
+        assertNotNull(dataSources().get(ANY_FILE_NAME));
     }
 
     @Test
     public void shouldGetAllDataSource() {
-        final DataSources dataSources = dataSources();
-
-        assertEquals("unknown", dataSources.get(0).getFileName());
-        assertEquals("pom.xml", dataSources.get(1).getFileName());
-        assertEquals("server.invalid?foo=bar", 
dataSources.get(2).getFileName());
-        assertEquals(3, dataSources.toList().size());
-        assertEquals(3, dataSources.toMap().size());
-        assertEquals(3, dataSources.size());
-        assertFalse(dataSources.isEmpty());
-    }
-
-    @Test
-    public void shouldGetParts() {
-        assertEquals(3, dataSources().getMetadata("name").size());
+        try (DataSources dataSources = dataSources()) {
+
+            assertEquals("unknown", dataSources.get(0).getName());
+            assertEquals("pom.xml", dataSources.get(1).getName());
+            assertEquals("server.invalid?foo=bar", 
dataSources.get(2).getName());
+            assertEquals(3, dataSources.toList().size());
+            assertEquals(3, dataSources.toMap().size());
+            assertEquals(3, dataSources.size());
+            assertFalse(dataSources.isEmpty());
+        }
     }
 
     @Test
-    public void shouldGetFileNamePart() {
-        assertEquals(asList("unknown", "pom.xml", "server.invalid?foo=bar"), 
dataSources().getMetadata("fileName"));
+    public void shouldGetMetadataParts() {
+        assertEquals(asList("", "pom.xml", ""), 
dataSources().getMetadata("filename"));
+        assertEquals(asList("", "xml", ""), 
dataSources().getMetadata("extension"));
+        assertEquals(asList("unknown", "pom.xml", "server.invalid?foo=bar"), 
dataSources().getMetadata("name"));
     }
 
     @Test
@@ -133,6 +133,14 @@ public class DataSourcesTest {
     }
 
     private static DataSource urlDataSource() {
-        return DataSourceFactory.create(ANY_URL);
+        return DataSourceFactory.fromUrl("server.invalid?foo=bar", "default", 
toUrl(ANY_URL), "plain/text", UTF_8);
+    }
+
+    private static URL toUrl(String value) {
+        try {
+            return new URL(value);
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("Failed to create URL:" + value, e);
+        }
     }
 }
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
index 06d5d17..6fbb177 100644
--- 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/file/RecursiveFileSupplierTest.java
@@ -35,7 +35,7 @@ public class RecursiveFileSupplierTest {
 
     @Test
     public void shouldResolveAllFilesOfDirectory() {
-        assertEquals(5, fileSupplier(ANY_DIRECTORY, null, null).get().size());
+        assertEquals(7, fileSupplier(ANY_DIRECTORY, null, null).get().size());
         assertTrue(fileSupplier(ANY_DIRECTORY, UNKNOWN_FILE_NAME, 
null).get().isEmpty());
     }
 
@@ -77,10 +77,12 @@ public class RecursiveFileSupplierTest {
     public void shouldExcludeFiles() {
         final List<File> files = fileSupplier(ANY_DIRECTORY, null, 
"*.csv").get();
 
-        assertEquals(3, files.size());
+        assertEquals(5, files.size());
         assertEquals("nginx.env", files.get(0).getName());
         assertEquals("test.properties", files.get(1).getName());
         assertEquals("file_01.txt", files.get(2).getName());
+        assertEquals("utf16.txt", files.get(3).getName());
+        assertEquals("utf8.txt", files.get(4).getName());
     }
 
     @Test
diff --git 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriStringParserTest.java
 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriStringParserTest.java
index 7954f50..ebcb84e 100644
--- 
a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriStringParserTest.java
+++ 
b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriStringParserTest.java
@@ -20,6 +20,7 @@ import org.apache.freemarker.generator.base.uri.NamedUri;
 import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
 import org.junit.Test;
 
+import static java.nio.charset.StandardCharsets.UTF_16;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -184,6 +185,7 @@ public class NamedUriStringParserTest {
         assertNull(namedURI.getGroup());
         assertEquals("file:///users.csv#charset=UTF-16&mimeType=text/csv", 
namedURI.getUri().toString());
         assertEquals(2, namedURI.getParameters().size());
+        assertEquals(UTF_16, namedURI.getCharset());
         assertEquals("UTF-16", namedURI.getParameters().get("charset"));
         assertEquals("text/csv", namedURI.getParameters().get("mimeType"));
     }
diff --git a/freemarker-generator-cli/CHANGELOG.md 
b/freemarker-generator-cli/CHANGELOG.md
index 6565367..c6a5cce 100644
--- a/freemarker-generator-cli/CHANGELOG.md
+++ b/freemarker-generator-cli/CHANGELOG.md
@@ -5,9 +5,9 @@ All notable changes to this project will be documented in this 
file. We try to a
 ## 0.1.0-SNAPSHOT
 
 ### Added
-* Parse list of `DataSources` for the various tools
+* Parse a list of `DataSources` for the various tools
+* [FREEMARKER-161] Allow multiple transformations on the CLI
 * [FREEMARKER-163] Integrate Java Faker library for test data generation
-* [FREEMARKER-148] Make usage of "DataSources" more "Freemarker" like
 * [FREEMARKER-149] Support multiple template transformations on the command 
line
 * [FREEMARKER-144] Proof Of Concept for providing DataFrames
 * [FREEMARKER-142] Support Transformation Of Directories
@@ -17,30 +17,32 @@ All notable changes to this project will be documented in 
this file. We try to a
 * [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
 * [FREEMARKER-129] Migrate `freemarker-cli` into `freemarker-generator` 
project (see 
[https://github.com/sgoeschl/freemarker-cli](https://github.com/sgoeschl/freemarker-cli))
+* [FREEMARKER-129] Provide a `toString()` method for all tools
 
 ### Changed
-* [FREEMARKER-161] Allow multiple transformations on the CLI
-* [FREEMARKER-155] Migrate the FTL code to terser dotter form 
+* [FREEMARKER-172] Use lower-case keys for DataSource metadata map
+* [FREEMARKER-148] Make usage of "DataSources" more "Freemarker" like
+* [FREEMARKER-155] Migrate the FTL code to terser dotter form
 * [FREEMARKER-153] Packaged templates are now prefixed with 
`freemarker-generator`, e.g. `freemarker-generator/info.ftl`
 * [FREEMARKER-153] Renamed `--basedir` command line option to `--template-dir`
 * [FREEMARKER-153] Renamed `freemarker-cli` to `freemarker-generator`
-* Removing `DataSources.first` and use `dataSources.get(0)` instead
 * [FREEMARKER-146] Cleanly separate example templates and data from 
user-supplied content
 * `DataSource` use `uri` instead of `location`
 * [FREEMARKER-138] freemarker-generator: Rename `Datasource` to `DataSource`
 * [FREEMARKER-136] Fix broken `site:stage` build
 * [FREEMARKER-134] Rename `Document` to `Datasource` which also changes 
`--document` to `--datasource`
 * [FREEMARKER-129] Use `freemarker.configuration.setting` in 
`freemarker-cli.properties` to configure FreeMarker
-* [FREEMARKER-129] Provide a `toString()` method for all tools
 * [FREEMARKER-129] Use version "0.X.Y" to cater for API changes according to 
[Semantic Versioning](https://semver.org)
 
 ### Fixed
+* [FREEMARKER-156] The Maven plugin unit tests failed randomly
 * [FREEMARKER-153] Configuration files are bootstrapped from "app.home" system 
property 
 * [FREEMARKER-151] Ensure that build and and examples are running on Windows
 * [FREEMARKER-147] Complete Maven site documentation
 * [FREEMARKER-127] Site build fails with missing 
"org/apache/maven/doxia/siterenderer/DocumentContent"
 
 ### Internal
+* [FREEMARKER-172] Refactor DataSourceFactory
 * [FREEMARKER-164] Use default unsafe FreeMarker configuration
 * [FREEMARKER-153] FreeMarker Generator release preparations
 * [FREEMARKER-168] Upgrade dependencies of freemarker-generator
@@ -62,7 +64,9 @@ All notable changes to this project will be documented in 
this file. We try to a
 [FREEMARKER-151]: https://issues.apache.org/jira/browse/FREEMARKER-151
 [FREEMARKER-153]: https://issues.apache.org/jira/browse/FREEMARKER-153
 [FREEMARKER-155]: https://issues.apache.org/jira/browse/FREEMARKER-155
+[FREEMARKER-156]: https://issues.apache.org/jira/browse/FREEMARKER-156
 [FREEMARKER-161]: https://issues.apache.org/jira/browse/FREEMARKER-161
 [FREEMARKER-163]: https://issues.apache.org/jira/browse/FREEMARKER-163
 [FREEMARKER-164]: https://issues.apache.org/jira/browse/FREEMARKER-164
-[FREEMARKER-168]: https://issues.apache.org/jira/browse/FREEMARKER-168
\ No newline at end of file
+[FREEMARKER-168]: https://issues.apache.org/jira/browse/FREEMARKER-168
+[FREEMARKER-172]: https://issues.apache.org/jira/browse/FREEMARKER-172
diff --git a/freemarker-generator-cli/src/app/examples/templates/demo.ftl 
b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
index 879fb3f..1154838 100644
--- a/freemarker-generator-cli/src/app/examples/templates/demo.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/demo.ftl
@@ -104,7 +104,7 @@ List all data sources having "json" extension
 - ${ds.name}
 </#list>
 List all data sources having "src/test/data/properties" in their file path
-<#list dataSources?values?filter(ds -> ds.match("filePath", 
"*/src/test/data/properties")) as ds>
+<#list dataSources?values?filter(ds -> ds.match("filepath", 
"*/src/test/data/properties")) as ds>
 - ${ds.name}
 </#list>
 
diff --git 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
index 60c7743..d936970 100644
--- 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
+++ 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
@@ -55,7 +55,7 @@ public class ConfigurationSupplier implements 
Supplier<Configuration> {
             // apply all "freemarker.configuration.setting" values
             configuration.setSettings(freeMarkerConfigurationSettings());
 
-            // provide custom models for "DataSources"
+            // TODO sgoeschl 2021-02-05 Probably not needed at all since we 
use the map of data source
             configuration.setObjectWrapper(new 
GeneratorObjectWrapper(FREEMARKER_VERSION));
 
             // override current configuration with caller-provided settings
diff --git 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
index 79d94b0..57591d6 100644
--- 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
+++ 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/DataModelSupplier.java
@@ -17,7 +17,8 @@
 package org.apache.freemarker.generator.cli.config;
 
 import org.apache.freemarker.generator.base.datasource.DataSource;
-import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
 import org.apache.freemarker.generator.base.uri.NamedUri;
 import org.apache.freemarker.generator.base.uri.NamedUriStringParser;
 import org.apache.freemarker.generator.base.util.PropertiesFactory;
@@ -48,6 +49,7 @@ import static 
org.apache.freemarker.generator.base.mime.Mimetypes.MIME_TEXT_YAML
  */
 public class DataModelSupplier implements Supplier<Map<String, Object>> {
 
+    private final DataSourceLoader dataSourceLoader;
     private final Collection<String> sources;
 
     /**
@@ -56,6 +58,7 @@ public class DataModelSupplier implements 
Supplier<Map<String, Object>> {
      * @param sources List of sources
      */
     public DataModelSupplier(Collection<String> sources) {
+        this.dataSourceLoader = DataSourceLoaderFactory.create();
         this.sources = new ArrayList<>(requireNonNull(sources));
     }
 
@@ -63,14 +66,14 @@ public class DataModelSupplier implements 
Supplier<Map<String, Object>> {
     public Map<String, Object> get() {
         return sources.stream()
                 .filter(StringUtils::isNotEmpty)
-                .map(DataModelSupplier::toDataModel)
+                .map(this::toDataModel)
                 .flatMap(map -> map.entrySet().stream())
                 .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
     }
 
-    private static Map<String, Object> toDataModel(String source) {
+    private Map<String, Object> toDataModel(String source) {
+        final DataSource dataSource = dataSourceLoader.load(source);
         final NamedUri namedUri = NamedUriStringParser.parse(source);
-        final DataSource dataSource = DataSourceFactory.fromNamedUri(namedUri);
         final boolean isExplodedDataModel = !namedUri.hasName();
         final String contentType = dataSource.getContentType();
 
diff --git 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
index 289611f..6ed0d39 100644
--- 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
+++ 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/GeneratorObjectWrapper.java
@@ -23,6 +23,11 @@ import freemarker.template.TemplateModelException;
 import freemarker.template.Version;
 import org.apache.freemarker.generator.base.datasource.DataSources;
 
+/**
+ * Custom FreeMarker object wrapper to expose <code>DataSources</code>
+ * as <code>Map</code> in the FreeMarker data model. Please note that
+ * this hides ALL operation exposed by "DataSources".
+ */
 public class GeneratorObjectWrapper extends DefaultObjectWrapper {
 
     public GeneratorObjectWrapper(Version incompatibleImprovements) {
diff --git 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
index 78352ec..a8fb389 100644
--- 
a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
+++ 
b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/picocli/OutputGeneratorDefinition.java
@@ -103,10 +103,8 @@ public class OutputGeneratorDefinition {
     private static boolean isFileSource(String source) {
         if (source.contains("file://")) {
             return true;
-        } else if (source.contains("://")) {
-            return false;
         } else {
-            return true;
+            return !source.contains("://");
         }
     }
 }
diff --git 
a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md 
b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
index e6e4b3e..b4205e3 100644
--- a/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
+++ b/freemarker-generator-cli/src/site/markdown/cli/concepts/data-sources.md
@@ -1,12 +1,12 @@
 ## DataSources
 
-A `DataSource` consists of lazy-loaded data available in Apache FreeMarker's 
model (context) - it provides
+A `DataSource` consists of lazy-loaded data available in Apache FreeMarker's 
model - 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
+* A `name` uniquely identifying a data source
+* An `uri` which as used to create the data source
+* A `content type` and `charset`
+* Access to textual content directly or using a line iterator
+* Access to the underlying data input stream
 
 ### Loading A DataSource
 
@@ -64,7 +64,7 @@ freemarker-generator -t freemarker-generator/info.ftl -s 
examples/data
 FreeMarker Generator DataSources
 ------------------------------------------------------------------------------
 [#1]: 
name=file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/accesslog/combined-access.log,
 group=default, fileName=combined-access.log mimeType=text/plain, 
charset=UTF-8, length=2,068 Bytes
-URI : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/accesslog/combined-access.log
    ...
+URI : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/accesslog/combined-access.log
 ...
 [#25]: 
name=file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/yaml/swagger-spec.yaml,
 group=default, fileName=swagger-spec.yaml mimeType=text/yaml, charset=UTF-8, 
length=17,555 Bytes
 URI : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/yaml/swagger-spec.yaml
@@ -83,7 +83,7 @@ URI : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-ge
 URI : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/target/appassembler/examples/data/json/swagger-spec.json```
 ```
 
-Access to `stdin` is implemented as `DataSource` - please not that `stdin` is 
read lazy to cater for arbitrary large input data
+Access to `stdin` is implemented as `DataSource` - please not that `stdin` is 
read lazily to cater for arbitrary large input data
 
 ```
 cat examples/data/csv/contract.csv | bin/freemarker-generator -t 
freemarker-generator/info.ftl --stdin
@@ -98,11 +98,39 @@ URI : system:///stdin
 
 After loading one or more `DataSource` they are accessible as `dataSource` map 
in the FreeMarker model
 
-* `dataSources?values[0]` selects the first data source
+* `dataSources?values[0]` or `dataSources?values?first` selects the first data 
source
 * `dataSources["user.csv"]` selects the data source with the name "user.csv"
 
+### Iterating Over DataSources
+
+The data sources are exposed as map within FreeMarker's data model 
+
+```
+<#-- Do something with the data sources -->
+<#if dataSources?has_content>
+Some data sources found
+<#else>
+No data sources found ...
+</#if>
+
+<#-- Get the number of data sources -->
+${dataSources?size}
+
+<#-- Iterate over a map of data sources -->
+<#list dataSources as name, dataSource>
+- ${name} => ${dataSource.length}
+</#list>
+
+<#-- Iterate over a list of data sources -->
+<#list dataSources?values as dataSource>
+- [#${dataSource?counter}]: name=${dataSource.name}
+</#list>
+```
+
+### Filtering of DataSources
+
 Combining FreeMarker's `filter` built-in  with the `DataSource#match` methods 
allows more advanced 
-selection of data sources (using Apache Commons IO wildcard matching)
+selection of data sources (using Apache Commons IO wild-card matching)
 
 ```
 <#-- List all data sources containing "test" in the name -->
@@ -119,4 +147,64 @@ selection of data sources (using Apache Commons IO 
wildcard matching)
 <#list dataSources?values?filter(ds -> ds.match("filePath", 
"*/src/test/data/properties")) as ds>
 - ${ds.name}
 </#list>
-```
\ No newline at end of file
+
+<#-- List all data sources of a group -->
+<#list dataSources?values?filter(ds -> ds.match("group", "default")) as ds>
+- ${ds.name}
+</#list>
+
+```
+
+### Using a DataSource
+
+In most cases the data source will passed to a tool but the are some useful 
operations available as shown below
+
+```text
+Invoke Arbitrary Methods On DataSource
+---------------------------------------------------------------------------
+<#if dataSources?has_content>
+<#assign dataSource=dataSources?values?first>
+Name            : ${dataSource.name}
+Nr of lines     : ${dataSource.lines?size}
+Content Type    : ${dataSource.contentType}
+Charset         : ${dataSource.charset}
+Extension       : ${dataSource.extension}
+Nr of chars     : ${dataSource.text?length}
+Nr of bytes     : ${dataSource.bytes?size}
+File name       : ${dataSource.metadata["filename"]}
+
+Iterating Over Metadata Of A Datasource
+---------------------------------------------------------------------------
+<#list dataSource.metadata as name, value>
+${name?right_pad(15)} : ${value}
+</#list>
+</#if>
+```
+
+will result in
+
+```text
+Invoke Arbitrary Methods On DataSource
+---------------------------------------------------------------------------
+Name            : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv/contract.csv
+Nr of lines     : 23
+Content Type    : text/csv
+Charset         : UTF-8
+Extension       : csv
+Nr of chars     : 6,328
+Nr of bytes     : 6,328
+File name       : contract.csv
+
+Iterating Over Metadata Of A Datasource
+---------------------------------------------------------------------------
+extension       : csv
+filename        : contract.csv
+basename        : contract
+filepath        : 
/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv
+name            : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv/contract.csv
+mimetype        : text/csv
+uri             : 
file:/Users/sgoeschl/work/github/apache/freemarker-generator/freemarker-generator-cli/src/app/examples/data/csv/contract.csv
+group           : default
+```
+
+
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 7d12754..54be597 100644
--- 
a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
+++ 
b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
@@ -24,7 +24,7 @@ import java.io.IOException;
 public class ManualTest extends AbstractMainTest {
 
     // private static final String CMD = "-V";
-    private static final String CMD = "-t src/app/examples/templates/demo.ftl 
src/test/data";
+    private static final String CMD = "-t src/test/templates/manual.ftl -s 
src/app/examples/data/csv";
 
     @Override
     public String execute(String commandLine) throws IOException {
diff --git a/freemarker-generator-cli/src/test/templates/manual.ftl 
b/freemarker-generator-cli/src/test/templates/manual.ftl
index 128274e..3ad42c6 100644
--- a/freemarker-generator-cli/src/test/templates/manual.ftl
+++ b/freemarker-generator-cli/src/test/templates/manual.ftl
@@ -22,44 +22,37 @@ Nr. of Documents: ${dataSources?size}
 
 Use FTL Array-style Access
 ---------------------------------------------------------------------------
-${dataSources[0].toString()}
-
-Use FTL Map-style access
----------------------------------------------------------------------------
-${DataSources["github-users.json"].toString()}
-${DataSources["github-users.json"].name}
+${dataSources?values[0].toString()}
+${dataSources?values?first.toString()}
 
 Get Document Names As Keys
 ---------------------------------------------------------------------------
-<#list DataSources?keys as name>
+<#list dataSources?keys as name>
     ${name}<#lt>
 </#list>
 
 Iterate Over Names & DataSources
 ---------------------------------------------------------------------------
-<#list DataSources as name, dataSource>
-    ${name} => ${dataSource}<#lt>
+<#list dataSources as name, dataSource>
+    ${name} => ${dataSource.uri}<#lt>
 </#list>
 
-Find DataSources By Group
+Invoke Arbitrary Methods On DataSource
 ---------------------------------------------------------------------------
-<#list dataSources.findByGroup("default") as dataSource>
-    ${dataSource}<#lt>
-</#list>
+<#if dataSources?has_content>
+<#assign dataSource=dataSources?values?first>
+Name            : ${dataSource.name}
+Nr of lines     : ${dataSource.lines?size}
+Content Type    : ${dataSource.contentType}
+Charset         : ${dataSource.charset}
+Extension       : ${dataSource.extension}
+Nr of chars     : ${dataSource.text?length}
+Nr of bytes     : ${dataSource.bytes?size}
+File name       : ${dataSource.metadata["filename"]}
 
-Find DataSources By Wildcard
+Iterating Over Metadata Of A Datasource
 ---------------------------------------------------------------------------
-<#list dataSources.find("*.csv") as dataSource>
-    ${dataSource}<#lt>
+<#list dataSource.metadata as name, value>
+${name?right_pad(15)} : ${value}
 </#list>
-
-Java Array-style access
----------------------------------------------------------------------------
-${dataSources?values[0].toString()}
-
-Invoke Arbitrary Methods On DataSources
----------------------------------------------------------------------------
-empty       : ${dataSources.empty?c}
-isEmpty()   : ${dataSources.isEmpty()?c}
-size()      : ${dataSources.size()}
-close()     : ${dataSources.close()}worx
+</#if>
diff --git 
a/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OutputGeneratorTest.java
 
b/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OutputGeneratorTest.java
index d577a83..0e27501 100644
--- 
a/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OutputGeneratorTest.java
+++ 
b/freemarker-generator-maven-plugin/src/test/java/org/apache/freemarker/generator/maven/OutputGeneratorTest.java
@@ -21,6 +21,7 @@ package org.apache.freemarker.generator.maven;
 import freemarker.template.Configuration;
 import org.assertj.core.api.*;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.io.File;
@@ -204,6 +205,7 @@ public class OutputGeneratorTest {
     }
 
     @Test
+    @Ignore("Only pollutes the build output")
     public void generate_missingVarTest() {
         final OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
         builder.addPomLastModifiedTimestamp(0);
diff --git 
a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
 
b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
index 0e1d395..e71e25c 100644
--- 
a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
+++ 
b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/dataframe/DataFrameToolTest.java
@@ -19,7 +19,8 @@ package org.apache.freemarker.generator.tools.dataframe;
 import de.unknownreality.dataframe.DataFrame;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVParser;
-import org.apache.freemarker.generator.base.datasource.DataSourceFactory;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoader;
+import org.apache.freemarker.generator.base.datasource.DataSourceLoaderFactory;
 import org.apache.freemarker.generator.tools.commonscsv.CommonsCSVTool;
 import org.apache.freemarker.generator.tools.excel.ExcelTool;
 import org.apache.freemarker.generator.tools.gson.GsonTool;
@@ -113,7 +114,7 @@ public class DataFrameToolTest {
     @Test
     public void shouldParseExcelSheet() {
         final ExcelTool excelTool = excelTool();
-        final Workbook workbook = 
excelTool.parse(DataSourceFactory.create("./src/test/data/excel/test.xls"));
+        final Workbook workbook = 
excelTool.parse(dataSourceLoader().load("./src/test/data/excel/test.xls"));
         final List<List<Object>> sheet = 
excelTool.toTable(workbook.getSheetAt(0));
 
         final DataFrame dataFrame = dataFrameTool().fromRows(sheet, true);
@@ -150,4 +151,8 @@ public class DataFrameToolTest {
     private CSVParser csvParser(String csv, CSVFormat csvFormat) {
         return commonsCSVTool().parse(csv, csvFormat);
     }
+
+    private static DataSourceLoader dataSourceLoader() {
+        return DataSourceLoaderFactory.create();
+    }
 }

Reply via email to