You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by sg...@apache.org on 2020/02/29 15:28:58 UTC

[freemarker-generator] branch FREEMARKER-135 created (now 272faac)

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

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


      at 272faac  FREEMARKER-135 Support user-supplied names for datasources

This branch includes the following new commits:

     new 272faac  FREEMARKER-135 Support user-supplied names for datasources

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[freemarker-generator] 01/01: FREEMARKER-135 Support user-supplied names for datasources

Posted by sg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 272faacc739b2bdf7dbffad83adcb915f031bd65
Author: Siegfried Goeschl <si...@gmail.com>
AuthorDate: Sat Feb 29 16:28:41 2020 +0100

    FREEMARKER-135 Support user-supplied names for datasources
---
 CHANGELOG.md                                       |   4 +-
 .../generator/base/FreeMarkerConstants.java        |   8 +
 .../activation/MimetypesFileTypeMapFactory.java    |  42 +++++
 .../generator/base/datasource/Datasource.java      |  16 +-
 .../base/datasource/DatasourceFactory.java         |  39 +++--
 .../generator/base/datasource/Datasources.java     |  36 +++-
 .../base/datasource/DatasourcesSupplier.java       |  56 ++++--
 .../freemarker/generator/base/uri/NamedUri.java    |  93 ++++++++++
 .../NamedUriFragmentParser.java}                   |  28 ++-
 .../generator/base/uri/NamedUriParser.java         |  74 ++++++++
 .../generator/base/util/CachingSupplier.java       |  16 ++
 .../generator/base/util/LocaleUtils.java           |   1 +
 .../generator/base/util/StringUtils.java           |  13 ++
 .../freemarker/generator/base/util/Validate.java   | 150 ++++++++++++++++
 .../datasource/DatasourceFactoryTest.java          |   8 +-
 .../generator/datasource/DatasourceTest.java       |  14 +-
 .../generator/datasource/DatasourcesTest.java      |  27 ++-
 .../generator/uri/NamedUriParserTest.java          | 192 +++++++++++++++++++++
 .../org/apache/freemarker/generator/cli/Main.java  |  75 +++++---
 .../freemarker/generator/cli/config/Settings.java  |  26 +--
 .../freemarker/generator/cli/config/Suppliers.java |   2 +-
 .../generator/cli/task/FreeMarkerTask.java         |   3 +-
 .../freemarker/generator/cli/ExamplesTest.java     |   4 +-
 .../freemarker/generator/cli/ManualTest.java       |   3 +-
 .../freemarker/generator/cli/PicocliTest.java      |  74 ++++++++
 .../generator/cli/config/SettingsTest.java         |   4 +-
 freemarker-generator-cli/templates/info.ftl        |  19 +-
 .../generator/maven/JsonPropertiesProvider.java    |   1 +
 .../tools/properties/PropertiesToolTest.java       |   3 +-
 .../tools/snakeyaml/SnakeYamlToolTest.java         |   4 +-
 .../generator/tools/xml/XmlToolTest.java           |   4 +-
 31 files changed, 937 insertions(+), 102 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9a227d..4a91545 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. We try to a
 * [FREEMARKER-129] Migrate `freemarker-cli` into `freemarker-generator` project (see [https://github.com/sgoeschl/freemarker-cli](https://github.com/sgoeschl/freemarker-cli))
 
 ### Changed
+* [FREEMARKER-134] Rename `Document` to `Datasource
 * [FREEMARKER-129] Use `freemarker.configuration.setting` in `freemarker-cli.properties` to configure FreeMarker
 * [FREEMARKER-129] Provide a `toString()` metheod for all tools
 * [FREEMARKER-129] Use version "0.X.Y" to cater for API changes according to [Semantic Versioning](https://semver.org)
@@ -24,4 +25,5 @@ All notable changes to this project will be documented in this file. We try to a
 
 [FREEMARKER-127]: https://issues.apache.org/jira/browse/FREEMARKER-127
 [FREEMARKER-128]: https://issues.apache.org/jira/browse/FREEMARKER-128
-[FREEMARKER-129]: https://issues.apache.org/jira/browse/FREEMARKER-129
\ No newline at end of file
+[FREEMARKER-129]: https://issues.apache.org/jira/browse/FREEMARKER-129
+[FREEMARKER-`134`]: https://issues.apache.org/jira/browse/FREEMARKER-134
\ No newline at end of file
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
index 94508d1..bc6f47f 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/FreeMarkerConstants.java
@@ -26,6 +26,14 @@ public class FreeMarkerConstants {
     /* Default encoding for textual content */
     public static final Charset DEFAULT_CHARSET = UTF_8;
 
+    /* Default group name for datasources */
+    public static final String DEFAULT_GROUP = "default";
+
+    public enum GeneratorMode {
+        DATASOURCE,
+        TEMPLATE
+    }
+
     public static class Configuration {
 
         private Configuration() {
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/MimetypesFileTypeMapFactory.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/MimetypesFileTypeMapFactory.java
new file mode 100644
index 0000000..b75a422
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/activation/MimetypesFileTypeMapFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.activation;
+
+import javax.activation.MimetypesFileTypeMap;
+
+public class MimetypesFileTypeMapFactory {
+
+    private static MimetypesFileTypeMap mimeTypes;
+
+    public static synchronized MimetypesFileTypeMap create() {
+        if (mimeTypes == null) {
+            mimeTypes = new MimetypesFileTypeMap();
+            mimeTypes.addMimeTypes("application/json json JSON");
+            mimeTypes.addMimeTypes("application/octet-stream bin");
+            mimeTypes.addMimeTypes("application/vnd.ms-excel xls XLS");
+            mimeTypes.addMimeTypes("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx XLSX");
+            mimeTypes.addMimeTypes("application/xml xml XML");
+            mimeTypes.addMimeTypes("text/csv csv CSV");
+            mimeTypes.addMimeTypes("text/plain txt TXT log LOG ini INI properties md MD");
+            mimeTypes.addMimeTypes("text/yaml yml YML yaml YAML");
+            mimeTypes.addMimeTypes("text/tab-separated-values tsv TSV");
+        }
+
+        return mimeTypes;
+    }
+}
\ No newline at end of file
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 5de9cae..ed038b0 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
@@ -36,6 +36,7 @@ import static java.nio.charset.Charset.forName;
 import static java.util.Objects.requireNonNull;
 import static org.apache.commons.io.IOUtils.lineIterator;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.DATASOURCE_UNKNOWN_LENGTH;
+import static org.apache.freemarker.generator.base.util.StringUtils.emptyToNull;
 
 /**
  * Datasource which encapsulates data to be used for rendering
@@ -47,6 +48,9 @@ public class Datasource implements Closeable {
     /** Human-readable name of the datasource */
     private final String name;
 
+    /** Optional group of datasource */
+    private final String group;
+
     /** Charset for directly accessing text-based content */
     private final Charset charset;
 
@@ -59,8 +63,9 @@ public class Datasource implements Closeable {
     /** Collect all closables handed out to the caller to be closed when the datasource is closed itself */
     private final CloseableReaper closables;
 
-    public Datasource(String name, DataSource dataSource, String location, Charset charset) {
+    public Datasource(String name, String group, DataSource dataSource, String location, Charset charset) {
         this.name = requireNonNull(name);
+        this.group = emptyToNull(group);
         this.dataSource = requireNonNull(dataSource);
         this.location = requireNonNull(location);
         this.charset = requireNonNull(charset);
@@ -71,6 +76,10 @@ public class Datasource implements Closeable {
         return name;
     }
 
+    public String getGroup() {
+        return group;
+    }
+
     public String getBaseName() {
         return FilenameUtils.getBaseName(name);
     }
@@ -83,6 +92,10 @@ public class Datasource implements Closeable {
         return charset;
     }
 
+    public String getContentType() {
+        return dataSource.getContentType();
+    }
+
     public String getLocation() {
         return location;
     }
@@ -213,6 +226,7 @@ public class Datasource implements Closeable {
     public String toString() {
         return "Datasource{" +
                 "name='" + name + '\'' +
+                "group='" + group + '\'' +
                 ", location=" + location +
                 ", charset='" + charset + '\'' +
                 '}';
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 c54aa4e..8cf721e 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
@@ -19,6 +19,7 @@ package org.apache.freemarker.generator.base.datasource;
 import org.apache.freemarker.generator.base.FreeMarkerConstants.Location;
 import org.apache.freemarker.generator.base.activation.ByteArrayDataSource;
 import org.apache.freemarker.generator.base.activation.InputStreamDataSource;
+import org.apache.freemarker.generator.base.activation.MimetypesFileTypeMapFactory;
 import org.apache.freemarker.generator.base.activation.StringDataSource;
 
 import javax.activation.DataSource;
@@ -30,6 +31,7 @@ import java.net.URL;
 import java.nio.charset.Charset;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
 
 /**
  * Creates a Datasource from various sources.
@@ -40,37 +42,48 @@ public class DatasourceFactory {
     }
 
     public static Datasource create(URL url) {
-        final String location = url.getProtocol() + "://" + url.getHost();
+        final String location = url.toString();
         final URLDataSource dataSource = new URLDataSource(url);
-        return create(url.getHost(), dataSource, location, UTF_8);
+        return create(url.getHost(), DEFAULT_GROUP, dataSource, location, UTF_8);
     }
 
-    public static Datasource create(String name, String content) {
+    public static Datasource create(String name, String group, URL url, Charset charset) {
+        final String location = url.toString();
+        final URLDataSource dataSource = new URLDataSource(url);
+        return create(name, group, dataSource, location, charset);
+    }
+
+    public static Datasource create(String name, String group, String content) {
         final StringDataSource dataSource = new StringDataSource(name, content, UTF_8);
-        return create(name, dataSource, Location.STRING, UTF_8);
+        return create(name, group, dataSource, Location.STRING, UTF_8);
     }
 
     public static Datasource create(File file, Charset charset) {
+        return create(file.getName(), DEFAULT_GROUP, file, charset);
+    }
+
+    public static Datasource create(String name, String group, File file, Charset charset) {
         final FileDataSource dataSource = new FileDataSource(file);
-        return create(file.getName(), dataSource, file.getAbsolutePath(), charset);
+        dataSource.setFileTypeMap(MimetypesFileTypeMapFactory.create());
+        return create(name, group, dataSource, file.getAbsolutePath(), charset);
     }
 
-    public static Datasource create(String name, byte[] content) {
+    public static Datasource create(String name, String group, byte[] content) {
         final ByteArrayDataSource dataSource = new ByteArrayDataSource(name, content);
-        return create(name, dataSource, Location.BYTES, UTF_8);
+        return create(name, group, dataSource, Location.BYTES, UTF_8);
     }
 
-    public static Datasource create(String name, InputStream is, Charset charset) {
+    public static Datasource create(String name, String group, InputStream is, Charset charset) {
         final InputStreamDataSource dataSource = new InputStreamDataSource(name, is);
-        return create(name, dataSource, Location.INPUTSTREAM, charset);
+        return create(name, group, dataSource, Location.INPUTSTREAM, charset);
     }
 
-    public static Datasource create(String name, InputStream is, String location, Charset charset) {
+    public static Datasource create(String name, String group, InputStream is, String location, Charset charset) {
         final InputStreamDataSource dataSource = new InputStreamDataSource(name, is);
-        return create(name, dataSource, location, charset);
+        return create(name, group, dataSource, location, charset);
     }
 
-    public static Datasource create(String name, DataSource dataSource, String location, Charset charset) {
-        return new Datasource(name, dataSource, location, charset);
+    public static Datasource create(String name, String group, DataSource dataSource, String location, Charset charset) {
+        return new Datasource(name, group, dataSource, location, charset);
     }
 }
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 51d2804..5c55c6b 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
@@ -17,6 +17,7 @@
 package org.apache.freemarker.generator.base.datasource;
 
 import org.apache.freemarker.generator.base.util.ClosableUtils;
+import org.apache.freemarker.generator.base.util.StringUtils;
 
 import java.io.Closeable;
 import java.util.ArrayList;
@@ -46,6 +47,21 @@ public class Datasources implements Closeable {
     public List<String> getNames() {
         return datasources.stream()
                 .map(Datasource::getName)
+                .filter(StringUtils::isNotEmpty)
+                .collect(toList());
+    }
+
+    /**
+     * Get the groups of all datasources.
+     *
+     * @return datasource names
+     */
+    public List<String> getGroups() {
+        return datasources.stream()
+                .map(Datasource::getGroup)
+                .filter(StringUtils::isNotEmpty)
+                .sorted()
+                .distinct()
                 .collect(toList());
     }
 
@@ -69,14 +85,6 @@ public class Datasources implements Closeable {
         return datasources.get(index);
     }
 
-    public boolean add(Datasource datasource) {
-        return datasources.add(datasource);
-    }
-
-    public Datasource remove(int index) {
-        return datasources.remove(index);
-    }
-
     /**
      * Get exactly one datasource. If not exactly one datasource
      * is found an exception is thrown.
@@ -110,6 +118,18 @@ public class Datasources implements Closeable {
                 .collect(toList());
     }
 
+    /**
+     * Find datasources based on their group and and globbing pattern.
+     *
+     * @param wildcard globbing pattern
+     * @return list of mathching datasources
+     */
+    public List<Datasource> findByGroup(String wildcard) {
+        return datasources.stream()
+                .filter(d -> wildcardMatch(d.getGroup(), wildcard))
+                .collect(toList());
+    }
+
     @Override
     public void close() {
         datasources.forEach(ClosableUtils::closeQuietly);
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 3e5a686..8309939 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DatasourcesSupplier.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/datasource/DatasourcesSupplier.java
@@ -17,8 +17,11 @@
 package org.apache.freemarker.generator.base.datasource;
 
 import org.apache.freemarker.generator.base.file.RecursiveFileSupplier;
+import org.apache.freemarker.generator.base.uri.NamedUri;
+import org.apache.freemarker.generator.base.uri.NamedUriParser;
 
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -26,9 +29,11 @@ import java.util.Collection;
 import java.util.List;
 import java.util.function.Supplier;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
 
 /**
  * Create a list of <code>Datasource</code> based on a list of sources consisting of
@@ -71,20 +76,34 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
     }
 
     private List<Datasource> get(String source) {
-        if (isHttpUrl(source)) {
-            return singletonList(resolveHttpUrl(source));
-        } else {
-            return resolveFile(source, include, exclude, charset);
+        try {
+            if (isHttpUrl(source)) {
+                return singletonList(resolveHttpUrl(source));
+            } else {
+                return resolveFile(source, include, exclude, charset);
+            }
+        } catch (RuntimeException e) {
+            throw new RuntimeException("Unable to create the datasource: " + source, e);
         }
     }
 
-    private static Datasource resolveHttpUrl(String url) {
-        return DatasourceFactory.create(toUrl(url));
+    private static Datasource resolveHttpUrl(String source) {
+        final NamedUri namedUri = NamedUriParser.parse(source);
+        final URI uri = namedUri.getUri();
+        final String name = getNameOrElse(namedUri, uri.toString());
+        final String group = getGroupOrElse(namedUri, DEFAULT_GROUP);
+        final Charset currCharset = getCharsetOrElse(namedUri, UTF_8);
+        return DatasourceFactory.create(name, group, toUrl(uri), currCharset);
     }
 
     private static List<Datasource> resolveFile(String source, String include, String exclude, Charset charset) {
-        return fileResolver(source, include, exclude).get().stream()
-                .map(file -> DatasourceFactory.create(file, charset))
+        final NamedUri namedUri = NamedUriParser.parse(source);
+        final String path = namedUri.getUri().getPath();
+        final String name = getNameOrElse(namedUri, path);
+        final String group = getGroupOrElse(namedUri, DEFAULT_GROUP);
+        final Charset currCharset = getCharsetOrElse(namedUri, charset);
+        return fileResolver(path, include, exclude).get().stream()
+                .map(file -> DatasourceFactory.create(name, group, file, currCharset))
                 .collect(toList());
     }
 
@@ -93,14 +112,27 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
     }
 
     private static boolean isHttpUrl(String value) {
-        return value.startsWith("http://") || value.startsWith("https://");
+        return value.contains("http://") || value.contains("https://");
     }
 
-    private static URL toUrl(String value) {
+    private static URL toUrl(URI uri) {
         try {
-            return new URL(value);
+            return uri.toURL();
         } catch (MalformedURLException e) {
-            throw new IllegalArgumentException(value, e);
+            throw new IllegalArgumentException(uri.toString(), e);
         }
     }
+
+    private static Charset getCharsetOrElse(NamedUri namedUri, Charset def) {
+        return Charset.forName(namedUri.getParameters().getOrDefault("charset", def.name()));
+    }
+
+    private static String getNameOrElse(NamedUri namedUri, String def) {
+        return namedUri.hasName() ? namedUri.getName() : def;
+    }
+
+    private static String getGroupOrElse(NamedUri namedUri, String def) {
+        return namedUri.hasGroup() ? namedUri.getGroup() : def;
+    }
+
 }
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
new file mode 100644
index 0000000..0b1211d
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUri.java
@@ -0,0 +1,93 @@
+/*
+ * 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.uri;
+
+import java.net.URI;
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
+
+/**
+ * Caputeres the information of a user-supplied "named URI".
+ */
+public class NamedUri {
+
+    /** User-supplied name */
+    private final String name;
+
+    /** User-supplied group */
+    private final String group;
+
+    /** The URI */
+    private final URI uri;
+
+    /** Name/value pairs parsed from URI fragment */
+    private final Map<String, String> parameters;
+
+    public NamedUri(URI uri, Map<String, String> parameters) {
+        this.name = null;
+        this.group = null;
+        this.uri = requireNonNull(uri);
+        this.parameters = requireNonNull(parameters);
+    }
+
+    public NamedUri(String name, String group, URI uri, Map<String, String> parameters) {
+        this.name = emptyToNull(name);
+        this.group = emptyToNull(group);
+        this.uri = requireNonNull(uri);
+        this.parameters = requireNonNull(parameters);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getGroup() {
+        return group;
+    }
+
+    public URI getUri() {
+        return uri;
+    }
+
+    public Map<String, String> getParameters() {
+        return parameters;
+    }
+
+    public boolean hasName() {
+        return !isEmpty(this.name);
+    }
+
+    public boolean hasGroup() {
+        return !isEmpty(this.group);
+    }
+
+    @Override
+    public String toString() {
+        return "NamedUri{" +
+                "name='" + name + '\'' +
+                ", group='" + group + '\'' +
+                ", uri=" + uri +
+                ", parameters=" + parameters +
+                '}';
+    }
+
+    private static String emptyToNull(String value) {
+        return value != null && value.trim().isEmpty() ? null : value;
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriFragmentParser.java
similarity index 52%
copy from freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java
copy to freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriFragmentParser.java
index 5edd405..304a233 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriFragmentParser.java
@@ -14,20 +14,32 @@
  * 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.uri;
 
-import java.util.Locale;
+import java.util.Arrays;
+import java.util.Map;
 
+import static java.util.Collections.emptyMap;
+import static java.util.stream.Collectors.toMap;
 import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 
-public class LocaleUtils {
+/**
+ * Parses the URI fragment as list of name/value pairs seperated by an ampersand.
+ */
+public class NamedUriFragmentParser {
 
-    public static Locale parseLocale(String value) {
-        if (isEmpty(value) || value.equalsIgnoreCase("JVM default") || value.equalsIgnoreCase("default")) {
-            return Locale.getDefault();
+    public static Map<String, String> parse(String fragment) {
+        if (isEmpty(fragment)) {
+            return emptyMap();
         }
 
-        final String[] parts = value.split("_");
-        return parts.length == 1 ? new Locale(parts[0]) : new Locale(parts[0], parts[1]);
+        try {
+            final String[] nameValuePairs = fragment.split("&");
+            return Arrays.stream(nameValuePairs)
+                    .map(s -> s.split("="))
+                    .collect(toMap(parts -> parts[0], parts -> parts[1]));
+        } catch (RuntimeException e) {
+            throw new RuntimeException("Unable to parse URI fragment: " + fragment, e);
+        }
     }
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriParser.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriParser.java
new file mode 100644
index 0000000..a5ab42b
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriParser.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.base.uri;
+
+import org.apache.freemarker.generator.base.util.Validate;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.regex.Pattern.compile;
+
+/**
+ * Parses a named URI provided by the caller.
+ * <ul>
+ *     <li>users.csv</li>
+ *     <li>file:///users.csv</li>
+ *     <li>users=file:///users.csv</li>
+ *     <li>users:admin=file:///users.csv</li>
+ *     <li>users=file:///users.csv#charset=UTF-16&mimetype=text/csv</li>
+ * </ul>
+ */
+public class NamedUriParser {
+
+    private static final String NAME = "name";
+    private static final String GROUP = "group";
+    private static final String URI = "uri";
+
+    private static final Pattern NAMED_URI_REGEXP = compile("^(?<name>[a-zA-Z0-9-_]*):?(?<group>[a-zA-Z0-9-_]*)=(?<uri>.*)");
+
+    public static NamedUri parse(String value) {
+        Validate.notEmpty(value, "Named URI is empty");
+
+        final Matcher matcher = NAMED_URI_REGEXP.matcher(value);
+
+        if (matcher.matches()) {
+            final String name = matcher.group(NAME);
+            final String group = matcher.group(GROUP);
+            final URI uri = uri(matcher.group(URI));
+            return new NamedUri(name, group, uri, parameters(uri));
+        } else {
+            final URI uri = uri(value);
+            return new NamedUri(uri, parameters(uri));
+        }
+    }
+
+    private static URI uri(String value) {
+        try {
+            return new URI(value);
+        } catch (URISyntaxException e) {
+            throw new RuntimeException("Failed to parse URI: " + value, e);
+        }
+    }
+
+    private static Map<String, String> parameters(URI uri) {
+        return NamedUriFragmentParser.parse(uri.getFragment());
+    }
+}
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/CachingSupplier.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/CachingSupplier.java
index fbada67..dab4334 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/CachingSupplier.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/CachingSupplier.java
@@ -1,3 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package org.apache.freemarker.generator.base.util;
 
 import java.util.function.Supplier;
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java
index 5edd405..95f6a7f 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/LocaleUtils.java
@@ -23,6 +23,7 @@ import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 public class LocaleUtils {
 
     public static Locale parseLocale(String value) {
+        // "JVM default" is a special value defined by FreeMarker
         if (isEmpty(value) || value.equalsIgnoreCase("JVM default") || value.equalsIgnoreCase("default")) {
             return Locale.getDefault();
         }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
index 769fd06..4dd8184 100644
--- a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/StringUtils.java
@@ -21,4 +21,17 @@ public class StringUtils {
     public static boolean isEmpty(String value) {
         return value == null || value.trim().isEmpty();
     }
+
+    public static boolean isNotEmpty(String value) {
+        return !isEmpty(value);
+    }
+
+    public static String emptyToNull(String value) {
+        return value != null && value.trim().isEmpty() ? null : value;
+    }
+
+    public static String nullToEmpty(String value) {
+        return value == null ? "" : value;
+    }
+
 }
diff --git a/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java
new file mode 100644
index 0000000..71bd28a
--- /dev/null
+++ b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/util/Validate.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.base.util;
+
+/**
+ * Simple validation methods designed for interal use.
+ */
+public final class Validate {
+
+    private Validate() {
+    }
+
+    /**
+     * Validates that the object is not null
+     *
+     * @param obj object to test
+     */
+    public static void notNull(Object obj) {
+        if (obj == null) {
+            throw new IllegalArgumentException("Object must not be null");
+        }
+    }
+
+    /**
+     * Validates that the object is not null
+     *
+     * @param obj object to test
+     * @param msg message to output if validation fails
+     */
+    public static void notNull(Object obj, String msg) {
+        if (obj == null) {
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Validates that the value is true
+     *
+     * @param val object to test
+     */
+    public static void isTrue(boolean val) {
+        if (!val) {
+            throw new IllegalArgumentException("Must be true");
+        }
+    }
+
+    /**
+     * Validates that the value is true
+     *
+     * @param val object to test
+     * @param msg message to output if validation fails
+     */
+    public static void isTrue(boolean val, String msg) {
+        if (!val) {
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Validates that the value is false
+     *
+     * @param val object to test
+     */
+    public static void isFalse(boolean val) {
+        if (val) {
+            throw new IllegalArgumentException("Must be false");
+        }
+    }
+
+    /**
+     * Validates that the value is false
+     *
+     * @param val object to test
+     * @param msg message to output if validation fails
+     */
+    public static void isFalse(boolean val, String msg) {
+        if (val) {
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Validates that the array contains no null elements
+     *
+     * @param objects the array to test
+     */
+    public static void noNullElements(Object[] objects) {
+        noNullElements(objects, "Array must not contain any null objects");
+    }
+
+    /**
+     * Validates that the array contains no null elements
+     *
+     * @param objects the array to test
+     * @param msg     message to output if validation fails
+     */
+    public static void noNullElements(Object[] objects, String msg) {
+        for (Object obj : objects) {
+            if (obj == null) {
+                throw new IllegalArgumentException(msg);
+            }
+        }
+    }
+
+    /**
+     * Validates that the string is not empty
+     *
+     * @param string the string to test
+     */
+    public static void notEmpty(String string) {
+        if (string == null || string.trim().isEmpty()) {
+            throw new IllegalArgumentException("String must not be empty");
+        }
+    }
+
+    /**
+     * Validates that the string is not empty
+     *
+     * @param string the string to test
+     * @param msg    message to output if validation fails
+     */
+    public static void notEmpty(String string, String msg) {
+        if (string == null || string.trim().isEmpty()) {
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+    /**
+     * Cause a failure.
+     *
+     * @param msg message to output.
+     */
+    public static void fail(String msg) {
+        throw new IllegalArgumentException(msg);
+    }
+}
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 677f991..c3bb493 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
@@ -49,9 +49,10 @@ public class DatasourceFactoryTest {
 
     @Test
     public void shouldCreateStringBasedDatasource() throws IOException {
-        final Datasource datasource = DatasourceFactory.create("test.txt", ANY_TEXT);
+        final Datasource datasource = DatasourceFactory.create("test.txt", "default", ANY_TEXT);
 
         assertEquals("test.txt", datasource.getName());
+        assertEquals("default", datasource.getGroup());
         assertEquals(UTF_8, datasource.getCharset());
         assertEquals("string", datasource.getLocation());
         assertEquals(ANY_TEXT, datasource.getText());
@@ -60,9 +61,10 @@ public class DatasourceFactoryTest {
 
     @Test
     public void shouldCreateByteArrayBasedDatasource() throws IOException {
-        final Datasource datasource = DatasourceFactory.create("test.txt", ANY_TEXT.getBytes(UTF_8));
+        final Datasource datasource = DatasourceFactory.create("test.txt", "default", ANY_TEXT.getBytes(UTF_8));
 
         assertEquals("test.txt", datasource.getName());
+        assertEquals("default", datasource.getGroup());
         assertEquals(UTF_8, datasource.getCharset());
         assertEquals("bytes", datasource.getLocation());
         assertEquals(ANY_TEXT, datasource.getText());
@@ -72,7 +74,7 @@ public class DatasourceFactoryTest {
     @Test
     public void shouldCreateInputStreamBasedDatasource() throws IOException {
         final InputStream is = new ByteArrayInputStream(ANY_TEXT.getBytes(UTF_8));
-        final Datasource datasource = DatasourceFactory.create("test.txt", is, UTF_8);
+        final Datasource datasource = DatasourceFactory.create("test.txt", "default", is, UTF_8);
 
         assertEquals("test.txt", datasource.getName());
         assertEquals(UTF_8, datasource.getCharset());
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 878dd2c..a1b73cd 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
@@ -30,12 +30,14 @@ import java.nio.charset.Charset;
 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.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class DatasourceTest {
 
+    private static final String ANY_GROUP = "group";
     private static final String ANY_TEXT = "Hello World";
     private static final String ANY_FILE_NAME = "pom.xml";
     private static final Charset ANY_CHAR_SET = UTF_8;
@@ -43,12 +45,14 @@ public class DatasourceTest {
 
     @Test
     public void shouldSupportTextDatasource() throws IOException {
-        try (Datasource datasource = DatasourceFactory.create("stdin", ANY_TEXT)) {
+        try (Datasource datasource = DatasourceFactory.create("stdin", ANY_GROUP, ANY_TEXT)) {
             assertEquals("stdin", datasource.getName());
+            assertEquals(ANY_GROUP, datasource.getGroup());
             assertEquals("stdin", datasource.getBaseName());
             assertEquals("", datasource.getExtension());
             assertEquals("string", datasource.getLocation());
             assertEquals(UTF_8, datasource.getCharset());
+            assertEquals("plain/text", datasource.getContentType());
             assertTrue(datasource.getLength() > 0);
             assertEquals(ANY_TEXT, datasource.getText());
         }
@@ -58,23 +62,27 @@ public class DatasourceTest {
     public void shouldSupportFileDatasource() throws IOException {
         try (Datasource datasource = DatasourceFactory.create(ANY_FILE, ANY_CHAR_SET)) {
             assertEquals(ANY_FILE_NAME, datasource.getName());
+            assertEquals(DEFAULT_GROUP, datasource.getGroup());
             assertEquals("pom", datasource.getBaseName());
             assertEquals("xml", datasource.getExtension());
             assertEquals(ANY_FILE.getAbsolutePath(), datasource.getLocation());
             assertEquals(Charset.defaultCharset(), datasource.getCharset());
+            assertEquals("application/xml", datasource.getContentType());
             assertTrue(datasource.getLength() > 0);
             assertFalse(datasource.getText().isEmpty());
         }
     }
 
-    @Ignore
+    @Ignore("Requires internet conenection")
     @Test
     public void shouldSupportUrlDatasource() throws IOException {
         try (Datasource datasource = DatasourceFactory.create(new URL("https://google.com?foo=bar"))) {
             assertEquals("google.com", datasource.getName());
+            assertEquals(DEFAULT_GROUP, datasource.getGroup());
             assertEquals("google", datasource.getBaseName());
             assertEquals("com", datasource.getExtension());
             assertEquals("https://google.com", datasource.getLocation());
+            assertEquals("text/html; charset=ISO-8859-1", datasource.getContentType());
             assertEquals(UTF_8, datasource.getCharset());
             assertEquals(-1, datasource.getLength());
             assertFalse(datasource.getText().isEmpty());
@@ -127,7 +135,7 @@ public class DatasourceTest {
     }
 
     private static Datasource textDatasource() {
-        return DatasourceFactory.create("stdin", ANY_TEXT);
+        return DatasourceFactory.create("stdin", "default", ANY_TEXT);
     }
 
     private static final class TestClosable implements Closeable {
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 b580191..de78af5 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
@@ -27,6 +27,8 @@ import java.net.URL;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -41,7 +43,7 @@ public class DatasourcesTest {
     private static final String ANY_URL = "https://server.invalid?foo=bar";
 
     @Test
-    public void shouldFindByWildcard() {
+    public void shouldFindByName() {
         final Datasources datasources = datasources();
 
         assertEquals(0, datasources.find(null).size());
@@ -63,6 +65,22 @@ public class DatasourcesTest {
     }
 
     @Test
+    public void shouldFindByGroup() {
+        final Datasources datasources = datasources();
+
+        assertEquals(0, datasources.findByGroup(null).size());
+        assertEquals(0, datasources.findByGroup("").size());
+
+        assertEquals(0, datasources.findByGroup("unknown").size());
+
+        assertEquals(3, datasources.findByGroup("*").size());
+        assertEquals(3, datasources.findByGroup("default").size());
+        assertEquals(3, datasources.findByGroup("d*").size());
+        assertEquals(3, datasources.findByGroup("d??????").size());
+
+    }
+
+    @Test
     public void shouldGetDatasource() {
         assertNotNull(datasources().get(ANY_FILE_NAME));
     }
@@ -85,6 +103,11 @@ public class DatasourcesTest {
         assertEquals(asList("unknown", "pom.xml", "server.invalid"), datasources().getNames());
     }
 
+    @Test
+    public void shouldGetGroups() {
+        assertEquals(singletonList(DEFAULT_GROUP), datasources().getGroups());
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void shouldThrowExceptionWhenGetDoesNotFindDatasource() {
         datasources().get("file-does-not-exist");
@@ -100,7 +123,7 @@ public class DatasourcesTest {
     }
 
     private static Datasource textDatasource() {
-        return DatasourceFactory.create(UNKNOWN, ANY_TEXT);
+        return DatasourceFactory.create(UNKNOWN, DEFAULT_GROUP, ANY_TEXT);
     }
 
     private static Datasource fileDatasource() {
diff --git a/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriParserTest.java b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriParserTest.java
new file mode 100644
index 0000000..06718a0
--- /dev/null
+++ b/freemarker-generator-base/src/test/java/org/apache/freemarker/generator/uri/NamedUriParserTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.uri;
+
+import org.apache.freemarker.generator.base.uri.NamedUri;
+import org.apache.freemarker.generator.base.uri.NamedUriParser;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class NamedUriParserTest {
+
+    @Test
+    public void shouldParseRelativeFileName() {
+        final NamedUri namedURI = parse("users.csv");
+
+        assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseAbsoluteFileName() {
+        final NamedUri namedURI = parse("/data/users.csv");
+
+        assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("/data/users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseDirectoryName() {
+        final NamedUri namedURI = parse("users/");
+
+        assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("users/", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseFileUri() {
+        final NamedUri namedURI = parse("file:///users.csv");
+
+        assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("file:///users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedFileUri() {
+        final NamedUri namedURI = parse("users=file:///users.csv");
+
+        assertEquals("users", namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("file:///users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedGroupFileUri() {
+        final NamedUri namedURI = parse("users:admin=file:///some-admin-users.csv");
+
+        assertEquals("users", namedURI.getName());
+        assertEquals("admin", namedURI.getGroup());
+        assertEquals("file:///some-admin-users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedWithEmptyGroupFileUri() {
+        final NamedUri namedURI = parse("users:=file:///some-admin-users.csv");
+
+        assertEquals("users", namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("file:///some-admin-users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseEmptyNamedWithGroupFileUri() {
+        final NamedUri namedURI = parse(":admin=file:///some-admin-users.csv");
+
+        assertNull(namedURI.getName());
+        assertEquals("admin", namedURI.getGroup());
+        assertEquals("file:///some-admin-users.csv", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedFileUriWithFragment() {
+        final NamedUri namedURI = parse("users=file:///users.csv#charset=UTF-16&mimetype=text/csv");
+
+        assertEquals("users", namedURI.getName());
+        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.getParameters().get("charset"));
+        assertEquals("text/csv", namedURI.getParameters().get("mimetype"));
+    }
+
+    @Test
+    public void shouldParseUrl() {
+        final NamedUri namedURI = parse("http://google.com");
+
+        assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
+        assertEquals("http://google.com", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseUrlWithFragment() {
+        final NamedUri namedURI = parse("http://google.com#charset=UTF-16");
+
+        assertNull(namedURI.getName());
+        assertEquals("http://google.com#charset=UTF-16", namedURI.getUri().toString());
+        assertEquals(1, namedURI.getParameters().size());
+        assertEquals("UTF-16", namedURI.getParameters().get("charset"));
+    }
+
+    @Test
+    public void shouldParseNamedUrl() {
+        final NamedUri namedURI = parse("google=http://google.com");
+
+        assertEquals("google", namedURI.getName());
+        assertEquals("http://google.com", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedUrlWithQuery() {
+        final NamedUri namedURI = parse("google=http://google.com?foo=bar");
+
+        assertEquals("google", namedURI.getName());
+        assertEquals("http://google.com?foo=bar", namedURI.getUri().toString());
+        assertEquals(0, namedURI.getParameters().size());
+    }
+
+    @Test
+    public void shouldParseNamedUrlWithQueryAndFragment() {
+        final NamedUri namedURI = parse("google=http://google.com?foo=bar#charset=UTF-16");
+
+        assertEquals("google", namedURI.getName());
+        assertEquals("http://google.com?foo=bar#charset=UTF-16", namedURI.getUri().toString());
+        assertEquals(1, namedURI.getParameters().size());
+        assertEquals("UTF-16", namedURI.getParameters().get("charset"));
+    }
+
+    @Test
+    public void shouldParseNamedGroupUrlWithQueryAndFragment() {
+        final NamedUri namedURI = parse("google:web=http://google.com?foo=bar#charset=UTF-16");
+
+        assertEquals("google", namedURI.getName());
+        assertEquals("web", namedURI.getGroup());
+        assertEquals("http://google.com?foo=bar#charset=UTF-16", namedURI.getUri().toString());
+        assertEquals(1, namedURI.getParameters().size());
+        assertEquals("UTF-16", namedURI.getParameters().get("charset"));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void shouldThrowExceptionOnParsingNull() {
+        parse(null);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void shouldThrowExceptionOnParsingEmptyString() {
+        parse(" ");
+    }
+
+    private static NamedUri parse(String value) {
+        return NamedUriParser.parse(value);
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
index 82dbc6f..406c70e 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/Main.java
@@ -16,6 +16,7 @@
  */
 package org.apache.freemarker.generator.cli;
 
+import org.apache.freemarker.generator.base.FreeMarkerConstants.GeneratorMode;
 import org.apache.freemarker.generator.base.util.ClosableUtils;
 import org.apache.freemarker.generator.base.util.StringUtils;
 import org.apache.freemarker.generator.cli.config.Settings;
@@ -33,13 +34,16 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 import static java.util.Objects.requireNonNull;
 import static org.apache.freemarker.generator.cli.config.Suppliers.propertiesSupplier;
@@ -51,9 +55,9 @@ public class Main implements Callable<Integer> {
     private static final String FREEMARKER_CLI_PROPERTY_FILE = "freemarker-cli.properties";
 
     @ArgGroup(multiplicity = "1")
-    private TemplateSourceOptions templateSourceOptions;
+    TemplateSourceOptions templateSourceOptions;
 
-    private static final class TemplateSourceOptions {
+    static final class TemplateSourceOptions {
         @Option(names = { "-t", "--template" }, description = "FreeMarker template to render")
         private String template;
 
@@ -62,52 +66,62 @@ public class Main implements Callable<Integer> {
     }
 
     @Option(names = { "-b", "--basedir" }, description = "Optional template base directory")
-    private String baseDir;
+    String baseDir;
+
+    @Option(names = { "-d", "--datasource" }, description = "Datasource used for rendering")
+    List<String> datasources;
 
     @Option(names = { "-D", "--system-property" }, description = "Set system property")
-    private Properties systemProperties;
+    Properties systemProperties;
 
     @Option(names = { "-e", "--input-encoding" }, description = "Encoding of datasource", defaultValue = "UTF-8")
-    private String inputEncoding;
+    String inputEncoding;
 
     @Option(names = { "-E", "--expose-env" }, description = "Expose environment variables and user-supplied properties globally")
-    private boolean isEnvironmentExposed;
+    boolean isEnvironmentExposed;
 
     @Option(names = { "-l", "--locale" }, description = "Locale being used for the output, e.g. 'en_US'")
-    private String locale;
+    String locale;
+
+    @Option(names = { "-m", "--mode" }, description = "[template|datasource]", defaultValue = "TEMPLATE")
+    GeneratorMode mode;
 
     @Option(names = { "-o", "--output" }, description = "Output file")
-    private String outputFile;
+    String outputFile;
 
     @Option(names = { "-P", "--param" }, description = "Set parameter")
-    private Map<String, String> parameters;
+    Map<String, String> parameters;
 
     @Option(names = { "--config" }, defaultValue = FREEMARKER_CLI_PROPERTY_FILE, description = "FreeMarker CLI configuration file")
-    private String configFile;
+    String configFile;
 
     @Option(names = { "--include" }, description = "File pattern for datasource input directory")
-    private String include;
+    String include;
 
     @Option(names = { "--exclude" }, description = "File pattern for datasource input directory")
-    private String exclude;
+    String exclude;
 
     @Option(names = { "--output-encoding" }, description = "Encoding of output, e.g. UTF-8", defaultValue = "UTF-8")
-    private String outputEncoding;
+    String outputEncoding;
 
     @Option(names = { "--stdin" }, description = "Read datasource from stdin")
-    private boolean readFromStdin;
+    boolean readFromStdin;
 
     @Option(names = { "--times" }, defaultValue = "1", description = "Re-run X times for profiling")
-    private int times;
+    int times;
 
     @Parameters(description = "List of input files and/or input directories")
-    private List<String> sources;
+    List<String> sources;
 
     /** User-supplied command line parameters */
-    private final String[] args;
+    final String[] args;
 
     /** User-supplied writer (used mainly for unit testing) */
-    private Writer userSuppliedWriter;
+    Writer userSuppliedWriter;
+
+    Main() {
+        this.args = new String[0];
+    }
 
     private Main(String[] args) {
         this.args = requireNonNull(args);
@@ -178,7 +192,7 @@ public class Main implements Callable<Integer> {
                 .setOutputEncoding(outputEncoding)
                 .setOutputFile(outputFile)
                 .setParameters(parameters != null ? parameters : new HashMap<>())
-                .setSources(sources != null ? sources : new ArrayList<>())
+                .setDatasources(getCombindedDatasources())
                 .setSystemProperties(systemProperties != null ? systemProperties : new Properties())
                 .setTemplateDirectories(templateDirectories)
                 .setTemplateName(templateSourceOptions.template)
@@ -206,6 +220,27 @@ public class Main implements Callable<Integer> {
         }
     }
 
+    /**
+     * Datasources can be passed via command line option and/or
+     * positional parameter so we need to merge them.
+     *
+     * @return List of datasources
+     */
+    private List<String> getCombindedDatasources() {
+        if (isTemplateDrivenGeneration()) {
+            return Stream.of(datasources, sources)
+                    .filter(Objects::nonNull)
+                    .flatMap(Collection::stream)
+                    .collect(Collectors.toList());
+        } else {
+            throw new IllegalArgumentException("Not implemented yet");
+        }
+    }
+
+    private boolean isTemplateDrivenGeneration() {
+        return mode == GeneratorMode.TEMPLATE;
+    }
+
     private static List<File> getTemplateDirectories(String baseDir) {
         return templateDirectorySupplier(baseDir).get();
     }
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
index c1a63b3..5facf82 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Settings.java
@@ -52,7 +52,7 @@ public class Settings {
     /** List of FreeMarker template directories */
     private final List<File> templateDirectories;
 
-    /** Name of the template to be loaded and rendered  */
+    /** Name of the template to be loaded and rendered */
     private final String templateName;
 
     /** Template provided by the user interactivly */
@@ -86,7 +86,7 @@ public class Settings {
     private final boolean isEnvironmentExposed;
 
     /** User-supplied list of source files or directories */
-    private final List<String> sources;
+    private final List<String> datasources;
 
     /** User-supplied parameters */
     private final Map<String, String> parameters;
@@ -112,7 +112,7 @@ public class Settings {
             Locale locale,
             boolean isReadFromStdin,
             boolean isEnvironmentExposed,
-            List<String> sources,
+            List<String> datasources,
             Map<String, String> parameters,
             Properties sytemProperties,
             Writer writer) {
@@ -133,7 +133,7 @@ public class Settings {
         this.locale = requireNonNull(locale);
         this.isReadFromStdin = isReadFromStdin;
         this.isEnvironmentExposed = isEnvironmentExposed;
-        this.sources = requireNonNull(sources);
+        this.datasources = requireNonNull(datasources);
         this.parameters = requireNonNull(parameters);
         this.sytemProperties = requireNonNull(sytemProperties);
         this.configuration = requireNonNull(configuration);
@@ -204,8 +204,8 @@ public class Settings {
         return isEnvironmentExposed;
     }
 
-    public List<String> getSources() {
-        return sources;
+    public List<String> getDatasources() {
+        return datasources;
     }
 
     public Map<String, String> getParameters() {
@@ -262,7 +262,7 @@ public class Settings {
                 ", locale=" + locale +
                 ", isReadFromStdin=" + isReadFromStdin +
                 ", isEnvironmentExposed=" + isEnvironmentExposed +
-                ", sources=" + sources +
+                ", datasources=" + datasources +
                 ", properties=" + parameters +
                 ", sytemProperties=" + sytemProperties +
                 ", writer=" + writer +
@@ -288,7 +288,7 @@ public class Settings {
         private String locale;
         private boolean isReadFromStdin;
         private boolean isEnvironmentExposed;
-        private List<String> sources;
+        private List<String> datasources;
         private Map<String, String> parameters;
         private Properties systemProperties;
         private Properties configuration;
@@ -302,7 +302,7 @@ public class Settings {
             this.systemProperties = new Properties();
             this.setInputEncoding(DEFAULT_CHARSET.name());
             this.setOutputEncoding(DEFAULT_CHARSET.name());
-            this.sources = emptyList();
+            this.datasources = emptyList();
             this.templateDirectories = emptyList();
         }
 
@@ -380,8 +380,8 @@ public class Settings {
             return this;
         }
 
-        public SettingsBuilder setSources(List<String> sources) {
-            this.sources = sources;
+        public SettingsBuilder setDatasources(List<String> datasources) {
+            this.datasources = datasources;
             return this;
         }
 
@@ -393,7 +393,7 @@ public class Settings {
         }
 
         public SettingsBuilder setSystemProperties(Properties systemProperties) {
-            if(systemProperties != null) {
+            if (systemProperties != null) {
                 this.systemProperties = systemProperties;
             }
             return this;
@@ -432,7 +432,7 @@ public class Settings {
                     LocaleUtils.parseLocale(currLocale),
                     isReadFromStdin,
                     isEnvironmentExposed,
-                    sources,
+                    datasources,
                     parameters,
                     systemProperties,
                     writer
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
index 7d2f5f1..0a7a9f2 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/Suppliers.java
@@ -34,7 +34,7 @@ public class Suppliers {
     }
 
     public static DatasourcesSupplier datasourcesSupplier(Settings settings) {
-        return new DatasourcesSupplier(settings.getSources(),
+        return new DatasourcesSupplier(settings.getDatasources(),
                 settings.getInclude(),
                 settings.getExclude(),
                 settings.getInputEncoding());
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
index f537309..b59bb62 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/task/FreeMarkerTask.java
@@ -37,6 +37,7 @@ import java.util.function.Supplier;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
+import static org.apache.freemarker.generator.base.FreeMarkerConstants.DEFAULT_GROUP;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Location.STDIN;
 import static org.apache.freemarker.generator.base.FreeMarkerConstants.Model.DATASOURCES;
 import static org.apache.freemarker.generator.cli.config.Suppliers.configurationSupplier;
@@ -89,7 +90,7 @@ public class FreeMarkerTask implements Callable<Integer> {
         // Add optional datasource from STDIN at the start of the list since
         // this allows easy sequence slicing in FreeMarker.
         if (settings.isReadFromStdin()) {
-            datasources.add(0, DatasourceFactory.create(STDIN, System.in, STDIN, UTF_8));
+            datasources.add(0, DatasourceFactory.create(STDIN, DEFAULT_GROUP, System.in, STDIN, UTF_8));
         }
 
         return new Datasources(datasources);
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
index e5fa6ac..97f7001 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ExamplesTest.java
@@ -26,7 +26,7 @@ import static org.junit.Assert.assertTrue;
 
 public class ExamplesTest extends AbstractMainTest {
 
-    private static final int MIN_OUTPUT_SIZE = 1;
+    private static final int MIN_OUTPUT_SIZE = 5;
 
     @Test
     public void shouldRunInfo() throws IOException {
@@ -102,6 +102,7 @@ public class ExamplesTest extends AbstractMainTest {
 
     @Test
     public void shouldRunInteractiveTemplateExamples() throws IOException {
+        // @TODO We should check the generated output directly
         assertValid(execute("-i ${JsonPathTool.parse(Datasources.first).read(\"$.info.title\")} site/sample/json/swagger-spec.json"));
         assertValid(execute("-i ${XmlTool.parse(Datasources.first)[\"recipients/person[1]/name\"]} site/sample/xml/recipients.xml"));
         assertValid(execute("-i ${JsoupTool.parse(Datasources.first).select(\"a\")[0]} site/sample/html/dependencies.html"));
@@ -129,5 +130,6 @@ public class ExamplesTest extends AbstractMainTest {
     private static void assertValid(String output) {
         assertTrue(output.length() > MIN_OUTPUT_SIZE);
         assertFalse(output.contains("Exception"));
+        assertFalse(output.contains("Error"));
     }
 }
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/ManualTest.java
index 35b1069..d693905 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
@@ -35,7 +35,8 @@ public class ManualTest {
     // private static final String CMD = "-i ${JsoupTool.parse(Datasources.first).select('a')[0]} site/sample/html/dependencies.html";
     // private static final String CMD = "-b ./src/test -t templates/properties/csv/locker-test-users.ftl site/sample/properties";
     // private static final String CMD = "-b ./src/test -e UTF-8 -l de_AT -Dcolumn=Order%20ID -Dvalues=226939189,957081544 -Dformat=DEFAULT -Ddelimiter=COMMA -t templates/csv/md/filter.ftl site/sample/csv/sales-records.csv";
-    private static final String CMD = "-E -b ./src/test -t templates/environment.ftl";
+    // private static final String CMD = "-E -b ./src/test -t templates/environment.ftl";
+    private static final String CMD = "-b ./src/test -l de_AT -DFOO=foo -DBAR=bar -t templates/info.ftl -d user:admin=site/sample/csv/contract.csv#charset=UTF-16 google:www=https://www.google.com?foo=bar#contenttype=application/json";
 
     public static void main(String[] args) {
         Main.execute(toArgs(CMD));
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
new file mode 100644
index 0000000..95662f0
--- /dev/null
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/PicocliTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.cli;
+
+import org.junit.Test;
+import picocli.CommandLine;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class PicocliTest {
+
+    private static final String TEMPLATE = "template.ftl";
+    private static final String ANY_FILE = "users.csv";
+    private static final String ANY_NAMED_FILE = "users=users.csv";
+    private static final String OTHER_FILE = "transctions.csv";
+    private static final String ANY_FILE_URI = "file:///users.csv";
+    private static final String OTHER_FILE_URI = "file:///transctions.csv";
+
+    @Test
+    public void testSinglePositionalParameter() {
+        assertEquals(ANY_FILE_URI, parse("-t", TEMPLATE, ANY_FILE_URI).sources.get(0));
+        assertNull(ANY_FILE, parse("-t", TEMPLATE, ANY_FILE_URI).datasources);
+    }
+
+    @Test
+    public void testMultiplePositionalParameter() {
+        assertEquals(ANY_FILE, parse("-t", TEMPLATE, ANY_FILE, OTHER_FILE).sources.get(0));
+        assertEquals(OTHER_FILE, parse("-t", TEMPLATE, ANY_FILE, OTHER_FILE).sources.get(1));
+
+        assertEquals(ANY_FILE, parse("-t", TEMPLATE, ANY_FILE, OTHER_FILE_URI).sources.get(0));
+        assertEquals(OTHER_FILE_URI, parse("-t", TEMPLATE, ANY_FILE, OTHER_FILE_URI).sources.get(1));
+
+        assertEquals(ANY_FILE_URI, parse("-t", TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sources.get(0));
+        assertEquals(OTHER_FILE_URI, parse("-t", TEMPLATE, ANY_FILE_URI, OTHER_FILE_URI).sources.get(1));
+    }
+
+    @Test
+    public void testSingleNamedDatasource() {
+        assertEquals(ANY_FILE, parse("-t", TEMPLATE, ANY_FILE).sources.get(0));
+        assertEquals(ANY_FILE, parse("-t", TEMPLATE, "-d", ANY_FILE).datasources.get(0));
+        assertEquals(ANY_FILE, parse("-t", TEMPLATE, "--datasource", ANY_FILE).datasources.get(0));
+        assertEquals(ANY_FILE_URI, parse("-t", TEMPLATE, "--datasource", ANY_FILE_URI).datasources.get(0));
+    }
+
+    @Test
+    public void testMultipleNamedDatasource() {
+        final Main main = parse("-t", TEMPLATE, "-d", ANY_FILE, "--datasource", OTHER_FILE_URI);
+
+        assertEquals(ANY_FILE, main.datasources.get(0));
+        assertEquals(OTHER_FILE_URI, main.datasources.get(1));
+        assertNull(main.sources);
+    }
+
+    private static Main parse(String... args) {
+        final Main main = new Main();
+        new CommandLine(main).parseArgs(args);
+        return main;
+    }
+}
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
index 4821723..655fdf6 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SettingsTest.java
@@ -56,7 +56,7 @@ public class SettingsTest {
         assertEquals(ANY_OUTPUT_ENCODING, settings.getOutputEncoding().name());
         assertEquals(ANY_OUTPUT_FILE, settings.getOutputFile().getName());
         assertEquals(ANY_TEMPLATE_NAME, settings.getTemplateName());
-        assertNotNull(settings.getSources());
+        assertNotNull(settings.getDatasources());
         assertNotNull(settings.getParameters());
         assertNotNull(settings.getSytemProperties());
         assertTrue(settings.isReadFromStdin());
@@ -78,7 +78,7 @@ public class SettingsTest {
                 .setOutputEncoding(ANY_OUTPUT_ENCODING)
                 .setOutputFile(ANY_OUTPUT_FILE)
                 .setParameters(ANY_USER_PARAMETERS)
-                .setSources(ANY_SOURCES)
+                .setDatasources(ANY_SOURCES)
                 .setSystemProperties(ANY_SYSTEM_PROPERTIES)
                 .setTemplateName(ANY_TEMPLATE_NAME)
                 .setWriter(new StringWriter())
diff --git a/freemarker-generator-cli/templates/info.ftl b/freemarker-generator-cli/templates/info.ftl
index 3ec4891..6b9380b 100644
--- a/freemarker-generator-cli/templates/info.ftl
+++ b/freemarker-generator-cli/templates/info.ftl
@@ -16,7 +16,7 @@
   under the License.
 -->
 FreeMarker CLI Information
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 FreeMarker version     : ${.version}
 Template name          : ${.current_template_name}
 Language               : ${.lang}
@@ -26,13 +26,13 @@ Output encoding        : ${.output_encoding}
 Output format          : ${.output_format}
 
 FreeMarker CLI Template Directories
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 <#list SystemTool.getTemplateDirectories() as directory>
-[${directory?counter}] ${directory}
+[#${directory?counter}] ${directory}
 </#list>
 
 FreeMarker CLI Tools
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 <#list .data_model?keys?sort as key>
 <#if key?ends_with("Tool")>
 - ${key?right_pad(20)} : ${.data_model[key]}
@@ -40,25 +40,26 @@ FreeMarker CLI Tools
 </#list>
 
 FreeMarker CLI Datasources
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 <#list Datasources.list as datasource>
-[${datasource?counter}] ${datasource.name}, ${datasource.location}, ${datasource.length} Bytes
+[#${datasource?counter}], name=${datasource.name}, group=${datasource.group}, charset=${datasource.charset}, length= ${datasource.length} Bytes
+Location : ${datasource.location}
 </#list>
 
 User Supplied Parameters
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 <#list SystemTool.parameters as name,value>
 - ${name} ==> ${value}
 </#list>
 
 User Supplied System Properties
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 <#list SystemTool.userSystemProperties as name,value>
 - ${name} ==> ${value}
 </#list>
 
 SystemTool
----------------------------------------------------------------------------
+------------------------------------------------------------------------------
 Command line         : ${SystemTool.getCommandLineArgs()?join(", ")}
 Host Name            : ${SystemTool.getHostName()}
 Java Home            : ${SystemTool.getEnv("JAVA_HOME", "N.A.")}
diff --git a/freemarker-generator-maven-plugin/src/main/java/org/apache/freemarker/generator/maven/JsonPropertiesProvider.java b/freemarker-generator-maven-plugin/src/main/java/org/apache/freemarker/generator/maven/JsonPropertiesProvider.java
index f2e2cc4..1d16822 100644
--- a/freemarker-generator-maven-plugin/src/main/java/org/apache/freemarker/generator/maven/JsonPropertiesProvider.java
+++ b/freemarker-generator-maven-plugin/src/main/java/org/apache/freemarker/generator/maven/JsonPropertiesProvider.java
@@ -54,6 +54,7 @@ public class JsonPropertiesProvider implements OutputGeneratorPropertiesProvider
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public void providePropertiesFromFile(Path path, OutputGenerator.OutputGeneratorBuilder builder) {
         final File jsonDataFile = path.toFile();
         final Map<String, Object> data = parseJson(jsonDataFile);
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/properties/PropertiesToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/properties/PropertiesToolTest.java
index 3a29908..5a9fc79 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/properties/PropertiesToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/properties/PropertiesToolTest.java
@@ -24,6 +24,7 @@ import static junit.framework.TestCase.assertEquals;
 
 public class PropertiesToolTest {
 
+    private static final String ANY_GROUP = "group";
     private static final String ANY_PROPERTIES_STRING = "foo=bar";
 
     @Test
@@ -43,6 +44,6 @@ public class PropertiesToolTest {
     }
 
     private Datasource datasource(String value) {
-        return DatasourceFactory.create("test.properties", value);
+        return DatasourceFactory.create("test.properties", ANY_GROUP, value);
     }
 }
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
index ee34a28..90be879 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/snakeyaml/SnakeYamlToolTest.java
@@ -27,6 +27,8 @@ import static junit.framework.TestCase.assertEquals;
 
 public class SnakeYamlToolTest {
 
+    private static final String ANY_GROUP = "group";
+
     private static final String ANY_YAML_STRING = "docker:\n" +
             "    - image: ubuntu:14.04\n" +
             "    - image: mongo:2.6.8\n" +
@@ -56,6 +58,6 @@ public class SnakeYamlToolTest {
     }
 
     private Datasource datasource(String value) {
-        return DatasourceFactory.create("test.yml", value);
+        return DatasourceFactory.create("test.yml", ANY_GROUP, value);
     }
 }
diff --git a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/xml/XmlToolTest.java b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/xml/XmlToolTest.java
index 79a5432..c2b16fa 100644
--- a/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/xml/XmlToolTest.java
+++ b/freemarker-generator-tools/src/test/java/org/apache/freemarker/generator/tools/xml/XmlToolTest.java
@@ -26,6 +26,8 @@ import static junit.framework.TestCase.assertNotNull;
 
 public class XmlToolTest {
 
+    private static final String ANY_GROUP = "group";
+
     private static final String ANY_XML_STRING = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
             "<note>\n" +
             "  <to>Tove</to>\n" +
@@ -57,6 +59,6 @@ public class XmlToolTest {
     }
 
     private Datasource datasource(String value) {
-        return DatasourceFactory.create("test.xml", value);
+        return DatasourceFactory.create("test.xml", ANY_GROUP, value);
     }
 }