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 2021/10/03 20:13:35 UTC

[freemarker-generator] 01/01: FREEMARKER-195 [freemarker-generator] Improve exposure of DataSources using TemplateHashModelEx2

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

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

commit 18eb24ba9f5c2022ebf410e5ce79f8295a4791dc
Author: Siegfried Goeschl <si...@gmail.com>
AuthorDate: Sun Oct 3 22:13:23 2021 +0200

    FREEMARKER-195 [freemarker-generator] Improve exposure of DataSources using TemplateHashModelEx2
---
 .../generator/base/datasource/DataSources.java     |   9 ++
 freemarker-generator-cli/CHANGELOG.md              |   9 +-
 .../src/app/examples/templates/datasources.ftl     | 105 +++++++------
 .../cli/config/ConfigurationSupplier.java          |   5 +
 .../generator/cli/model/DataSourcesAdapter.java    | 165 +++++++++++++++++++++
 .../model/FreeMarkerGeneratorObjectWrapper.java    |  39 +++++
 .../generator/cli/task/FreeMarkerTask.java         |   3 +-
 .../freemarker/generator/cli/ExamplesTest.java     |   1 +
 .../cli/config/ConfigurationSupplierTest.java      |   3 +-
 .../generator/cli/config/SuppliersTest.java        |   2 -
 pom.xml                                            |   2 +-
 11 files changed, 292 insertions(+), 51 deletions(-)

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 fcb3432..e6b4c3f 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
@@ -89,6 +89,15 @@ public class DataSources implements Closeable {
     }
 
     /**
+     * Get an array representation of the underlying data sources.
+     *
+     * @return list of data sources
+     */
+    public DataSource[] toArray() {
+        return dataSources.toArray(new DataSource[0]);
+    }
+
+    /**
      * Get a list representation of the underlying data sources.
      *
      * @return list of data sources
diff --git a/freemarker-generator-cli/CHANGELOG.md b/freemarker-generator-cli/CHANGELOG.md
index 8a250ec..7e3ccbc 100644
--- a/freemarker-generator-cli/CHANGELOG.md
+++ b/freemarker-generator-cli/CHANGELOG.md
@@ -2,7 +2,12 @@
 
 All notable changes to this project will be documented in this file. We try to adhere to https://github.com/olivierlacan/keep-a-changelog.
 
-## 0.1.0-SNAPSHOT
+## 0.2.0-SNAPSHOT
+
+## 0.1.0-SNAPSHOT (unreleased)
+
+### Changed
+* [FREEMARKER-195] Improve exposure of DataSources using TemplateHashModelEx2
 
 ### Added
 * Use `-Xverify:none -XX:TieredStopAtLevel=1` to improve startup time of CLI
@@ -22,7 +27,6 @@ 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))
 * [FREEMARKER-129] Provide a `toString()` method for all tools
 
-### Changed
 * [FREEMARKER-182] Upgrade to Apache FreeMarker 2.3.31
 * [FREEMARKER-175] Use latest FreeMarker version
 * [FREEMARKER-173] Allow to pass arbitrary key/value pairs to DataSource when using NamedURIs
@@ -85,4 +89,5 @@ All notable changes to this project will be documented in this file. We try to a
 [FREEMARKER-181]: https://issues.apache.org/jira/browse/FREEMARKER-181
 [FREEMARKER-182]: https://issues.apache.org/jira/browse/FREEMARKER-182
 [FREEMARKER-188]: https://issues.apache.org/jira/browse/FREEMARKER-188
+[FREEMARKER-195]: https://issues.apache.org/jira/browse/FREEMARKER-195
 
diff --git a/freemarker-generator-cli/src/app/examples/templates/datasources.ftl b/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
index 4c7d11f..ec02a83 100644
--- a/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
+++ b/freemarker-generator-cli/src/app/examples/templates/datasources.ftl
@@ -1,4 +1,4 @@
-<#ftl output_format="plainText">
+<#ftl output_format="plainText" strip_whitespace=true>
 <#--
   Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
@@ -17,64 +17,83 @@
 -->
 Support FreeMarker Directives
 ==============================================================================
-has_content: ${dataSources?has_content?c}
-size: ${dataSources?size}
+dataSources?has_content: ${dataSources?has_content?c}
+dataSources?size: ${dataSources?size}
 
 Use FTL Array-style Access
 ==============================================================================
-${dataSources?values[0].name}
-${dataSources?values?first.name}
+<#if dataSources?has_content>
+    dataSources[0]: ${dataSources[0].name}
+<#else>
+    No data sources provided ...
+</#if>
 
-Get Document Names As Keys
+Iterate Over DataSources as List
 ==============================================================================
-<#list dataSources?keys as name>
-    ${name}<#lt>
+<#list dataSources as dataSource>
+    dataSource[${dataSource?index}] => ${dataSource.uri}<#lt>
 </#list>
 
-Iterate Over Names & DataSources
+Iterate Over DataSources as Map
 ==============================================================================
 <#list dataSources as name, dataSource>
-    ${name} => ${dataSource.uri}<#lt>
+    dataSource["${name}"] => ${dataSource.uri}<#lt>
+</#list>
+
+Iterate Over DataSources as Values
+==============================================================================
+<#list dataSources?values as dataSource>
+    dataSource[${dataSource?index}] => ${dataSource.uri}<#lt>
+</#list>
+
+Get Document Names As Keys
+==============================================================================
+<#list dataSources?keys as name>
+    - ${name}<#lt>
 </#list>
 
+Access Underlying DataSources API
+==============================================================================
+DataSources.getNames(): ${dataSources?api.names?size}
+DataSources.getGroups(): ${dataSources?api.getGroups()?size}
+DataSources.find(): ${dataSources?api.find("*")?size}
+
+Iterate Over DataSources Using Wildcard Search
+==============================================================================
 <#if dataSources?has_content>
-    <#list dataSources?values as dataSource>
-        <@writeDataSource dataSource/>
+    <#list dataSources?api.find("*") as dataSource>
+        - ${dataSource.name}
     </#list>
 <#else>
-    No data sources found ...
+    No data sources provided ...
 </#if>
 
-<#macro writeDataSource dataSource>
-
-${dataSource.name}
-==============================================================================
+<#if dataSources?has_content>
+    <#list dataSources?values as dataSource>
+        [#${dataSource?counter}] - ${dataSource.name}
+        ==============================================================================
 
-Invoke Arbitrary Methods On DataSource
----------------------------------------------------------------------------
-<#assign dataSource=dataSources?values?first>
-Name                : ${dataSource.name}
-Group               : ${dataSource.group}
-Nr of lines         : ${dataSource.lines?size}
-ContentType         : ${dataSource.contentType}
-MimeType            : ${dataSource.mimeType}
-Charset             : ${dataSource.charset}
-Extension           : ${dataSource.extension}
-Nr of chars         : ${dataSource.text?length}
-Nr of bytes         : ${dataSource.bytes?size}
-File name           : ${dataSource.fileName}
-URI schema          : ${dataSource.uri.scheme}
-Relative File Path  : ${dataSource.relativeFilePath}
+        Invoke Arbitrary Methods On DataSource
+        ---------------------------------------------------------------------------
+        <#assign dataSource=dataSources?values?first>
+        Name                : ${dataSource.name}
+        Nr of lines         : ${dataSource.lines?size}
+        Content Type        : ${dataSource.contentType}
+        Charset             : ${dataSource.charset}
+        Extension           : ${dataSource.extension}
+        Nr of chars         : ${dataSource.text?length}
+        Nr of bytes         : ${dataSource.bytes?size}
 
-Iterating Over Metadata Of A Datasource
----------------------------------------------------------------------------
-<#list dataSource.metadata as name, value>
-${name?right_pad(15)} : ${value}
-</#list>
+        Iterating Over Metadata Of A Datasource
+        ---------------------------------------------------------------------------
+        <#list dataSource.metadata as name, value>
+            ${name?right_pad(19)} : ${value}
+        </#list>
 
-Iterating Over Properties Of A Datasource
----------------------------------------------------------------------------
-<#list dataSource.properties as name, value>
-${name?right_pad(15)} : ${value}
-</#list>
-</#macro>
+        Iterating Over Properties Of A Datasource
+        ---------------------------------------------------------------------------
+        <#list dataSource.properties as name, value>
+            ${name?right_pad(19)} : ${value}
+        </#list>
+    </#list>
+</#if>
\ No newline at end of file
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
index 593f7f7..63f21c0 100644
--- a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplier.java
@@ -20,6 +20,7 @@ import freemarker.cache.TemplateLoader;
 import freemarker.template.Configuration;
 import freemarker.template.Version;
 import org.apache.freemarker.generator.base.util.PropertiesTransformer;
+import org.apache.freemarker.generator.cli.model.FreeMarkerGeneratorObjectWrapper;
 
 import java.util.Properties;
 import java.util.function.Supplier;
@@ -50,6 +51,10 @@ public class ConfigurationSupplier implements Supplier<Configuration> {
         try {
             final Configuration configuration = new Configuration(FREEMARKER_VERSION);
 
+            // support a custom "DataSourcesAdaptor"
+            configuration.setAPIBuiltinEnabled(true);
+            configuration.setObjectWrapper(new FreeMarkerGeneratorObjectWrapper(configuration.getIncompatibleImprovements()));
+
             // apply all "freemarker.configuration.setting" values
             configuration.setSettings(freeMarkerConfigurationSettings());
 
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesAdapter.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesAdapter.java
new file mode 100644
index 0000000..2fd3582
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/DataSourcesAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.cli.model;
+
+import freemarker.core._DelayedJQuote;
+import freemarker.core._TemplateModelException;
+import freemarker.ext.util.WrapperTemplateModel;
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.MapKeyValuePairIterator;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.SimpleCollection;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelWithAPISupport;
+import freemarker.template.TemplateSequenceModel;
+import freemarker.template.WrappingTemplateModel;
+import freemarker.template.utility.ObjectWrapperWithAPISupport;
+import org.apache.freemarker.generator.base.datasource.DataSource;
+import org.apache.freemarker.generator.base.datasource.DataSources;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.SortedMap;
+
+/**
+ * Wraps a map of <code>DataSorces</code> into a FreeMarker template model
+ * providing sequence and hash type access.
+ */
+public class DataSourcesAdapter extends WrappingTemplateModel
+        implements TemplateHashModelEx2, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport, TemplateSequenceModel,
+        Serializable {
+
+    private final DataSources dataSources;
+    private final Map<String, DataSource> map;
+
+    /**
+     * Factory method for creating new adapter instances.
+     *
+     * @param dataSources The dataSources to adapt; can't be {@code null}.
+     * @param wrapper     The {@link ObjectWrapper} used to wrap the items in the array.
+     */
+    public static DataSourcesAdapter create(DataSources dataSources, ObjectWrapperWithAPISupport wrapper) {
+        return new DataSourcesAdapter(dataSources, wrapper);
+    }
+
+    private DataSourcesAdapter(DataSources dataSources, ObjectWrapper wrapper) {
+        super(wrapper);
+        this.dataSources = dataSources;
+        this.map = dataSources.toMap();
+    }
+
+    @Override
+    public TemplateModel get(String key) throws TemplateModelException {
+        Object val;
+        try {
+            val = map.get(key);
+        } catch (ClassCastException e) {
+            throw new _TemplateModelException(e,
+                    "ClassCastException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        } catch (NullPointerException e) {
+            throw new _TemplateModelException(e,
+                    "NullPointerException while getting Map entry with String key ",
+                    new _DelayedJQuote(key));
+        }
+
+        if (val == null) {
+            // Check for Character key if this is a single-character string.
+            // In SortedMap-s, however, we can't do that safely, as it can cause ClassCastException.
+            if (key.length() == 1 && !(map instanceof SortedMap)) {
+                final Character charKey = key.charAt(0);
+                try {
+                    val = map.get(charKey);
+                    if (val == null) {
+                        final TemplateModel wrappedNull = wrap(null);
+                        if (wrappedNull == null || !(map.containsKey(key) || map.containsKey(charKey))) {
+                            return null;
+                        } else {
+                            return wrappedNull;
+                        }
+                    }
+                } catch (ClassCastException e) {
+                    throw new _TemplateModelException(e,
+                            "Class casting exception while getting Map entry with Character key ",
+                            new _DelayedJQuote(charKey));
+                } catch (NullPointerException e) {
+                    throw new _TemplateModelException(e,
+                            "NullPointerException while getting Map entry with Character key ",
+                            new _DelayedJQuote(charKey));
+                }
+            } else {  // No char key fallback was possible
+                final TemplateModel wrappedNull = wrap(null);
+                if (wrappedNull == null || !map.containsKey(key)) {
+                    return null;
+                } else {
+                    return wrappedNull;
+                }
+            }
+        }
+
+        return wrap(val);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return map.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        return map.size();
+    }
+
+    @Override
+    public TemplateCollectionModel keys() {
+        return new SimpleCollection(map.keySet(), getObjectWrapper());
+    }
+
+    @Override
+    public TemplateCollectionModel values() {
+        return new SimpleCollection(map.values(), getObjectWrapper());
+    }
+
+    @Override
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+
+    @Override
+    public TemplateModel get(int index) throws TemplateModelException {
+        final DataSource[] array = this.dataSources.toArray();
+        return index >= 0 && index < array.length ? wrap(array[index]) : null;
+    }
+
+    @Override
+    public Object getAdaptedObject(Class hint) {
+        return dataSources;
+    }
+
+    @Override
+    public Object getWrappedObject() {
+        return dataSources;
+    }
+
+    @Override
+    public TemplateModel getAPI() throws TemplateModelException {
+        return ((ObjectWrapperWithAPISupport) getObjectWrapper()).wrapAsAPI(dataSources);
+    }
+}
diff --git a/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/FreeMarkerGeneratorObjectWrapper.java b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/FreeMarkerGeneratorObjectWrapper.java
new file mode 100644
index 0000000..08027ce
--- /dev/null
+++ b/freemarker-generator-cli/src/main/java/org/apache/freemarker/generator/cli/model/FreeMarkerGeneratorObjectWrapper.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.freemarker.generator.cli.model;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.Version;
+import org.apache.freemarker.generator.base.datasource.DataSources;
+
+public class FreeMarkerGeneratorObjectWrapper extends DefaultObjectWrapper {
+
+    public FreeMarkerGeneratorObjectWrapper(Version incompatibleImprovements) {
+        super(incompatibleImprovements);
+    }
+
+    @Override
+    protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException {
+        if (obj instanceof DataSources) {
+            return DataSourcesAdapter.create((DataSources) obj, this);
+        }
+
+        return super.handleUnknownType(obj);
+    }
+}
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 914bd79..8e158e4 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
@@ -138,8 +138,7 @@ public class FreeMarkerTask implements Callable<Integer> {
     private static Map<String, Object> toTemplateDataModel(DataSources dataSources, Map<String, Object>... maps) {
         final Map<String, Object> result = new HashMap<>();
         Arrays.stream(maps).forEach(result::putAll);
-        // expose only the map and not the "DataSources" instance (see FREEMARKER-174)
-        result.put(Model.DATASOURCES, dataSources.toMap());
+        result.put(Model.DATASOURCES, dataSources);
         return result;
     }
 
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 a5192e4..7bbe810 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
@@ -49,6 +49,7 @@ public class ExamplesTest extends AbstractMainTest {
 
     @Test
     public void shouldRunDataSourceExamples() throws IOException {
+        assertValid(execute("-t src/app/examples/templates/datasources.ftl"));
         assertValid(execute("-t src/app/examples/templates/datasources.ftl -s :csv=src/app/examples/data/csv"));
     }
 
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
index ae9fa68..b2daff2 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/ConfigurationSupplierTest.java
@@ -41,11 +41,12 @@ public class ConfigurationSupplierTest {
         assertTrue(configuration.isOutputEncodingSet());
 
         assertFalse(configuration.isCacheStorageExplicitlySet());
-        assertFalse(configuration.isObjectWrapperExplicitlySet());
+        assertTrue(configuration.isObjectWrapperExplicitlySet());
         assertFalse(configuration.isOutputFormatExplicitlySet());
         assertFalse(configuration.isTemplateExceptionHandlerExplicitlySet());
         assertFalse(configuration.isTimeZoneExplicitlySet());
         assertFalse(configuration.isWrapUncheckedExceptionsExplicitlySet());
+        assertTrue(configuration.isAPIBuiltinEnabled());
     }
 
     private ConfigurationSupplier configurationSupplier(Settings settings) {
diff --git a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
index a772bb2..fa9b47e 100644
--- a/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
+++ b/freemarker-generator-cli/src/test/java/org/apache/freemarker/generator/cli/config/SuppliersTest.java
@@ -39,7 +39,6 @@ import java.util.Properties;
 
 import static java.util.Collections.singletonList;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -95,7 +94,6 @@ public class SuppliersTest {
 
         assertNotNull(configuration.getSharedVariable(Model.TOOLS));
         assertTrue(configuration.isTemplateLoaderExplicitlySet());
-        assertFalse(configuration.isObjectWrapperExplicitlySet());
     }
 
     @Test
diff --git a/pom.xml b/pom.xml
index 6d71dfb..5a0c552 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
     <groupId>org.apache.freemarker.generator</groupId>
     <artifactId>freemarker-generator</artifactId>
     <packaging>pom</packaging>
-    <version>0.1.0-SNAPSHOT</version>
+    <version>0.2.0-SNAPSHOT</version>
     <name>Apache FreeMarker Generator</name>
     <url>https://freemarker-generator.apache.org/</url>