Continued work on TemplateLanguage configuration and new file extensions: Renamed...
[freemarker.git] / freemarker-core-test / src / test / java / org / apache / freemarker / core / TemplateConfigurationTest.java
1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.freemarker.core;
20
21 import static org.apache.freemarker.core.ProcessingConfiguration.*;
22 import static org.junit.Assert.*;
23
24 import java.beans.BeanInfo;
25 import java.beans.IntrospectionException;
26 import java.beans.Introspector;
27 import java.beans.PropertyDescriptor;
28 import java.io.IOException;
29 import java.io.StringReader;
30 import java.io.StringWriter;
31 import java.lang.reflect.InvocationTargetException;
32 import java.lang.reflect.Method;
33 import java.nio.charset.Charset;
34 import java.nio.charset.StandardCharsets;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.TimeZone;
45
46 import org.apache.commons.collections.ListUtils;
47 import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
48 import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine;
49 import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
50 import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
51 import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
52 import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
53 import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
54 import org.apache.freemarker.core.templateresolver.FileExtensionMatcher;
55 import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
56 import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
57 import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory;
58 import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactory;
59 import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
60 import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory;
61 import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
62 import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory;
63 import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
64 import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
65 import org.apache.freemarker.test.MonitoredTemplateLoader;
66 import org.apache.freemarker.test.TestConfigurationBuilder;
67 import org.junit.Test;
68
69 import com.google.common.collect.ImmutableList;
70 import com.google.common.collect.ImmutableMap;
71
72 @SuppressWarnings("boxing")
73 public class TemplateConfigurationTest {
74
75 private static final Charset ISO_8859_2 = Charset.forName("ISO-8859-2");
76
77 private final class DummyArithmeticEngine extends ArithmeticEngine {
78
79 @Override
80 public int compareNumbers(Number first, Number second) throws TemplateException {
81 return 0;
82 }
83
84 @Override
85 public Number add(Number first, Number second) throws TemplateException {
86 return 22;
87 }
88
89 @Override
90 public Number subtract(Number first, Number second) throws TemplateException {
91 return null;
92 }
93
94 @Override
95 public Number multiply(Number first, Number second) throws TemplateException {
96 return 33;
97 }
98
99 @Override
100 public Number divide(Number first, Number second) throws TemplateException {
101 return null;
102 }
103
104 @Override
105 public Number modulus(Number first, Number second) throws TemplateException {
106 return null;
107 }
108
109 @Override
110 public Number toNumber(String s) {
111 return 11;
112 }
113 }
114
115 private static final Configuration DEFAULT_CFG;
116 static {
117 TestConfigurationBuilder cfgB = new TestConfigurationBuilder();
118 StringTemplateLoader stl = new StringTemplateLoader();
119 stl.putTemplate("t1.f3ah", "<#global loaded = (loaded!) + 't1;'>In t1;");
120 stl.putTemplate("t2.f3ah", "<#global loaded = (loaded!) + 't2;'>In t2;");
121 stl.putTemplate("t3.f3ah", "<#global loaded = (loaded!) + 't3;'>In t3;");
122 try {
123 DEFAULT_CFG = cfgB.templateLoader(stl).build();
124 } catch (ConfigurationException e) {
125 throw new IllegalStateException("Faild to create default configuration", e);
126 }
127 }
128
129 private static final TimeZone NON_DEFAULT_TZ;
130 static {
131 TimeZone defaultTZ = DEFAULT_CFG.getTimeZone();
132 TimeZone tz = TimeZone.getTimeZone("UTC");
133 if (tz.equals(defaultTZ)) {
134 tz = TimeZone.getTimeZone("GMT+01");
135 if (tz.equals(defaultTZ)) {
136 throw new AssertionError("Couldn't chose a non-default time zone");
137 }
138 }
139 NON_DEFAULT_TZ = tz;
140 }
141
142 private static final TimeZone NON_DEFAULT_SQL_TZ;
143 static {
144 TimeZone defaultTZ = DEFAULT_CFG.getSQLDateAndTimeTimeZone();
145 TimeZone tz = TimeZone.getTimeZone("UTC");
146 if (tz.equals(defaultTZ)) {
147 tz = TimeZone.getTimeZone("GMT+01");
148 if (tz.equals(defaultTZ)) {
149 throw new AssertionError("Couldn't chose a non-default SQL time zone");
150 }
151 }
152 NON_DEFAULT_SQL_TZ = tz;
153 }
154
155 private static final Locale NON_DEFAULT_LOCALE =
156 DEFAULT_CFG.getLocale().equals(Locale.US) ? Locale.GERMAN : Locale.US;
157
158 private static final Charset NON_DEFAULT_ENCODING =
159 DEFAULT_CFG.getSourceEncoding().equals(StandardCharsets.UTF_8) ? StandardCharsets.UTF_16LE
160 : StandardCharsets.UTF_8;
161
162 private static final Map<String, Object> SETTING_ASSIGNMENTS;
163
164 static {
165 SETTING_ASSIGNMENTS = new HashMap<>();
166
167 // "MutableProcessingConfiguration" settings:
168 SETTING_ASSIGNMENTS.put("APIBuiltinEnabled", true);
169 SETTING_ASSIGNMENTS.put("SQLDateAndTimeTimeZone", NON_DEFAULT_SQL_TZ);
170 SETTING_ASSIGNMENTS.put("URLEscapingCharset", StandardCharsets.UTF_16);
171 SETTING_ASSIGNMENTS.put("autoFlush", false);
172 SETTING_ASSIGNMENTS.put("booleanFormat", "J,N");
173 SETTING_ASSIGNMENTS.put("dateFormat", "yyyy-#DDD");
174 SETTING_ASSIGNMENTS.put("dateTimeFormat", "yyyy-#DDD-@HH:mm");
175 SETTING_ASSIGNMENTS.put("locale", NON_DEFAULT_LOCALE);
176 SETTING_ASSIGNMENTS.put("newBuiltinClassResolver", TemplateClassResolver.ALLOW_NOTHING);
177 SETTING_ASSIGNMENTS.put("numberFormat", "0.0000");
178 SETTING_ASSIGNMENTS.put("objectWrapper",
179 new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
180 SETTING_ASSIGNMENTS.put("outputEncoding", StandardCharsets.UTF_16);
181 SETTING_ASSIGNMENTS.put("showErrorTips", false);
182 SETTING_ASSIGNMENTS.put("templateExceptionHandler", TemplateExceptionHandler.IGNORE);
183 SETTING_ASSIGNMENTS.put("attemptExceptionReporter", AttemptExceptionReporter.LOG_WARN);
184 SETTING_ASSIGNMENTS.put("timeFormat", "@HH:mm");
185 SETTING_ASSIGNMENTS.put("timeZone", NON_DEFAULT_TZ);
186 SETTING_ASSIGNMENTS.put("arithmeticEngine", ConservativeArithmeticEngine.INSTANCE);
187 SETTING_ASSIGNMENTS.put("customNumberFormats",
188 ImmutableMap.of("dummy", HexTemplateNumberFormatFactory.INSTANCE));
189 SETTING_ASSIGNMENTS.put("customDateFormats",
190 ImmutableMap.of("dummy", EpochMillisTemplateDateFormatFactory.INSTANCE));
191
192 // Parser-only settings:
193 SETTING_ASSIGNMENTS.put("templateLanguage", UnparsedTemplateLanguage.INSTANCE);
194 SETTING_ASSIGNMENTS.put("tagSyntax", TagSyntax.SQUARE_BRACKET);
195 SETTING_ASSIGNMENTS.put("interpolationSyntax", InterpolationSyntax.SQUARE_BRACKET);
196 SETTING_ASSIGNMENTS.put("whitespaceStripping", false);
197 SETTING_ASSIGNMENTS.put("strictSyntaxMode", false);
198 SETTING_ASSIGNMENTS.put("autoEscapingPolicy", AutoEscapingPolicy.DISABLE);
199 SETTING_ASSIGNMENTS.put("outputFormat", HTMLOutputFormat.INSTANCE);
200 SETTING_ASSIGNMENTS.put("recognizeStandardFileExtensions", false);
201 SETTING_ASSIGNMENTS.put("tabSize", 1);
202 SETTING_ASSIGNMENTS.put("lazyImports", Boolean.TRUE);
203 SETTING_ASSIGNMENTS.put("lazyAutoImports", Boolean.FALSE);
204 SETTING_ASSIGNMENTS.put("autoImports", ImmutableMap.of("a", "/lib/a.f3ah"));
205 SETTING_ASSIGNMENTS.put("autoIncludes", ImmutableList.of("/lib/b.f3ah"));
206
207 // Special settings:
208 SETTING_ASSIGNMENTS.put("sourceEncoding", NON_DEFAULT_ENCODING);
209 }
210
211 public static String getIsSetMethodName(String readMethodName) {
212 return (readMethodName.startsWith("get") ? "is" + readMethodName.substring(3)
213 : readMethodName)
214 + "Set";
215 }
216
217 public static List<PropertyDescriptor> getTemplateConfigurationSettingPropDescs(
218 Class<? extends ProcessingConfiguration> confClass, boolean includeCompilerSettings)
219 throws IntrospectionException {
220 List<PropertyDescriptor> settingPropDescs = new ArrayList<>();
221
222 BeanInfo beanInfo = Introspector.getBeanInfo(confClass);
223 for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
224 String name = pd.getName();
225 if (pd.getWriteMethod() != null && !IGNORED_PROP_NAMES.contains(name)
226 && (includeCompilerSettings
227 || (CONFIGURABLE_PROP_NAMES.contains(name) || !PARSER_PROP_NAMES.contains(name)))) {
228 if (pd.getReadMethod() == null) {
229 throw new AssertionError("Property has no read method: " + pd);
230 }
231 settingPropDescs.add(pd);
232 }
233 }
234
235 Collections.sort(settingPropDescs, new Comparator<PropertyDescriptor>() {
236 @Override
237 public int compare(PropertyDescriptor o1, PropertyDescriptor o2) {
238 return o1.getName().compareToIgnoreCase(o2.getName());
239 }
240 });
241
242 return settingPropDescs;
243 }
244
245 private static final Set<String> IGNORED_PROP_NAMES;
246
247 static {
248 IGNORED_PROP_NAMES = new HashSet();
249 IGNORED_PROP_NAMES.add("class");
250 IGNORED_PROP_NAMES.add("strictBeanModels");
251 IGNORED_PROP_NAMES.add("parentConfiguration");
252 IGNORED_PROP_NAMES.add("settings");
253 IGNORED_PROP_NAMES.add("customSettings");
254 }
255
256 private static final Set<String> CONFIGURABLE_PROP_NAMES;
257 static {
258 CONFIGURABLE_PROP_NAMES = new HashSet<>();
259 try {
260 for (PropertyDescriptor propDesc : Introspector.getBeanInfo(MutableProcessingConfiguration.class).getPropertyDescriptors()) {
261 String propName = propDesc.getName();
262 if (!IGNORED_PROP_NAMES.contains(propName)) {
263 CONFIGURABLE_PROP_NAMES.add(propName);
264 }
265 }
266 } catch (IntrospectionException e) {
267 throw new IllegalStateException("Failed to init static field", e);
268 }
269 }
270
271 private static final Set<String> PARSER_PROP_NAMES;
272 static {
273 PARSER_PROP_NAMES = new HashSet<>();
274 // It's an interface; can't use standard Inrospector
275 for (Method m : ParsingConfiguration.class.getMethods()) {
276 String propertyName;
277 String name = m.getName();
278 if (name.startsWith("get")) {
279 propertyName = name.substring(3);
280 } else if (name.startsWith("is") && !name.endsWith("Set")) {
281 propertyName = name.substring(2);
282 } else {
283 propertyName = null;
284 }
285 if (propertyName != null) {
286 if (!Character.isUpperCase(propertyName.charAt(1))) {
287 propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1);
288 }
289 PARSER_PROP_NAMES.add(propertyName);
290 }
291 }
292 }
293
294 private static final Integer CA1 = Integer.valueOf(123);
295 private static final String CA2 = "ca2";
296 private static final String CA3 = "ca3";
297 private static final String CA4 = "ca4";
298
299 @Test
300 public void testMergeBasicFunctionality() throws Exception {
301 for (PropertyDescriptor propDesc1 : getTemplateConfigurationSettingPropDescs(
302 TemplateConfiguration.Builder.class, true)) {
303 for (PropertyDescriptor propDesc2 : getTemplateConfigurationSettingPropDescs(
304 TemplateConfiguration.Builder.class, true)) {
305 TemplateConfiguration.Builder tcb1 = new TemplateConfiguration.Builder();
306 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
307
308 Object value1 = SETTING_ASSIGNMENTS.get(propDesc1.getName());
309 propDesc1.getWriteMethod().invoke(tcb1, value1);
310 Object value2 = SETTING_ASSIGNMENTS.get(propDesc2.getName());
311 propDesc2.getWriteMethod().invoke(tcb2, value2);
312
313 tcb1.merge(tcb2.build());
314 if (propDesc1.getName().equals(propDesc2.getName()) && value1 instanceof List
315 && !propDesc1.getName().equals("autoIncludes")) {
316 assertEquals("For " + propDesc1.getName(),
317 ListUtils.union((List) value1, (List) value1), propDesc1.getReadMethod().invoke(tcb1));
318 } else { // Values of the same setting merged
319 assertEquals("For " + propDesc1.getName(), value1, propDesc1.getReadMethod().invoke(tcb1));
320 assertEquals("For " + propDesc2.getName(), value2, propDesc2.getReadMethod().invoke(tcb1));
321 }
322 }
323 }
324 }
325
326 @Test
327 public void testMergeMapSettings() throws Exception {
328 TemplateConfiguration.Builder tcb1 = new TemplateConfiguration.Builder();
329 tcb1.setCustomDateFormats(ImmutableMap.of(
330 "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE,
331 "x", LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE));
332 tcb1.setCustomNumberFormats(ImmutableMap.of(
333 "hex", HexTemplateNumberFormatFactory.INSTANCE,
334 "x", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE));
335 tcb1.setAutoImports(ImmutableMap.of("a", "a1.f3ah", "b", "b1.f3ah"));
336
337 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
338 tcb2.setCustomDateFormats(ImmutableMap.of(
339 "loc", LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE,
340 "x", EpochMillisDivTemplateDateFormatFactory.INSTANCE));
341 tcb2.setCustomNumberFormats(ImmutableMap.of(
342 "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
343 "x", BaseNTemplateNumberFormatFactory.INSTANCE));
344 tcb2.setAutoImports(ImmutableMap.of("b", "b2.f3ah", "c", "c2.f3ah"));
345
346 tcb1.merge(tcb2.build());
347
348 Map<String, ? extends TemplateDateFormatFactory> mergedCustomDateFormats = tcb1.getCustomDateFormats();
349 assertEquals(EpochMillisTemplateDateFormatFactory.INSTANCE, mergedCustomDateFormats.get("epoch"));
350 assertEquals(LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE, mergedCustomDateFormats.get("loc"));
351 assertEquals(EpochMillisDivTemplateDateFormatFactory.INSTANCE, mergedCustomDateFormats.get("x"));
352
353 Map<String, ? extends TemplateNumberFormatFactory> mergedCustomNumberFormats = tcb1.getCustomNumberFormats();
354 assertEquals(HexTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("hex"));
355 assertEquals(LocaleSensitiveTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("loc"));
356 assertEquals(BaseNTemplateNumberFormatFactory.INSTANCE, mergedCustomNumberFormats.get("x"));
357
358 Map<String, String> mergedAutoImports = tcb1.getAutoImports();
359 assertEquals("a1.f3ah", mergedAutoImports.get("a"));
360 assertEquals("b2.f3ah", mergedAutoImports.get("b"));
361 assertEquals("c2.f3ah", mergedAutoImports.get("c"));
362
363 // Empty map merging optimization:
364 tcb1.merge(new TemplateConfiguration.Builder().build());
365 assertSame(mergedCustomDateFormats, tcb1.getCustomDateFormats());
366 assertSame(mergedCustomNumberFormats, tcb1.getCustomNumberFormats());
367
368 // Empty map merging optimization:
369 TemplateConfiguration.Builder tcb3 = new TemplateConfiguration.Builder();
370 tcb3.merge(tcb1.build());
371 assertSame(mergedCustomDateFormats, tcb3.getCustomDateFormats());
372 assertSame(mergedCustomNumberFormats, tcb3.getCustomNumberFormats());
373 }
374
375 @Test
376 public void testMergeListSettings() throws Exception {
377 TemplateConfiguration.Builder tcb1 = new TemplateConfiguration.Builder();
378 tcb1.setAutoIncludes(ImmutableList.of("a.f3ah", "x.f3ah", "b.f3ah"));
379
380 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
381 tcb2.setAutoIncludes(ImmutableList.of("c.f3ah", "x.f3ah", "d.f3ah"));
382
383 tcb1.merge(tcb2.build());
384
385 assertEquals(ImmutableList.of("a.f3ah", "b.f3ah", "c.f3ah", "x.f3ah", "d.f3ah"), tcb1.getAutoIncludes());
386 }
387
388 @Test
389 public void testMergePriority() throws Exception {
390 TemplateConfiguration.Builder tcb1 = new TemplateConfiguration.Builder();
391 tcb1.setDateFormat("1");
392 tcb1.setTimeFormat("1");
393 tcb1.setDateTimeFormat("1");
394
395 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
396 tcb2.setDateFormat("2");
397 tcb2.setTimeFormat("2");
398
399 TemplateConfiguration.Builder tcb3 = new TemplateConfiguration.Builder();
400 tcb3.setDateFormat("3");
401
402 tcb1.merge(tcb2.build());
403 tcb1.merge(tcb3.build());
404
405 assertEquals("3", tcb1.getDateFormat());
406 assertEquals("2", tcb1.getTimeFormat());
407 assertEquals("1", tcb1.getDateTimeFormat());
408 }
409
410 @Test
411 public void testMergeCustomSettings() throws Exception {
412 TemplateConfiguration.Builder tc1 = new TemplateConfiguration.Builder();
413 tc1.setCustomSetting("k1", "v1");
414 tc1.setCustomSetting("k2", "v1");
415 tc1.setCustomSetting("k3", "v1");
416 tc1.setCustomSetting(CA1, "V1");
417 tc1.setCustomSetting(CA2, "V1");
418 tc1.setCustomSetting(CA3, "V1");
419
420 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
421 tcb2.setCustomSetting("k1", "v2");
422 tcb2.setCustomSetting("k2", "v2");
423 tcb2.setCustomSetting(CA1, "V2");
424 tcb2.setCustomSetting(CA2, "V2");
425
426 TemplateConfiguration.Builder tcb3 = new TemplateConfiguration.Builder();
427 tcb3.setCustomSetting("k1", "v3");
428 tcb3.setCustomSetting(CA1, "V3");
429
430 tc1.merge(tcb2.build());
431 tc1.merge(tcb3.build());
432
433 assertEquals("v3", tc1.getCustomSetting("k1"));
434 assertEquals("v2", tc1.getCustomSetting("k2"));
435 assertEquals("v1", tc1.getCustomSetting("k3"));
436 assertEquals("V3", tc1.getCustomSetting(CA1));
437 assertEquals("V2", tc1.getCustomSetting(CA2));
438 assertEquals("V1", tc1.getCustomSetting(CA3));
439 }
440
441 @Test
442 public void testMergeNullCustomSettings() throws Exception {
443 TemplateConfiguration.Builder tcb1 = new TemplateConfiguration.Builder();
444 tcb1.setCustomSetting("k1", "v1");
445 tcb1.setCustomSetting("k2", "v1");
446 tcb1.setCustomSetting(CA1, "V1");
447 tcb1.setCustomSetting(CA2,"V1");
448
449 assertEquals("v1", tcb1.getCustomSetting("k1"));
450 assertEquals("v1", tcb1.getCustomSetting("k2"));
451 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting("k3", MISSING_VALUE_MARKER));
452 assertEquals("V1", tcb1.getCustomSetting(CA1));
453 assertEquals("V1", tcb1.getCustomSetting(CA2));
454 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting(CA3, MISSING_VALUE_MARKER));
455
456 TemplateConfiguration.Builder tcb2 = new TemplateConfiguration.Builder();
457 tcb2.setCustomSetting("k1", "v2");
458 tcb2.setCustomSetting("k2", null);
459 tcb2.setCustomSetting(CA1, "V2");
460 tcb2.setCustomSetting(CA2, null);
461
462 TemplateConfiguration.Builder tcb3 = new TemplateConfiguration.Builder();
463 tcb3.setCustomSetting("k1", null);
464 tcb2.setCustomSetting(CA1, null);
465
466 tcb1.merge(tcb2.build());
467 tcb1.merge(tcb3.build());
468
469 assertNull(tcb1.getCustomSetting("k1"));
470 assertNull(tcb1.getCustomSetting("k2"));
471 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting("k3", MISSING_VALUE_MARKER));
472 assertNull(tcb1.getCustomSetting(CA1));
473 assertNull(tcb1.getCustomSetting(CA2));
474 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting(CA3, MISSING_VALUE_MARKER));
475
476 TemplateConfiguration.Builder tcb4 = new TemplateConfiguration.Builder();
477 tcb4.setCustomSetting("k1", "v4");
478 tcb4.setCustomSetting(CA1, "V4");
479
480 tcb1.merge(tcb4.build());
481
482 assertEquals("v4", tcb1.getCustomSetting("k1"));
483 assertNull(tcb1.getCustomSetting("k2"));
484 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting("k3", MISSING_VALUE_MARKER));
485 assertEquals("V4", tcb1.getCustomSetting(CA1));
486 assertNull(tcb1.getCustomSetting(CA2));
487 assertEquals(MISSING_VALUE_MARKER, tcb1.getCustomSetting(CA3, MISSING_VALUE_MARKER));
488 }
489
490 @Test
491 public void testConfigureNonParserConfig() throws Exception {
492 for (PropertyDescriptor pd : getTemplateConfigurationSettingPropDescs(
493 TemplateConfiguration.Builder.class, false)) {
494 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
495
496 Object newValue = SETTING_ASSIGNMENTS.get(pd.getName());
497 pd.getWriteMethod().invoke(tcb, newValue);
498
499 TemplateConfiguration tc = tcb.build();
500
501 Method tReaderMethod = Template.class.getMethod(pd.getReadMethod().getName());
502
503 // Without TC
504 assertNotEquals("For \"" + pd.getName() + "\"",
505 tReaderMethod.invoke(new Template(null, "", DEFAULT_CFG)));
506 // With TC
507 assertEquals("For \"" + pd.getName() + "\"", newValue,
508 tReaderMethod.invoke(new Template(null, "", DEFAULT_CFG, tc)));
509 }
510 }
511
512 @Test
513 public void testConfigureCustomSettings() throws Exception {
514 Configuration cfg = new TestConfigurationBuilder()
515 .customSetting("k1", "c")
516 .customSetting("k2", "c")
517 .customSetting("k3", "c")
518 .customSetting("k8", "c")
519 .build();
520
521 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
522 tcb.setCustomSetting("k2", "tc");
523 tcb.setCustomSetting("k3", null);
524 tcb.setCustomSetting("k4", "tc");
525 tcb.setCustomSetting("k5", "tc");
526 tcb.setCustomSetting("k6", "tc");
527
528 TemplateConfiguration tc = tcb.build();
529 Template t = new Template(null, "<#ftl customSettings={'k5':'t', 'k7':'t', 'k8':'t'}>", cfg, tc);
530
531 assertEquals("c", t.getCustomSetting("k1"));
532 assertEquals("tc", t.getCustomSetting("k2"));
533 assertNull(t.getCustomSetting("k3"));
534 assertEquals("tc", t.getCustomSetting("k4"));
535 assertEquals("t", t.getCustomSetting("k5"));
536 // TODO [FM3] when { ... 'k6': null ... } works in FTL, put this back.
537 // assertNull(t.getCustomSetting("k6"));
538 assertEquals("t", t.getCustomSetting("k7"));
539 assertEquals("t", t.getCustomSetting("k8"));
540 }
541
542 @Test
543 public void testConfigureParser() throws Exception {
544 Set<String> testedProps = new HashSet<>();
545
546 {
547 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
548 tcb.setWhitespaceStripping(false);
549 TemplateConfiguration tc = tcb.build();
550 assertOutputWithoutAndWithTC(tc, "<#if true>\nx\n</#if>\n", "x\n", "\nx\n\n");
551 testedProps.add(Configuration.ExtendableBuilder.WHITESPACE_STRIPPING_KEY);
552 }
553
554 {
555 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
556 tcb.setArithmeticEngine(new DummyArithmeticEngine());
557 TemplateConfiguration tc = tcb.build();
558 assertOutputWithoutAndWithTC(tc, "${1} ${1+1}", "1 2", "11 22");
559 testedProps.add(Configuration.ExtendableBuilder.ARITHMETIC_ENGINE_KEY);
560 }
561
562 {
563 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
564 tcb.setOutputFormat(XMLOutputFormat.INSTANCE);
565 TemplateConfiguration tc = tcb.build();
566 assertOutputWithoutAndWithTC(tc, "${.outputFormat} ${\"a'b\"}",
567 UndefinedOutputFormat.INSTANCE.getName() + " a'b",
568 XMLOutputFormat.INSTANCE.getName() + " a&apos;b");
569 testedProps.add(Configuration.ExtendableBuilder.OUTPUT_FORMAT_KEY);
570 }
571
572 {
573 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
574 tcb.setOutputFormat(XMLOutputFormat.INSTANCE);
575 tcb.setAutoEscapingPolicy(AutoEscapingPolicy.DISABLE);
576 TemplateConfiguration tc = tcb.build();
577 assertOutputWithoutAndWithTC(tc, "${'a&b'}", "a&b", "a&b");
578 testedProps.add(Configuration.ExtendableBuilder.AUTO_ESCAPING_POLICY_KEY);
579 }
580
581 {
582 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
583 /* Can't test this now, as the only valid value is 3.0.0. [FM3.0.1]
584 TemplateConfiguration tc = tcb.build();
585 tc.setParentConfiguration(new Configuration(new Version(2, 3, 0)));
586 assertOutputWithoutAndWithTC(tc, "<#foo>", null, "<#foo>");
587 */
588 testedProps.add(Configuration.ExtendableBuilder.INCOMPATIBLE_IMPROVEMENTS_KEY);
589 }
590
591 {
592 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
593 tcb.setRecognizeStandardFileExtensions(false);
594 TemplateConfiguration tc = tcb.build();
595 assertOutputWithoutAndWithTC(tc, "adhoc.f3ah", "${.outputFormat}",
596 HTMLOutputFormat.INSTANCE.getName(), UndefinedOutputFormat.INSTANCE.getName());
597 testedProps.add(Configuration.ExtendableBuilder.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY);
598 }
599
600 {
601 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
602 tcb.setTabSize(3);
603 TemplateConfiguration tc = tcb.build();
604 assertOutputWithoutAndWithTC(tc,
605 "<#attempt><@'\\t$\\{1+}'?interpret/><#recover>"
606 + "${.error?replace('(?s).*?column ([0-9]+).*', '$1', 'r')}"
607 + "</#attempt>",
608 "13", "8");
609 testedProps.add(Configuration.ExtendableBuilder.TAB_SIZE_KEY);
610 }
611
612 {
613 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
614 tcb.setTemplateLanguage(DefaultTemplateLanguage.F3SH);
615 TemplateConfiguration tc = tcb.build();
616 assertOutputWithoutAndWithTC(tc, "[#if true]y[/#if]${1}[=2]", "[#if true]y[/#if]1[=2]", "y${1}2");
617 testedProps.add(Configuration.ExtendableBuilder.TEMPLATE_LANGUAGE_KEY);
618 }
619 {
620 // TemplateResolver-based TemplateLanguage selection, we can't use
621 // assertOutput here, as that hard-coded to create an FTL Template.
622
623 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
624 tcb.setTemplateLanguage(UnparsedTemplateLanguage.INSTANCE);
625
626 TestConfigurationBuilder cfgB = new TestConfigurationBuilder();
627 cfgB.setTemplateConfigurations(
628 new ConditionalTemplateConfigurationFactory(new FileExtensionMatcher("txt"), tcb.build()));
629
630 StringTemplateLoader templateLoader = new StringTemplateLoader();
631 templateLoader.putTemplate("adhoc.f3ah", "${1+1}");
632 templateLoader.putTemplate("adhoc.txt", "${1+1}");
633 cfgB.setTemplateLoader(templateLoader);
634
635 Configuration cfg = cfgB.build();
636
637 {
638 StringWriter out = new StringWriter();
639 cfg.getTemplate("adhoc.f3ah").process(null, out);
640 assertEquals("2", out.toString());
641 }
642 {
643 StringWriter out = new StringWriter();
644 cfg.getTemplate("adhoc.txt").process(null, out);
645 assertEquals("${1+1}", out.toString());
646 }
647
648 testedProps.add(Configuration.ExtendableBuilder.TEMPLATE_LANGUAGE_KEY);
649 }
650
651 {
652 // As the charset-selection happens in the TemplateResolver, we can't use
653 // assertOutput here, as that hard-coded to create an FTL Template.
654
655 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
656 tcb.setSourceEncoding(StandardCharsets.ISO_8859_1);
657
658 TestConfigurationBuilder cfgB = new TestConfigurationBuilder();
659 cfgB.setSourceEncoding(StandardCharsets.UTF_8);
660 cfgB.setTemplateConfigurations(new ConditionalTemplateConfigurationFactory(
661 new FileNameGlobMatcher("latin1.f3ah"), tcb.build()));
662
663 MonitoredTemplateLoader templateLoader = new MonitoredTemplateLoader();
664 templateLoader.putBinaryTemplate("utf8.f3ah", "próba", StandardCharsets.UTF_8, 1);
665 templateLoader.putBinaryTemplate("latin1.f3ah", "próba", StandardCharsets.ISO_8859_1, 1);
666 cfgB.setTemplateLoader(templateLoader);
667
668 Configuration cfg = cfgB.build();
669
670 {
671 StringWriter out = new StringWriter();
672 cfg.getTemplate("utf8.f3ah").process(null, out);
673 assertEquals("próba", out.toString());
674 }
675 {
676 StringWriter out = new StringWriter();
677 cfg.getTemplate("latin1.f3ah").process(null, out);
678 assertEquals("próba", out.toString());
679 }
680
681 testedProps.add(Configuration.ExtendableBuilder.SOURCE_ENCODING_KEY);
682 }
683
684 if (!PARSER_PROP_NAMES.equals(testedProps)) {
685 Set<String> diff = new HashSet<>(PARSER_PROP_NAMES);
686 diff.removeAll(testedProps);
687 fail("Some settings weren't checked: " + diff);
688 }
689 }
690
691 @Test
692 public void testArithmeticEngine() throws TemplateException, IOException {
693 TemplateConfiguration tc = new TemplateConfiguration.Builder()
694 .arithmeticEngine(new DummyArithmeticEngine())
695 .build();
696 assertOutputWithoutAndWithTC(tc,
697 "<#setting locale='en_US'>${1} ${1+1} ${1*3} <#assign x = 1>${x + x} ${x * 3}",
698 "1 2 3 2 3", "11 22 33 22 33");
699
700 // Does affect template.arithmeticEngine (unlike in FM2)
701 Template t = new Template(null, null, new StringReader(""), DEFAULT_CFG, tc, null);
702 assertEquals(tc.getArithmeticEngine(), t.getArithmeticEngine());
703 }
704
705 @Test
706 public void testAutoImport() throws TemplateException, IOException {
707 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
708 tcb.setAutoImports(ImmutableMap.of("t1", "t1.f3ah", "t2", "t2.f3ah"));
709 TemplateConfiguration tc = tcb.build();
710 assertOutputWithoutAndWithTC(tc, "<#import 't3.f3ah' as t3>${loaded}", "t3;", "t1;t2;t3;");
711 }
712
713 @Test
714 public void testAutoIncludes() throws TemplateException, IOException {
715 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
716 tcb.setAutoIncludes(ImmutableList.of("t1.f3ah", "t2.f3ah"));
717 TemplateConfiguration tc = tcb.build();
718 assertOutputWithoutAndWithTC(tc, "<#include 't3.f3ah'>", "In t3;", "In t1;In t2;In t3;");
719 }
720
721 @Test
722 public void testStringInterpolate() throws TemplateException, IOException {
723 TemplateConfiguration tc = new TemplateConfiguration.Builder()
724 .arithmeticEngine(new DummyArithmeticEngine())
725 .build();
726 assertOutputWithoutAndWithTC(tc,
727 "<#setting locale='en_US'>${'${1} ${1+1} ${1*3}'} <#assign x = 1>${'${x + x} ${x * 3}'}",
728 "1 2 3 2 3", "11 22 33 22 33");
729
730 // Does affect template.arithmeticEngine (unlike in FM2):
731 Template t = new Template(null, null, new StringReader(""), DEFAULT_CFG, tc, null);
732 assertEquals(tc.getArithmeticEngine(), t.getArithmeticEngine());
733 }
734
735 @Test
736 public void testInterpret() throws TemplateException, IOException {
737 {
738 TemplateConfiguration tc = new TemplateConfiguration.Builder()
739 .arithmeticEngine(new DummyArithmeticEngine())
740 .build();
741 assertOutputWithoutAndWithTC(tc,
742 "<#setting locale='en_US'><#assign src = r'${1} <#assign x = 1>${x + x}'><@src?interpret />",
743 "1 2", "11 22");
744 }
745 {
746 TemplateConfiguration tc = new TemplateConfiguration.Builder()
747 .whitespaceStripping(false)
748 .build();
749 assertOutputWithoutAndWithTC(tc,
750 "<#if true>\nX</#if><#assign src = r'<#if true>\nY</#if>'><@src?interpret />",
751 "XY", "\nX\nY");
752 }
753 }
754
755 @Test
756 public void testEval() throws TemplateException, IOException {
757 {
758 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
759 tcb.setArithmeticEngine(new DummyArithmeticEngine());
760 TemplateConfiguration tc = tcb.build();
761 assertOutputWithoutAndWithTC(tc,
762 "<#assign x = 1>${r'1 + x'?eval?c}",
763 "2", "22");
764 assertOutputWithoutAndWithTC(tc,
765 "${r'1?c'?eval}",
766 "1", "11");
767 }
768
769 {
770 Charset outputEncoding = ISO_8859_2;
771 TemplateConfiguration tc = new TemplateConfiguration.Builder()
772 .outputEncoding(outputEncoding)
773 .build();
774
775 // Default is re-auto-detecting in ?eval:
776 assertOutputWithoutAndWithTC(tc, "${r'.outputEncoding!\"null\"'?eval}",
777 "null", outputEncoding.name());
778 }
779 }
780
781 private void assertOutputWithoutAndWithTC(
782 TemplateConfiguration tc, String ftl, String expectedDefaultOutput,
783 String expectedConfiguredOutput) throws TemplateException, IOException {
784 assertOutputWithoutAndWithTC(tc, null, ftl, expectedDefaultOutput, expectedConfiguredOutput);
785 }
786
787 private void assertOutputWithoutAndWithTC(
788 TemplateConfiguration tc, String templateName, String ftl, String expectedDefaultOutput,
789 String expectedConfiguredOutput) throws TemplateException, IOException {
790 if (templateName == null) {
791 templateName = "adhoc";
792 }
793 assertOutput(null, templateName, ftl, expectedDefaultOutput);
794 assertOutput(tc, templateName, ftl, expectedConfiguredOutput);
795 }
796
797 private void assertOutput(TemplateConfiguration tc, String templateName, String ftl, String
798 expectedConfiguredOutput)
799 throws TemplateException, IOException {
800 StringWriter sw = new StringWriter();
801 try {
802 Template t = new Template(templateName, null, new StringReader(ftl), DEFAULT_CFG, tc, null);
803 t.process(null, sw);
804 if (expectedConfiguredOutput == null) {
805 fail("Template should have fail.");
806 }
807 } catch (TemplateException|ParseException e) {
808 if (expectedConfiguredOutput != null) {
809 throw e;
810 }
811 }
812 if (expectedConfiguredOutput != null) {
813 assertEquals(expectedConfiguredOutput, sw.toString());
814 }
815 }
816
817 @Test
818 public void testIsSet() throws Exception {
819 for (PropertyDescriptor pd : getTemplateConfigurationSettingPropDescs(
820 TemplateConfiguration.Builder.class, true)) {
821 checkAllIsSetFalseExcept(new TemplateConfiguration.Builder().build(), null);
822
823 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
824 pd.getWriteMethod().invoke(tcb, SETTING_ASSIGNMENTS.get(pd.getName()));
825 checkAllIsSetFalseExcept(tcb.build(), pd.getName());
826 }
827 }
828
829 private void checkAllIsSetFalseExcept(TemplateConfiguration tc, String setSetting)
830 throws SecurityException, IntrospectionException,
831 IllegalArgumentException, IllegalAccessException, InvocationTargetException {
832 for (PropertyDescriptor pd : getTemplateConfigurationSettingPropDescs(TemplateConfiguration.class, true)) {
833 String isSetMethodName = getIsSetMethodName(pd.getReadMethod().getName());
834 Method isSetMethod;
835 try {
836 isSetMethod = tc.getClass().getMethod(isSetMethodName);
837 } catch (NoSuchMethodException e) {
838 fail("Missing " + isSetMethodName + " method for \"" + pd.getName() + "\".");
839 return;
840 }
841 if (pd.getName().equals(setSetting)) {
842 assertTrue(isSetMethod + " should return true", (Boolean) (isSetMethod.invoke(tc)));
843 } else {
844 assertFalse(isSetMethod + " should return false", (Boolean) (isSetMethod.invoke(tc)));
845 }
846 }
847 }
848
849 /**
850 * Test case self-check.
851 */
852 @Test
853 public void checkTestAssignments() throws Exception {
854 for (PropertyDescriptor pd : getTemplateConfigurationSettingPropDescs(
855 TemplateConfiguration.Builder.class, true)) {
856 String propName = pd.getName();
857 if (!SETTING_ASSIGNMENTS.containsKey(propName)) {
858 fail("Test case doesn't cover all settings in SETTING_ASSIGNMENTS. Missing: " + propName);
859 }
860 Method readMethod = pd.getReadMethod();
861 String cfgMethodName = readMethod.getName();
862 Method cfgMethod = DEFAULT_CFG.getClass().getMethod(cfgMethodName, readMethod.getParameterTypes());
863 Object defaultSettingValue = cfgMethod.invoke(DEFAULT_CFG);
864 Object assignedValue = SETTING_ASSIGNMENTS.get(propName);
865 assertNotEquals("SETTING_ASSIGNMENTS must contain a non-default value for " + propName,
866 assignedValue, defaultSettingValue);
867
868 TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
869 try {
870 pd.getWriteMethod().invoke(tcb, assignedValue);
871 } catch (Exception e) {
872 throw new IllegalStateException("For setting \"" + propName + "\" and assigned value of type "
873 + (assignedValue != null ? assignedValue.getClass().getName() : "Null"),
874 e);
875 }
876 }
877 }
878
879 @Test
880 public void testCanBeBuiltOnlyOnce() {
881 TemplateConfiguration.Builder builder = new TemplateConfiguration.Builder();
882 builder.build();
883 try {
884 builder.build();
885 fail();
886 } catch (IllegalStateException e) {
887 // Expected
888 }
889 }
890
891 }