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 10:19:22 UTC

[freemarker-generator] branch FREEMARKER-135 updated: FREEMARKER-135 Support user-supplied names for datasources

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


The following commit(s) were added to refs/heads/FREEMARKER-135 by this push:
     new f2322d1  FREEMARKER-135 Support user-supplied names for datasources
f2322d1 is described below

commit f2322d1679885995445d7af88826cc7e76662224
Author: Siegfried Goeschl <si...@gmail.com>
AuthorDate: Sat Feb 29 11:19:05 2020 +0100

    FREEMARKER-135 Support user-supplied names for datasources
---
 .../generator/base/FreeMarkerConstants.java        |   3 +
 .../activation/MimetypesFileTypeMapFactory.java    |   2 +-
 .../generator/base/datasource/Datasource.java      |  23 ++--
 .../base/datasource/DatasourceFactory.java         |  35 ++---
 .../generator/base/datasource/Datasources.java     |  20 +--
 .../base/datasource/DatasourcesSupplier.java       |  38 +++---
 .../freemarker/generator/base/uri/NamedUri.java    |  29 ++--
 .../generator/base/uri/NamedUriParser.java         |  24 +++-
 .../generator/base/util/StringUtils.java           |   5 +
 .../freemarker/generator/base/util/Validate.java   | 150 +++++++++++++++++++++
 .../datasource/DatasourceFactoryTest.java          |   8 +-
 .../generator/datasource/DatasourceTest.java       |  11 +-
 .../generator/datasource/DatasourcesTest.java      |  21 ++-
 .../generator/uri/NamedUriParserTest.java          |  37 +++++
 .../generator/cli/task/FreeMarkerTask.java         |   3 +-
 .../freemarker/generator/cli/ExamplesTest.java     |   4 +-
 .../freemarker/generator/cli/ManualTest.java       |   2 +-
 freemarker-generator-cli/templates/info.ftl        |   2 +-
 .../tools/properties/PropertiesToolTest.java       |   3 +-
 .../tools/snakeyaml/SnakeYamlToolTest.java         |   4 +-
 .../generator/tools/xml/XmlToolTest.java           |   4 +-
 21 files changed, 343 insertions(+), 85 deletions(-)

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 c40c29e..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,9 @@ 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
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
index 3631395..b75a422 100644
--- 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
@@ -32,7 +32,7 @@ public class MimetypesFileTypeMapFactory {
             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");
+            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");
         }
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 c3c45b8..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,11 +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;
 
-    /** Cached content type - UrlDataSource actually opens a connection so we should cache the result */
-    private String contentType = null;
-
-    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);
@@ -74,6 +76,10 @@ public class Datasource implements Closeable {
         return name;
     }
 
+    public String getGroup() {
+        return group;
+    }
+
     public String getBaseName() {
         return FilenameUtils.getBaseName(name);
     }
@@ -86,12 +92,8 @@ public class Datasource implements Closeable {
         return charset;
     }
 
-    public synchronized String getContentType() {
-        if (contentType == null) {
-            contentType = dataSource.getContentType();
-        }
-
-        return contentType;
+    public String getContentType() {
+        return dataSource.getContentType();
     }
 
     public String getLocation() {
@@ -224,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 c53be60..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
@@ -31,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.
@@ -41,48 +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, URL url, Charset charset) {
+    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, dataSource, location, charset);
+        return create(name, group, dataSource, location, charset);
     }
 
-    public static Datasource create(String name, String content) {
+    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(), file, charset);
+        return create(file.getName(), DEFAULT_GROUP, file, charset);
     }
 
-    public static Datasource create(String name, File file, Charset charset) {
+    public static Datasource create(String name, String group, File file, Charset charset) {
         final FileDataSource dataSource = new FileDataSource(file);
         dataSource.setFileTypeMap(MimetypesFileTypeMapFactory.create());
-        return create(name, dataSource, file.getAbsolutePath(), charset);
+        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..5381faa 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
@@ -69,14 +69,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 +102,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 c671fcf..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
@@ -33,6 +33,7 @@ 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
@@ -81,8 +82,7 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
             } else {
                 return resolveFile(source, include, exclude, charset);
             }
-        }
-        catch(RuntimeException e) {
+        } catch (RuntimeException e) {
             throw new RuntimeException("Unable to create the datasource: " + source, e);
         }
     }
@@ -90,19 +90,20 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
     private static Datasource resolveHttpUrl(String source) {
         final NamedUri namedUri = NamedUriParser.parse(source);
         final URI uri = namedUri.getUri();
-        final String location = uri.getScheme() + "://" + uri.getHost();
-        final String name = namedUri.hasName() ? namedUri.getName() : location;
-        final Charset currCharset = getCharset(namedUri, UTF_8);
-        return DatasourceFactory.create(name, toUrl(uri), currCharset);
+        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) {
         final NamedUri namedUri = NamedUriParser.parse(source);
         final String path = namedUri.getUri().getPath();
-        final String name = namedUri.hasName() ? namedUri.getName() : path;
-        final Charset currCharset = getCharset(namedUri, charset);
+        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, file, currCharset))
+                .map(file -> DatasourceFactory.create(name, group, file, currCharset))
                 .collect(toList());
     }
 
@@ -114,14 +115,6 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
         return value.contains("http://") || value.contains("https://");
     }
 
-    private static URL toUrl(String value) {
-        try {
-            return new URL(value);
-        } catch (MalformedURLException e) {
-            throw new IllegalArgumentException(value, e);
-        }
-    }
-
     private static URL toUrl(URI uri) {
         try {
             return uri.toURL();
@@ -130,7 +123,16 @@ public class DatasourcesSupplier implements Supplier<List<Datasource>> {
         }
     }
 
-    private static Charset getCharset(NamedUri namedUri, Charset def) {
+    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
index c2dffdb..0b1211d 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
@@ -19,7 +19,6 @@ package org.apache.freemarker.generator.base.uri;
 import java.net.URI;
 import java.util.Map;
 
-import static java.util.Collections.emptyMap;
 import static java.util.Objects.requireNonNull;
 import static org.apache.freemarker.generator.base.util.StringUtils.isEmpty;
 
@@ -31,26 +30,25 @@ 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) {
-        this.name = null;
-        this.uri = requireNonNull(uri);
-        this.parameters = emptyMap();
-    }
-
     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, URI uri, Map<String, String> parameters) {
-        this.name = requireNonNull(name);
+    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);
     }
@@ -59,6 +57,10 @@ public class NamedUri {
         return name;
     }
 
+    public String getGroup() {
+        return group;
+    }
+
     public URI getUri() {
         return uri;
     }
@@ -71,12 +73,21 @@ public class NamedUri {
         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/uri/NamedUriParser.java b/freemarker-generator-base/src/main/java/org/apache/freemarker/generator/base/uri/NamedUriParser.java
index 174a303..a5ab42b 100644
--- 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
@@ -16,34 +16,44 @@
  */
 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>user=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 REGEXP_GROUP_NAME = "name";
-    private static final String REGEXP_GROUP_URI = "uri";
-    private static final Pattern NAMED_URI_REGEXP = Pattern.compile("^(?<name>[a-zA-Z0-9-_]*)=(?<uri>.*)");
+    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(REGEXP_GROUP_NAME);
-            final URI uri = uri(matcher.group(REGEXP_GROUP_URI));
-            return new NamedUri(name, uri, parameters(uri));
+            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));
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..ba7327b 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,9 @@ public class StringUtils {
     public static boolean isEmpty(String value) {
         return value == null || value.trim().isEmpty();
     }
+
+    public 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/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 5aba8de..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,8 +45,9 @@ 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());
@@ -59,6 +62,7 @@ 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());
@@ -69,11 +73,12 @@ public class DatasourceTest {
         }
     }
 
-    @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());
@@ -130,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..6e492ef 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,7 @@ import java.net.URL;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
+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 +42,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 +64,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));
     }
@@ -100,7 +117,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
index 075491d..e9a7024 100644
--- 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
@@ -30,6 +30,7 @@ public class NamedUriParserTest {
         final NamedUri namedURI = parse("users.csv");
 
         assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
         assertEquals("users.csv", namedURI.getUri().toString());
         assertEquals(0, namedURI.getParameters().size());
     }
@@ -39,6 +40,7 @@ public class NamedUriParserTest {
         final NamedUri namedURI = parse("users/");
 
         assertNull(namedURI.getName());
+        assertNull(namedURI.getGroup());
         assertEquals("users/", namedURI.getUri().toString());
         assertEquals(0, namedURI.getParameters().size());
     }
@@ -48,6 +50,7 @@ public class NamedUriParserTest {
         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());
     }
@@ -57,15 +60,27 @@ public class NamedUriParserTest {
         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 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"));
@@ -77,6 +92,7 @@ public class NamedUriParserTest {
         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());
     }
@@ -119,6 +135,27 @@ public class NamedUriParserTest {
         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/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 15e918a..2fc7d43 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
@@ -36,7 +36,7 @@ public class ManualTest {
     // 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 = "-b ./src/test -l de_AT -DFOO=foo -DBAR=bar -t templates/info.ftl google=https://google.com?foo=bar#charset=UTF-16";
+    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 google:www=https://www.google.com?foo=bar";
 
     public static void main(String[] args) {
         Main.execute(toArgs(CMD));
diff --git a/freemarker-generator-cli/templates/info.ftl b/freemarker-generator-cli/templates/info.ftl
index 2000d4e..0e69545 100644
--- a/freemarker-generator-cli/templates/info.ftl
+++ b/freemarker-generator-cli/templates/info.ftl
@@ -42,7 +42,7 @@ FreeMarker CLI Tools
 FreeMarker CLI Datasources
 ---------------------------------------------------------------------------
 <#list Datasources.list as datasource>
-[${datasource?counter}] ${datasource.name}, ${datasource.location}, ${datasource.charset}, ${datasource.contentType}, ${datasource.length} Bytes
+[${datasource?counter}] ${datasource.name}, ${datasource.group}, ${datasource.location}, ${datasource.charset}, ${datasource.contentType}, ${datasource.length} Bytes
 </#list>
 
 User Supplied Parameters
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);
     }
 }