Repository: incubator-freemarker Updated Branches: refs/heads/3 18fd4e8f2 -> 164f292ef
Added `protected Xxx getImplictXxx()` methods to Configuration.ExtendableBuilder for Map and List settings. These allow adding values to these Map-s/List-s over those explictly set by the user who configure FreeMarker. The default implemention doesn't utilize these, except for sharedVariables to add the legacy directives like "compress" (though these will be eventually removed). Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/164f292e Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/164f292e Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/164f292e Branch: refs/heads/3 Commit: 164f292efe9d59e2dc818d544ec4388df064afc5 Parents: 18fd4e8 Author: ddekany <[email protected]> Authored: Sat Jun 10 09:54:56 2017 +0200 Committer: ddekany <[email protected]> Committed: Sat Jun 10 09:54:56 2017 +0200 ---------------------------------------------------------------------- .../freemarker/core/ConfigurationTest.java | 91 ++++++++++- .../apache/freemarker/core/Configuration.java | 150 ++++++++++++++++--- .../core/MutableProcessingConfiguration.java | 6 +- .../org/apache/freemarker/core/Template.java | 4 +- .../freemarker/core/TemplateConfiguration.java | 64 +------- .../freemarker/core/util/_CollectionUtil.java | 51 +++++++ 6 files changed, 281 insertions(+), 85 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java index fa1c4d2..7cf205f 100644 --- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ConfigurationTest.java @@ -55,6 +55,8 @@ import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat; import org.apache.freemarker.core.templateresolver.CacheStorageWithGetSize; import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory; import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher; +import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactoryException; import org.apache.freemarker.core.templateresolver.TemplateLookupContext; import org.apache.freemarker.core.templateresolver.TemplateLookupResult; import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy; @@ -1454,6 +1456,51 @@ public class ConfigurationTest extends TestCase { assertNotEquals(mutableValue, immutableValue); // No aliasing } + @Test + public void testImpliedSettingValues() throws IOException, TemplateConfigurationFactoryException { + Configuration cfg = new ImpliedSettingValuesTestBuilder().build(); + + assertEquals("Y,N", cfg.getTemplateConfigurations().get("t.yn", null).getBooleanFormat()); + assertNotNull(cfg.getCustomNumberFormat("hex")); + assertNotNull(cfg.getCustomDateFormat("epoch")); + assertEquals(ImmutableMap.of("lib", "lib.ftl"), cfg.getAutoImports()); + assertEquals(ImmutableList.of("inc.ftl"), cfg.getAutoIncludes()); + assertEquals(ImmutableMap.of("v", 1), cfg.getSharedVariables()); + } + + @Test + public void testImpliedSettingValues2() throws IOException, TemplateConfigurationFactoryException { + Configuration cfg = new ImpliedSettingValuesTestBuilder() + .templateConfigurations( + new ConditionalTemplateConfigurationFactory( + new FileNameGlobMatcher("*.jn"), + new TemplateConfiguration.Builder().booleanFormat("J,N").build() + ) + ) + .customNumberFormats(ImmutableMap.of("baseN", BaseNTemplateNumberFormatFactory.INSTANCE)) + .customDateFormats(ImmutableMap.of("epochDiv", EpochMillisDivTemplateDateFormatFactory.INSTANCE)) + .autoImports(ImmutableMap.of("lib2", "lib2.ftl")) + .autoIncludes(ImmutableList.of("inc2.ftl")) + .sharedVariables(ImmutableMap.of("v2", 2)) + .build(); + + TemplateConfigurationFactory tcf = cfg.getTemplateConfigurations(); + assertEquals("Y,N", tcf.get("t.yn", null).getBooleanFormat()); + assertEquals("J,N", tcf.get("t.jn", null).getBooleanFormat()); + + assertNotNull(cfg.getCustomNumberFormat("hex")); + assertNotNull(cfg.getCustomNumberFormat("baseN")); + + assertNotNull(cfg.getCustomDateFormat("epoch")); + assertNotNull(cfg.getCustomDateFormat("epochDiv")); + + assertEquals(ImmutableMap.of("lib", "lib.ftl", "lib2", "lib2.ftl"), cfg.getAutoImports()); + + assertEquals(ImmutableList.of("inc.ftl", "inc2.ftl"), cfg.getAutoIncludes()); + + assertEquals(ImmutableMap.of("v", 1, "v2", 2), cfg.getSharedVariables()); + } + @SuppressWarnings("boxing") private void assertStartsWith(List<String> list, List<String> headList) { int index = 0; @@ -1483,5 +1530,47 @@ public class ConfigurationTest extends TestCase { } } - + + private static class ImpliedSettingValuesTestBuilder + extends Configuration.ExtendableBuilder<ImpliedSettingValuesTestBuilder> { + + ImpliedSettingValuesTestBuilder() { + super(Configuration.VERSION_3_0_0); + } + + @Override + protected TemplateConfigurationFactory getImpliedTemplateConfigurations() { + return new ConditionalTemplateConfigurationFactory( + new FileNameGlobMatcher("*.yn"), + new TemplateConfiguration.Builder().booleanFormat("Y,N").build()); + } + + @Override + protected Map<String, TemplateNumberFormatFactory> getImpliedCustomNumberFormats() { + return ImmutableMap.<String, TemplateNumberFormatFactory>of( + "hex", HexTemplateNumberFormatFactory.INSTANCE); + } + + @Override + protected Map<String, TemplateDateFormatFactory> getImpliedCustomDateFormats() { + return ImmutableMap.<String, TemplateDateFormatFactory>of( + "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE); + } + + @Override + protected Map<String, String> getImpliedAutoImports() { + return ImmutableMap.of("lib", "lib.ftl"); + } + + @Override + protected List<String> getImpliedAutoIncludes() { + return ImmutableList.of("inc.ftl"); + } + + @Override + protected Map<String, Object> getImplicitSharedVariables() { + return ImmutableMap.<String, Object>of("v", 1); + } + } + } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java index e7e5947..93bd1ff 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java @@ -64,6 +64,7 @@ import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat; import org.apache.freemarker.core.templateresolver.CacheStorage; import org.apache.freemarker.core.templateresolver.GetTemplateResult; import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; +import org.apache.freemarker.core.templateresolver.MergingTemplateConfigurationFactory; import org.apache.freemarker.core.templateresolver.TemplateConfigurationFactory; import org.apache.freemarker.core.templateresolver.TemplateLoader; import org.apache.freemarker.core.templateresolver.TemplateLookupContext; @@ -372,26 +373,15 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc ObjectWrapper objectWrapper = builder.getObjectWrapper(); { - Map<String, Object> sharedVariables = builder.getSharedVariables(); + Map<String, Object> sharedVariables = _CollectionUtil.mergeImmutableMaps(builder + .getImplicitSharedVariables(), builder.getSharedVariables(), false); HashMap<String, TemplateModel> wrappedSharedVariables = new HashMap<>( - (sharedVariables.size() + 5 /* [FM3] 5 legacy vars */) * 4 / 3 + 1, 0.75f); - - // TODO [FM3] Get rid of this - wrappedSharedVariables.put("capture_output", new CaptureOutput()); - wrappedSharedVariables.put("compress", StandardCompress.INSTANCE); - wrappedSharedVariables.put("html_escape", new HtmlEscape()); - wrappedSharedVariables.put("normalize_newlines", new NormalizeNewlines()); - wrappedSharedVariables.put("xml_escape", new XmlEscape()); - - // In case the inherited sharedVariables aren't empty, we want to merge the two maps: - wrapAndPutSharedVariables(wrappedSharedVariables, builder.getDefaultSharedVariables(), - objectWrapper); - if (builder.isSharedVariablesSet()) { - wrapAndPutSharedVariables(wrappedSharedVariables, sharedVariables, objectWrapper); - } + sharedVariables.size() * 4 / 3 + 1, 0.75f); + wrapAndPutSharedVariables(wrappedSharedVariables, sharedVariables, objectWrapper); + this.wrappedSharedVariables = wrappedSharedVariables; - this.sharedVariables = Collections.unmodifiableMap(new LinkedHashMap<>(sharedVariables)); + this.sharedVariables = Collections.unmodifiableMap(sharedVariables); } // ParsingConfiguration settings: @@ -426,10 +416,14 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc showErrorTips = builder.getShowErrorTips(); apiBuiltinEnabled = builder.getAPIBuiltinEnabled(); logTemplateExceptions = builder.getLogTemplateExceptions(); - customDateFormats = builder.getCustomDateFormats(); - customNumberFormats = builder.getCustomNumberFormats(); - autoImports = builder.getAutoImports(); - autoIncludes = builder.getAutoIncludes(); + customDateFormats = _CollectionUtil.mergeImmutableMaps( + builder.getImpliedCustomDateFormats(), builder.getCustomDateFormats(), false); + customNumberFormats = _CollectionUtil.mergeImmutableMaps( + builder.getImpliedCustomNumberFormats(), builder.getCustomNumberFormats(), false); + autoImports = _CollectionUtil.mergeImmutableMaps( + builder.getImpliedAutoImports(), builder.getAutoImports(), true); + autoIncludes = _CollectionUtil.mergeImmutableLists( + builder.getImpliedAutoIncludes(), builder.getAutoIncludes(), true); lazyImports = builder.getLazyImports(); lazyAutoImports = builder.getLazyAutoImports(); customSettings = builder.getCustomSettings(false); @@ -474,11 +468,21 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc templateResolver, TEMPLATE_NAME_FORMAT_KEY, templateNameFormat); } - templateConfigurations = builder.getTemplateConfigurations(); + TemplateConfigurationFactory templateConfigurations = builder.getTemplateConfigurations(); if (!templateResolver.supportsTemplateConfigurationsSetting()) { checkSettingIsNullForThisTemplateResolver( templateResolver, TEMPLATE_CONFIGURATIONS_KEY, templateConfigurations); } + TemplateConfigurationFactory impliedTemplateConfigurations = builder.getImpliedTemplateConfigurations(); + if (impliedTemplateConfigurations != null) { + if (templateConfigurations != null) { + templateConfigurations = new MergingTemplateConfigurationFactory( + impliedTemplateConfigurations, templateConfigurations); + } else { + templateConfigurations = impliedTemplateConfigurations; + } + } + this.templateConfigurations = templateConfigurations; templateResolver.setDependencies(new TemplateResolverDependenciesImpl(this, templateResolver)); } @@ -2338,6 +2342,18 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc } /** + * The template configurations that will be added to the built {@link Configuration} before the ones + * coming from {@link #setCustomNumberFormats(Map)}}, where addition happens with + * {@link MergingTemplateConfigurationFactory}. When overriding this method, always + * consider adding to the return value of the super method, rather than replacing it. + * + * @return Maybe {@code null}. + */ + protected TemplateConfigurationFactory getImpliedTemplateConfigurations() { + return null; + } + + /** * Setter pair of {@link Configuration#getTemplateConfigurations()}. */ public void setTemplateConfigurations(TemplateConfigurationFactory templateConfigurations) { @@ -2515,10 +2531,38 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc return sharedVariables != null; } + /** + * The {@link Map} to use as shared variables if {@link #isSharedVariablesSet()} is {@code false}. + * + * @see #getImplicitSharedVariables() + */ protected Map<String, Object> getDefaultSharedVariables() { return Collections.emptyMap(); } + private static final Map<String, Object> DEFAULT_SHARED_VARIABLES; + static { + // TODO [FM3] Get rid of these + Map<String, Object> sharedVariables = new HashMap<>(); + sharedVariables.put("capture_output", new CaptureOutput()); + sharedVariables.put("compress", StandardCompress.INSTANCE); + sharedVariables.put("html_escape", new HtmlEscape()); + sharedVariables.put("normalize_newlines", new NormalizeNewlines()); + sharedVariables.put("xml_escape", new XmlEscape()); + DEFAULT_SHARED_VARIABLES = Collections.unmodifiableMap(sharedVariables); + } + + /** + * The shared variables that will be added to the built {@link Configuration} before the ones coming from + * {@link #getSharedVariables()}. When overriding this method, always consider adding to the return value + * of the super method, rather than replacing it. + * + * @return Immutable {@link Map}; not {@code null} + */ + protected Map<String, Object> getImplicitSharedVariables() { + return DEFAULT_SHARED_VARIABLES; + } + /** * Setter pair of {@link Configuration#getSharedVariables()}. * @@ -2669,11 +2713,27 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc return Collections.emptyMap(); } + /** + * {@inheritDoc} + * + * @see #getImpliedCustomNumberFormats() + */ @Override protected TemplateNumberFormatFactory getDefaultCustomNumberFormat(String name) { return null; } + /** + * The custom number formats that will be added to the built {@link Configuration} before the ones coming from + * {@link #getCustomNumberFormats()}. When overriding this method, always consider adding to the return + * value of the super method, rather than replacing it. + * + * @return Immutable {@link Map}; not {@code null} + */ + protected Map<String, TemplateNumberFormatFactory> getImpliedCustomNumberFormats() { + return Collections.emptyMap(); + } + @Override protected String getDefaultBooleanFormat() { return TemplateBooleanFormat.C_TRUE_FALSE; @@ -2694,11 +2754,27 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc return ""; } + /** + * {@inheritDoc} + * + * @see #getImpliedCustomDateFormats() + */ @Override protected Map<String, TemplateDateFormatFactory> getDefaultCustomDateFormats() { return Collections.emptyMap(); } + /** + * The custom date formats that will be added to the built {@link Configuration} before the ones coming from + * {@link #getCustomDateFormats()}. When overriding this method, always consider adding to the return value + * of the super method, rather than replacing it. + * + * @return Immutable {@link Map}; not {@code null} + */ + protected Map<String, TemplateDateFormatFactory> getImpliedCustomDateFormats() { + return Collections.emptyMap(); + } + @Override protected TemplateDateFormatFactory getDefaultCustomDateFormat(String name) { return null; @@ -2781,16 +2857,46 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc return null; } + /** + * {@inheritDoc} + * + * @see #getImpliedAutoImports() + */ @Override protected Map<String, String> getDefaultAutoImports() { return Collections.emptyMap(); } + /** + * The auto-imports that will be added to the built {@link Configuration} before the ones coming from + * {@link #getSharedVariables()}. When overriding this method, always consider adding to the return value + * of the super method, rather than replacing it. + */ + protected Map<String, String> getImpliedAutoImports() { + return Collections.emptyMap(); + } + + /** + * {@inheritDoc} + * + * @see #getImpliedAutoIncludes() + */ @Override protected List<String> getDefaultAutoIncludes() { return Collections.emptyList(); } + /** + * The imports that will be added to the built {@link Configuration} before the ones coming from + * {@link #getAutoIncludes()}. When overriding this method, always consider adding to the return + * value of the super method, rather than replacing it. + * + * @return Immutable {@link List}; not {@code null} + */ + protected List<String> getImpliedAutoIncludes() { + return Collections.emptyList(); + } + @Override protected Object getDefaultCustomSetting(Serializable key, Object defaultValue, boolean useDefaultValue) { if (useDefaultValue) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java index 75eac65..98d927f 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java @@ -555,7 +555,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces /** * Fluent API equivalent of {@link #setCustomNumberFormats(Map)} */ - public SelfT customNumberFormats(Map<String, TemplateNumberFormatFactory> value) { + public SelfT customNumberFormats(Map<String, ? extends TemplateNumberFormatFactory> value) { setCustomNumberFormats(value); return self(); } @@ -619,7 +619,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces int commaIdx = booleanFormat.indexOf(','); if (commaIdx == -1) { throw new IllegalArgumentException( - "Setting value must be string that contains two comma-separated values for true and false, " + + "Setting value must be a string that contains two comma-separated values for true and false, " + "respectively."); } @@ -826,7 +826,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces /** * Fluent API equivalent of {@link #setCustomDateFormats(Map)} */ - public SelfT customDateFormats(Map<String, TemplateDateFormatFactory> value) { + public SelfT customDateFormats(Map<String, ? extends TemplateDateFormatFactory> value) { setCustomDateFormats(value); return self(); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java index 3a0122d..8113eb8 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java @@ -661,7 +661,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope { return customLookupCondition; } - // TODO [FM3] Should not be public, should be final field + // TODO [FM3] Should be gone; see comment above the lookupLocale field /** * Mostly only used internally; setter pair of {@link #getCustomLookupCondition()}. This meant to be called directly * after instantiating the template with its constructor, after a successfull lookup that used this condition. So @@ -803,7 +803,7 @@ public class Template implements ProcessingConfiguration, CustomStateScope { return tCfg != null && tCfg.isLocaleSet() ? tCfg.getLocale() : cfg.getLocale(); } - // TODO [FM3] Temporary hack; See comment above the locale field + // TODO [FM3] Temporary hack; See comment above the lookupLocale field public void setLookupLocale(Locale lookupLocale) { this.lookupLocale = lookupLocale; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java index 4aaa7c2..284ec6b 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java @@ -21,20 +21,16 @@ package org.apache.freemarker.core; import java.io.Reader; import java.io.Serializable; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.TimeZone; import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.model.ObjectWrapper; import org.apache.freemarker.core.outputformat.OutputFormat; import org.apache.freemarker.core.util.CommonBuilder; +import org.apache.freemarker.core.util._CollectionUtil; import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; @@ -139,52 +135,6 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur tabSize = builder.isTabSizeSet() ? builder.getTabSize() : null; } - /** - * Adds two {@link Map}-s (keeping the iteration order); assuming the inputs are already unmodifiable and - * unchanging, it returns an unmodifiable and unchanging {@link Map} itself. - */ - private static <K,V> Map<K,V> mergeMaps(Map<K,V> m1, Map<K,V> m2, boolean overwriteUpdatesOrder) { - if (m1 == null) return m2; - if (m2 == null) return m1; - if (m1.isEmpty()) return m2; - if (m2.isEmpty()) return m1; - - LinkedHashMap<K, V> mergedM = new LinkedHashMap<>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f); - mergedM.putAll(m1); - if (overwriteUpdatesOrder) { - for (K m2Key : m2.keySet()) { - mergedM.remove(m2Key); // So that duplicate keys are moved after m1 keys - } - } - mergedM.putAll(m2); - return Collections.unmodifiableMap(mergedM); - } - - /** - * Adds two {@link List}-s; assuming the inputs are already unmodifiable and unchanging, it returns an - * unmodifiable and unchanging {@link List} itself. - */ - private static List<String> mergeLists(List<String> list1, List<String> list2, boolean skipDuplicatesInList1) { - if (list1 == null) return list2; - if (list2 == null) return list1; - if (list1.isEmpty()) return list2; - if (list2.isEmpty()) return list1; - - ArrayList<String> mergedList = new ArrayList<>(list1.size() + list2.size()); - if (skipDuplicatesInList1) { - Set<String> list2Set = new HashSet<>(list2); - for (String it : list1) { - if (!list2Set.contains(it)) { - mergedList.add(it); - } - } - } else { - mergedList.addAll(list1); - } - mergedList.addAll(list2); - return Collections.unmodifiableList(mergedList); - } - @Override public TagSyntax getTagSyntax() { if (!isTagSyntaxSet()) { @@ -858,13 +808,13 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur setBooleanFormat(tc.getBooleanFormat()); } if (tc.isCustomDateFormatsSet()) { - setCustomDateFormats(mergeMaps( + setCustomDateFormats(_CollectionUtil.mergeImmutableMaps( isCustomDateFormatsSet() ? getCustomDateFormats() : null, tc.getCustomDateFormats(), false), true ); } if (tc.isCustomNumberFormatsSet()) { - setCustomNumberFormats(mergeMaps( + setCustomNumberFormats(_CollectionUtil.mergeImmutableMaps( isCustomNumberFormatsSet() ? getCustomNumberFormats() : null, tc.getCustomNumberFormats(), false), true); } @@ -941,24 +891,24 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur setLazyAutoImports(tc.getLazyAutoImports()); } if (tc.isAutoImportsSet()) { - setAutoImports(mergeMaps( + setAutoImports(_CollectionUtil.mergeImmutableMaps( isAutoImportsSet() ? getAutoImports() : null, tc.isAutoImportsSet() ? tc.getAutoImports() : null, true), true); } if (tc.isAutoIncludesSet()) { - setAutoIncludes(mergeLists( + setAutoIncludes(_CollectionUtil.mergeImmutableLists( isAutoIncludesSet() ? getAutoIncludes() : null, tc.isAutoIncludesSet() ? tc.getAutoIncludes() : null, true), true); } - setCustomSettingsMap(mergeMaps( + setCustomSettingsMap(_CollectionUtil.mergeImmutableMaps( getCustomSettings(false), tc.getCustomSettings(false), - true)); + false)); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/164f292e/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java index 1f91821..d5a6574 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_CollectionUtil.java @@ -23,8 +23,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Don't use this; used internally by FreeMarker, might changes without notice. @@ -144,4 +147,52 @@ public class _CollectionUtil { return isMapKnownToBeUnmodifiable(map) ? map : Collections.unmodifiableMap(map); } + /** + * Adds two {@link Map}-s (keeping the iteration order); assuming the inputs are already unmodifiable and + * unchanging, it returns an unmodifiable and unchanging {@link Map} itself. + */ + public static <K,V> Map<K,V> mergeImmutableMaps(Map<K,V> m1, Map<K,V> m2, boolean keepOriginalOrder) { + if (m1 == null) return m2; + if (m2 == null) return m1; + if (m1.isEmpty()) return m2; + if (m2.isEmpty()) return m1; + + Map<K, V> mergedM = keepOriginalOrder + ? new LinkedHashMap<K, V>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f) + : new HashMap<K, V>((m1.size() + m2.size()) * 4 / 3 + 1, 0.75f); + mergedM.putAll(m1); + if (keepOriginalOrder) { + for (K m2Key : m2.keySet()) { + mergedM.remove(m2Key); // So that duplicate keys are moved after m1 keys + } + } + mergedM.putAll(m2); + return Collections.unmodifiableMap(mergedM); + } + + /** + * Adds two {@link List}-s; assuming the inputs are already unmodifiable and unchanging, it returns an + * unmodifiable and unchanging {@link List} itself. + */ + public static List<String> mergeImmutableLists(List<String> list1, List<String> list2, + boolean skipDuplicatesInList1) { + if (list1 == null) return list2; + if (list2 == null) return list1; + if (list1.isEmpty()) return list2; + if (list2.isEmpty()) return list1; + + ArrayList<String> mergedList = new ArrayList<>(list1.size() + list2.size()); + if (skipDuplicatesInList1) { + Set<String> list2Set = new HashSet<>(list2); + for (String it : list1) { + if (!list2Set.contains(it)) { + mergedList.add(it); + } + } + } else { + mergedList.addAll(list1); + } + mergedList.addAll(list2); + return Collections.unmodifiableList(mergedList); + } }
