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 <[email protected]> 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>
