http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/DateFormatTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/src/test/java/org/apache/freemarker/core/DateFormatTest.java new file mode 100644 index 0000000..acac66d --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/DateFormatTest.java @@ -0,0 +1,438 @@ +/* + * 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.core; + +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.impl.SimpleDate; +import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher; +import org.apache.freemarker.test.TemplateTest; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; + +public class DateFormatTest extends TemplateTest { + + /** 2015-09-06T12:00:00Z */ + private static long T = 1441540800000L; + private static TemplateDateModel TM = new SimpleDate(new Date(T), TemplateDateModel.DATETIME); + + @Before + public void setup() { + Configuration cfg = getConfiguration(); + cfg.setIncompatibleImprovements(Configuration.VERSION_3_0_0); + cfg.setLocale(Locale.US); + cfg.setTimeZone(TimeZone.getTimeZone("GMT+01:00")); + cfg.setSQLDateAndTimeTimeZone(TimeZone.getTimeZone("UTC")); + + cfg.setCustomDateFormats(ImmutableMap.of( + "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE, + "loc", LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE, + "div", EpochMillisDivTemplateDateFormatFactory.INSTANCE, + "appMeta", AppMetaTemplateDateFormatFactory.INSTANCE, + "htmlIso", HTMLISOTemplateDateFormatFactory.INSTANCE)); + } + + @Test + public void testCustomFormat() throws Exception { + addToDataModel("d", new Date(123456789)); + assertOutput( + "${d?string.@epoch} ${d?string.@epoch} <#setting locale='de_DE'>${d?string.@epoch}", + "123456789 123456789 123456789"); + + getConfiguration().setDateTimeFormat("@epoch"); + assertOutput( + "<#assign d = d?datetime>" + + "${d} ${d?string} <#setting locale='de_DE'>${d}", + "123456789 123456789 123456789"); + + getConfiguration().setDateTimeFormat("@htmlIso"); + assertOutput( + "<#assign d = d?datetime>" + + "${d} ${d?string} <#setting locale='de_DE'>${d}", + "1970-01-02<span class='T'>T</span>10:17:36Z " + + "1970-01-02T10:17:36Z " + + "1970-01-02<span class='T'>T</span>10:17:36Z"); + } + + @Test + public void testLocaleChange() throws Exception { + addToDataModel("d", new Date(123456789)); + assertOutput( + "${d?string.@loc} ${d?string.@loc} " + + "<#setting locale='de_DE'>" + + "${d?string.@loc} ${d?string.@loc} " + + "<#setting locale='en_US'>" + + "${d?string.@loc} ${d?string.@loc}", + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 " + + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 " + + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00"); + + getConfiguration().setDateTimeFormat("@loc"); + assertOutput( + "<#assign d = d?datetime>" + + "${d} ${d?string} " + + "<#setting locale='de_DE'>" + + "${d} ${d?string} " + + "<#setting locale='en_US'>" + + "${d} ${d?string}", + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 " + + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 " + + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00"); + } + + @Test + public void testTimeZoneChange() throws Exception { + addToDataModel("d", new Date(123456789)); + getConfiguration().setDateTimeFormat("iso"); + assertOutput( + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} " + + "<#setting timeZone='GMT+02:00'>" + + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} " + + "<#setting timeZone='GMT+01:00'>" + + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal}", + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00 " + + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 1970-01-02T12:17:36+02:00 " + + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00"); + + getConfiguration().setDateTimeFormat("@loc"); + assertOutput( + "<#assign d = d?datetime>" + + "${d} ${d?string} " + + "<#setting timeZone='GMT+02:00'>" + + "${d} ${d?string} " + + "<#setting timeZone='GMT+01:00'>" + + "${d} ${d?string}", + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 " + + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 " + + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00"); + } + + @Test + public void testWrongFormatStrings() throws Exception { + getConfiguration().setDateTimeFormat("x1"); + assertErrorContains("${.now}", "\"x1\"", "'x'"); + assertErrorContains("${.now?string}", "\"x1\"", "'x'"); + getConfiguration().setDateTimeFormat("short"); + assertErrorContains("${.now?string('x2')}", "\"x2\"", "'x'"); + } + + @Test + public void testCustomParameterized() throws Exception { + Configuration cfg = getConfiguration(); + addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME)); + cfg.setDateTimeFormat("@div 1000"); + assertOutput("${d}", "12345"); + assertOutput("${d?string}", "12345"); + assertOutput("${d?string.@div_100}", "123456"); + + assertErrorContains("${d?string.@div_xyz}", "\"@div_xyz\"", "\"xyz\""); + cfg.setDateTimeFormat("@div"); + assertErrorContains("${d}", "\"datetime_format\"", "\"@div\"", "format parameter is required"); + } + + @Test + public void testUnknownCustomFormat() throws Exception { + { + getConfiguration().setDateTimeFormat("@noSuchFormat"); + Throwable exc = assertErrorContains( + "${.now}", + "\"@noSuchFormat\"", "\"noSuchFormat\"", "\"datetime_format\""); + assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class)); + + } + { + getConfiguration().setDateFormat("@noSuchFormatD"); + assertErrorContains( + "${.now?date}", + "\"@noSuchFormatD\"", "\"noSuchFormatD\"", "\"date_format\""); + } + { + getConfiguration().setTimeFormat("@noSuchFormatT"); + assertErrorContains( + "${.now?time}", + "\"@noSuchFormatT\"", "\"noSuchFormatT\"", "\"time_format\""); + } + + { + getConfiguration().setDateTimeFormat(""); + Throwable exc = assertErrorContains("${.now?string('@noSuchFormat2')}", + "\"@noSuchFormat2\"", "\"noSuchFormat2\""); + assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class)); + } + } + + @Test + public void testNullInModel() throws Exception { + addToDataModel("d", new MutableTemplateDateModel()); + assertErrorContains("${d}", "nothing inside it"); + assertErrorContains("${d?string}", "nothing inside it"); + } + + @Test + public void testIcIAndEscaping() throws Exception { + Configuration cfg = getConfiguration(); + addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME)); + + cfg.setDateTimeFormat("@epoch"); + assertOutput("${d}", "12345678"); + cfg.setDateTimeFormat("'@'yyyy"); + assertOutput("${d}", "@1970"); + cfg.setDateTimeFormat("@@yyyy"); + assertOutput("${d}", "@@1970"); + + cfg.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap()); + + cfg.setDateTimeFormat("@epoch"); + assertErrorContains("${d}", "custom", "\"epoch\""); + } + + @Test + public void testEnvironmentGetters() throws Exception { + Template t = new Template(null, "", getConfiguration()); + Environment env = t.createProcessingEnvironment(null, null); + + Configuration cfg = getConfiguration(); + + String dateFormatStr = "yyyy.MM.dd. (Z)"; + String timeFormatStr = "HH:mm"; + String dateTimeFormatStr = "yyyy.MM.dd. HH:mm"; + cfg.setDateFormat(dateFormatStr); + cfg.setTimeFormat(timeFormatStr); + cfg.setDateTimeFormat(dateTimeFormatStr); + + // Test that values are coming from the cache if possible + for (Class dateClass : new Class[] { Date.class, Timestamp.class, java.sql.Date.class, Time.class } ) { + for (int dateType + : new int[] { TemplateDateModel.DATE, TemplateDateModel.TIME, TemplateDateModel.DATETIME }) { + String formatString = + dateType == TemplateDateModel.DATE ? cfg.getDateFormat() : + (dateType == TemplateDateModel.TIME ? cfg.getTimeFormat() + : cfg.getDateTimeFormat()); + TemplateDateFormat expectedF = env.getTemplateDateFormat(formatString, dateType, dateClass); + assertSame(expectedF, env.getTemplateDateFormat(dateType, dateClass)); // Note: Only reads the cache + assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass)); + assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale())); + assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale(), + cfg.getTimeZone(), cfg.getSQLDateAndTimeTimeZone())); + } + } + + String dateFormatStr2 = dateFormatStr + "'!'"; + String timeFormatStr2 = timeFormatStr + "'!'"; + String dateTimeFormatStr2 = dateTimeFormatStr + "'!'"; + + assertEquals("2015.09.06. 13:00", + env.getTemplateDateFormat(TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM)); + assertEquals("2015.09.06. 13:00!", + env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM)); + + assertEquals("2015.09.06. (+0100)", + env.getTemplateDateFormat(TemplateDateModel.DATE, Date.class).formatToPlainText(TM)); + assertEquals("2015.09.06. (+0100)!", + env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, Date.class).formatToPlainText(TM)); + + assertEquals("13:00", + env.getTemplateDateFormat(TemplateDateModel.TIME, Date.class).formatToPlainText(TM)); + assertEquals("13:00!", + env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Date.class).formatToPlainText(TM)); + + assertEquals("2015.09.06. 13:00", + env.getTemplateDateFormat(TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM)); + assertEquals("2015.09.06. 13:00!", + env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM)); + + assertEquals("2015.09.06. (+0000)", + env.getTemplateDateFormat(TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM)); + assertEquals("2015.09.06. (+0000)!", + env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM)); + + assertEquals("12:00", + env.getTemplateDateFormat(TemplateDateModel.TIME, Time.class).formatToPlainText(TM)); + assertEquals("12:00!", + env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Time.class).formatToPlainText(TM)); + + { + String dateTimeFormatStrLoc = dateTimeFormatStr + " EEEE"; + // Gets into cache: + TemplateDateFormat format1 + = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class); + assertEquals("2015.09.06. 13:00 Sunday", format1.formatToPlainText(TM)); + // Different locale (not cached): + assertEquals("2015.09.06. 13:00 Sonntag", + env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class, + Locale.GERMANY).formatToPlainText(TM)); + // Different locale and zone (not cached): + assertEquals("2015.09.06. 14:00 Sonntag", + env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class, + Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM)); + // Different locale and zone (not cached): + assertEquals("2015.09.06. 15:00 Sonntag", + env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, java.sql.Date.class, + Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM)); + // Check for corrupted cache: + TemplateDateFormat format2 + = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class); + assertEquals("2015.09.06. 13:00 Sunday", format2.formatToPlainText(TM)); + assertSame(format1, format2); + } + + addToDataModel("d", TM); + assertErrorContains("${d?string('[wrong]')}", "format string", "[wrong]"); + cfg.setDateFormat("[wrong d]"); + cfg.setDateTimeFormat("[wrong dt]"); + cfg.setTimeFormat("[wrong t]"); + assertErrorContains("${d?date}", "\"date_format\"", "[wrong d]"); + assertErrorContains("${d?datetime}", "\"datetime_format\"", "[wrong dt]"); + assertErrorContains("${d?time}", "\"time_format\"", "[wrong t]"); + } + + @Test + public void testAlieses() throws Exception { + Configuration cfg = getConfiguration(); + cfg.setCustomDateFormats(ImmutableMap.of( + "d", new AliasTemplateDateFormatFactory("yyyy-MMM-dd"), + "m", new AliasTemplateDateFormatFactory("yyyy-MMM"), + "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE)); + + TemplateConfiguration tc = new TemplateConfiguration(); + tc.setCustomDateFormats(ImmutableMap.of( + "m", new AliasTemplateDateFormatFactory("yyyy-MMMM"), + "i", new AliasTemplateDateFormatFactory("@epoch"))); + cfg.setTemplateConfigurations(new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*2*"), tc)); + + addToDataModel("d", TM); + String commonFtl = "${d?string.@d} ${d?string.@m} " + + "<#setting locale='fr_FR'>${d?string.@m} " + + "<#attempt>${d?string.@i}<#recover>E</#attempt>"; + addTemplate("t1.ftl", commonFtl); + addTemplate("t2.ftl", commonFtl); + + // 2015-09-06T12:00:00Z + assertOutputForNamed("t1.ftl", "2015-Sep-06 2015-Sep 2015-sept. E"); + assertOutputForNamed("t2.ftl", "2015-Sep-06 2015-September 2015-septembre " + T); + } + + @Test + public void testAlieses2() throws Exception { + Configuration cfg = getConfiguration(); + cfg.setCustomDateFormats(ImmutableMap.of( + "d", new AliasTemplateDateFormatFactory("yyyy-MMM", + ImmutableMap.of( + new Locale("en"), "yyyy-MMM'_en'", + Locale.UK, "yyyy-MMM'_en_GB'", + Locale.FRANCE, "yyyy-MMM'_fr_FR'")))); + cfg.setDateTimeFormat("@d"); + addToDataModel("d", TM); + assertOutput( + "<#setting locale='en_US'>${d} " + + "<#setting locale='en_GB'>${d} " + + "<#setting locale='en_GB_Win'>${d} " + + "<#setting locale='fr_FR'>${d} " + + "<#setting locale='hu_HU'>${d}", + "2015-Sep_en 2015-Sep_en_GB 2015-Sep_en_GB 2015-sept._fr_FR 2015-szept."); + } + + /** + * ?date() and such are new in 2.3.24. + */ + @Test + public void testZeroArgDateBI() throws IOException, TemplateException { + Configuration cfg = getConfiguration(); + cfg.setDateFormat("@epoch"); + cfg.setDateTimeFormat("@epoch"); + cfg.setTimeFormat("@epoch"); + + addToDataModel("t", String.valueOf(T)); + + assertOutput( + "${t?date?string.xs_u} ${t?date()?string.xs_u}", + "2015-09-06Z 2015-09-06Z"); + assertOutput( + "${t?time?string.xs_u} ${t?time()?string.xs_u}", + "12:00:00Z 12:00:00Z"); + assertOutput( + "${t?datetime?string.xs_u} ${t?datetime()?string.xs_u}", + "2015-09-06T12:00:00Z 2015-09-06T12:00:00Z"); + } + + @Test + public void testAppMetaRoundtrip() throws IOException, TemplateException { + Configuration cfg = getConfiguration(); + cfg.setDateFormat("@appMeta"); + cfg.setDateTimeFormat("@appMeta"); + cfg.setTimeFormat("@appMeta"); + + addToDataModel("t", String.valueOf(T) + "/foo"); + + assertOutput( + "${t?date} ${t?date()}", + T + " " + T + "/foo"); + assertOutput( + "${t?time} ${t?time()}", + T + " " + T + "/foo"); + assertOutput( + "${t?datetime} ${t?datetime()}", + T + " " + T + "/foo"); + } + + @Test + public void testUnknownDateType() throws IOException, TemplateException { + addToDataModel("u", new Date(T)); + assertErrorContains("${u?string}", "isn't known"); + assertOutput("${u?string('yyyy')}", "2015"); + assertOutput("<#assign s = u?string>${s('yyyy')}", "2015"); + } + + private static class MutableTemplateDateModel implements TemplateDateModel { + + private Date date; + + public void setDate(Date date) { + this.date = date; + } + + @Override + public Date getAsDate() throws TemplateModelException { + return date; + } + + @Override + public int getDateType() { + return DATETIME; + } + + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java b/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java new file mode 100644 index 0000000..057b7dc --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java @@ -0,0 +1,260 @@ +/* + * 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.core; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.freemarker.core.CallPlaceCustomDataInitializationException; +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.DirectiveCallPlace; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.ASTDirUserDefined; +import org.apache.freemarker.core.model.TemplateDirectiveBody; +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.util.ObjectFactory; +import org.apache.freemarker.test.TemplateTest; +import org.junit.Test; + +public class DirectiveCallPlaceTest extends TemplateTest { + + @Override + protected Configuration createConfiguration() { + return new Configuration(Configuration.VERSION_3_0_0); + } + + @Test + public void testCustomDataBasics() throws IOException, TemplateException { + addTemplate( + "customDataBasics.ftl", + "<@uc>Abc</@uc> <@uc>x=${x}</@uc> <@uc>Ab<#-- -->c</@uc> <@lc/><@lc></@lc> <@lc>Abc</@lc>"); + + CachingTextConverterDirective.resetCacheRecreationCount(); + for (int i = 0; i < 3; i++) { + assertOutputForNamed( + "customDataBasics.ftl", + "ABC[cached 1] X=123 ABC[cached 2] abc[cached 3]"); + } + } + + @Test + public void testCustomDataProviderMismatch() throws IOException, TemplateException { + addTemplate( + "customDataProviderMismatch.ftl", + "<#list [uc, lc, uc] as d><#list 1..2 as _><@d>Abc</@d></#list></#list>"); + + CachingTextConverterDirective.resetCacheRecreationCount(); + assertOutputForNamed( + "customDataProviderMismatch.ftl", + "ABC[cached 1]ABC[cached 1]abc[cached 2]abc[cached 2]ABC[cached 3]ABC[cached 3]"); + assertOutputForNamed( + "customDataProviderMismatch.ftl", + "ABC[cached 3]ABC[cached 3]abc[cached 4]abc[cached 4]ABC[cached 5]ABC[cached 5]"); + } + + @Test + public void testPositions() throws IOException, TemplateException { + addTemplate( + "positions.ftl", + "<@pa />\n" + + "..<@pa\n" + + "/><@pa>xxx</@>\n" + + "<@pa>{<@pa/> <@pa/>}</@>\n" + + "${curDirLine}<@argP p=curDirLine?string>${curDirLine}</@argP>${curDirLine}\n" + + "<#macro m p>(p=${p}){<#nested>}</#macro>\n" + + "${curDirLine}<@m p=curDirLine?string>${curDirLine}</@m>${curDirLine}"); + + assertOutputForNamed( + "positions.ftl", + "[positions.ftl:1:1-1:7]" + + "..[positions.ftl:2:3-3:2]" + + "[positions.ftl:3:3-3:14]xxx\n" + + "[positions.ftl:4:1-4:24]{[positions.ftl:4:7-4:12] [positions.ftl:4:14-4:19]}\n" + + "-(p=5){-}-\n" + + "-(p=7){-}-" + ); + } + + @SuppressWarnings("boxing") + @Override + protected Object createDataModel() { + Map<String, Object> dm = new HashMap<>(); + dm.put("uc", new CachingUpperCaseDirective()); + dm.put("lc", new CachingLowerCaseDirective()); + dm.put("pa", new PositionAwareDirective()); + dm.put("argP", new ArgPrinterDirective()); + dm.put("curDirLine", new CurDirLineScalar()); + dm.put("x", 123); + return dm; + } + + private abstract static class CachingTextConverterDirective implements TemplateDirectiveModel { + + /** Only needed for testing. */ + private static AtomicInteger cacheRecreationCount = new AtomicInteger(); + + /** Only needed for testing. */ + static void resetCacheRecreationCount() { + cacheRecreationCount.set(0); + } + + @Override + public void execute(Environment env, Map params, TemplateModel[] loopVars, final TemplateDirectiveBody body) + throws TemplateException, IOException { + if (body == null) { + return; + } + + final String convertedText; + + final DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace(); + if (callPlace.isNestedOutputCacheable()) { + try { + convertedText = (String) callPlace.getOrCreateCustomData( + getTextConversionIdentity(), new ObjectFactory<String>() { + + @Override + public String createObject() throws TemplateException, IOException { + return convertBodyText(body) + + "[cached " + cacheRecreationCount.incrementAndGet() + "]"; + } + + }); + } catch (CallPlaceCustomDataInitializationException e) { + throw new TemplateModelException("Failed to pre-render nested content", e); + } + } else { + convertedText = convertBodyText(body); + } + + env.getOut().write(convertedText); + } + + protected abstract Class getTextConversionIdentity(); + + private String convertBodyText(TemplateDirectiveBody body) throws TemplateException, + IOException { + StringWriter sw = new StringWriter(); + body.render(sw); + return convertText(sw.toString()); + } + + protected abstract String convertText(String s); + + } + + private static class CachingUpperCaseDirective extends CachingTextConverterDirective { + + @Override + protected String convertText(String s) { + return s.toUpperCase(); + } + + @Override + protected Class getTextConversionIdentity() { + return CachingUpperCaseDirective.class; + } + + } + + private static class CachingLowerCaseDirective extends CachingTextConverterDirective { + + @Override + protected String convertText(String s) { + return s.toLowerCase(); + } + + @Override + protected Class getTextConversionIdentity() { + return CachingLowerCaseDirective.class; + } + + } + + private static class PositionAwareDirective implements TemplateDirectiveModel { + + @Override + public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) + throws TemplateException, IOException { + Writer out = env.getOut(); + DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace(); + out.write("["); + out.write(getTemplateSourceName(callPlace)); + out.write(":"); + out.write(Integer.toString(callPlace.getBeginLine())); + out.write(":"); + out.write(Integer.toString(callPlace.getBeginColumn())); + out.write("-"); + out.write(Integer.toString(callPlace.getEndLine())); + out.write(":"); + out.write(Integer.toString(callPlace.getEndColumn())); + out.write("]"); + if (body != null) { + body.render(out); + } + } + + private String getTemplateSourceName(DirectiveCallPlace callPlace) { + return ((ASTDirUserDefined) callPlace).getTemplate().getSourceName(); + } + + } + + private static class ArgPrinterDirective implements TemplateDirectiveModel { + + @Override + public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) + throws TemplateException, IOException { + final Writer out = env.getOut(); + if (params.size() > 0) { + out.write("(p="); + out.write(((TemplateScalarModel) params.get("p")).getAsString()); + out.write(")"); + } + if (body != null) { + out.write("{"); + body.render(out); + out.write("}"); + } + } + + } + + private static class CurDirLineScalar implements TemplateScalarModel { + + @Override + public String getAsString() throws TemplateModelException { + DirectiveCallPlace callPlace = Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace(); + return callPlace != null + ? String.valueOf(Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace().getBeginLine()) + : "-"; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/DummyOutputFormat.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/DummyOutputFormat.java b/src/test/java/org/apache/freemarker/core/DummyOutputFormat.java new file mode 100644 index 0000000..013b793 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/DummyOutputFormat.java @@ -0,0 +1,65 @@ +/* + * 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.core; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.freemarker.core.CommonMarkupOutputFormat; +import org.apache.freemarker.core.model.TemplateModelException; + +public class DummyOutputFormat extends CommonMarkupOutputFormat<TemplateDummyOutputModel> { + + public static final DummyOutputFormat INSTANCE = new DummyOutputFormat(); + + private DummyOutputFormat() { + // hide + } + + @Override + public String getName() { + return "dummy"; + } + + @Override + public String getMimeType() { + return "text/dummy"; + } + + @Override + public void output(String textToEsc, Writer out) throws IOException, TemplateModelException { + out.write(escapePlainText(textToEsc)); + } + + @Override + public String escapePlainText(String plainTextContent) { + return plainTextContent.replaceAll("(\\.|\\\\)", "\\\\$1"); + } + + @Override + public boolean isLegacyBuiltInBypassed(String builtInName) { + return false; + } + + @Override + protected TemplateDummyOutputModel newTemplateMarkupOutputModel(String plainTextContent, String markupContent) { + return new TemplateDummyOutputModel(plainTextContent, markupContent); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java b/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java new file mode 100644 index 0000000..b5a1f1a --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java @@ -0,0 +1,69 @@ +/* + * 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.core; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateException; +import org.junit.Test; + +public class EncodingOverrideTest { + + @Test + public void testExactMarchingCharset() throws Exception { + Template t = createConfig("UTF-8").getTemplate("encodingOverride-UTF-8.ftl"); + assertEquals("UTF-8", t.getEncoding()); + checkTempateOutput(t); + } + + @Test + public void testCaseDiffCharset() throws Exception { + Template t = createConfig("utf-8").getTemplate("encodingOverride-UTF-8.ftl"); + assertEquals("utf-8", t.getEncoding()); + checkTempateOutput(t); + } + + @Test + public void testReallyDiffCharset() throws Exception { + Template t = createConfig("utf-8").getTemplate("encodingOverride-ISO-8859-1.ftl"); + assertEquals("ISO-8859-1", t.getEncoding()); + checkTempateOutput(t); + } + + private void checkTempateOutput(Template t) throws TemplateException, IOException { + StringWriter out = new StringWriter(); + t.process(Collections.emptyMap(), out); + assertEquals("Béka", out.toString()); + } + + private Configuration createConfig(String charset) { + Configuration cfg = new Configuration(Configuration.VERSION_3_0_0); + cfg.setClassForTemplateLoading(EncodingOverrideTest.class, ""); + cfg.setDefaultEncoding(charset); + return cfg; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/EnvironmentCustomStateTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/EnvironmentCustomStateTest.java b/src/test/java/org/apache/freemarker/core/EnvironmentCustomStateTest.java new file mode 100644 index 0000000..e7df6e7 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/EnvironmentCustomStateTest.java @@ -0,0 +1,51 @@ +/* + * 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.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.Template; +import org.junit.Test; + +public class EnvironmentCustomStateTest { + + private static final Object KEY_1 = new Object(); + private static final Object KEY_2 = new Object(); + + @Test + public void test() throws Exception { + Configuration cfg = new Configuration(Configuration.VERSION_3_0_0); + Template t = new Template(null, "", cfg); + Environment env = t.createProcessingEnvironment(null, null); + assertNull(env.getCustomState(KEY_1)); + assertNull(env.getCustomState(KEY_2)); + env.setCustomState(KEY_1, "a"); + env.setCustomState(KEY_2, "b"); + assertEquals("a", env.getCustomState(KEY_1)); + assertEquals("b", env.getCustomState(KEY_2)); + env.setCustomState(KEY_1, "c"); + env.setCustomState(KEY_2, null); + assertEquals("c", env.getCustomState(KEY_1)); + assertNull(env.getCustomState(KEY_2)); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java new file mode 100644 index 0000000..52508f9 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java @@ -0,0 +1,218 @@ +/* + * 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.core; + +import static org.junit.Assert.assertSame; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.Map; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.Template; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.Version; +import org.apache.freemarker.core.model.TemplateDirectiveBody; +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader; +import org.apache.freemarker.test.TemplateTest; +import org.junit.Test; + +public class EnvironmentGetTemplateVariantsTest extends TemplateTest { + + private static final StringTemplateLoader TEMPLATES = new StringTemplateLoader(); + static { + TEMPLATES.putTemplate("main", + "<@tNames />\n" + + "---1---\n" + + "[imp: <#import 'imp' as i>${i.impIni}]\n" + + "---2---\n" + + "<@i.impM>" + + "<@tNames />" + + "</@>\n" + + "---3---\n" + + "[inc: <#include 'inc'>]\n" + + "---4---\n" + + "<@incM>" + + "<@tNames />" + + "</@>\n" + + "---5---\n" + + "[inc2: <#include 'inc2'>]\n" + + "---6---\n" + + "<#import 'imp2' as i2>" + + "<@i.impM2><@tNames /></@>\n" + + "---7---\n" + + "<#macro mainM>" + + "[mainM: <@tNames /> {<#nested>} <@tNames />]" + + "</#macro>" + + "[inc3: <#include 'inc3'>]\n" + + "<@mainM><@tNames /> <#include 'inc4'> <@tNames /></@>\n" + + "<@tNames />\n" + + "---8---\n" + + "<#function mainF>" + + "<@tNames />" + + "<#return lastTNamesResult>" + + "</#function>" + + "mainF: ${mainF()}, impF: ${i.impF()}, incF: ${incF()}\n" + ); + TEMPLATES.putTemplate("inc", + "<@tNames />\n" + + "<#macro incM>" + + "[incM: <@tNames /> {<#nested>}]" + + "</#macro>" + + "<#function incF>" + + "<@tNames />" + + "<#return lastTNamesResult>" + + "</#function>" + + "<@incM><@tNames /></@>\n" + + "<#if !included!false>[incInc: <#assign included=true><#include 'inc'>]\n</#if>" + ); + TEMPLATES.putTemplate("imp", + "<#assign impIni><@tNames /></#assign>\n" + + "<#macro impM>" + + "[impM: <@tNames />\n" + + "{<#nested>}\n" + + "[inc: <#include 'inc'>]\n" + + "<@incM><@tNames /></@>\n" + + "]" + + "</#macro>" + + "<#macro impM2>" + + "[impM2: <@tNames />\n" + + "{<#nested>}\n" + + "<@i2.imp2M><@tNames /></@>\n" + + "]" + + "</#macro>" + + "<#function impF>" + + "<@tNames />" + + "<#return lastTNamesResult>" + + "</#function>" + ); + TEMPLATES.putTemplate("inc2", + "<@tNames />\n" + + "<@i.impM><@tNames /></@>\n" + ); + TEMPLATES.putTemplate("imp2", + "<#macro imp2M>" + + "[imp2M: <@tNames /> {<#nested>}]" + + "</#macro>"); + TEMPLATES.putTemplate("inc3", + "<@tNames />\n" + + "<@mainM><@tNames /></@>\n" + ); + TEMPLATES.putTemplate("inc4", + "<@tNames />" + ); + } + + @Test + public void test() throws IOException, TemplateException { + setConfiguration(createConfiguration(Configuration.VERSION_3_0_0)); + assertOutputForNamed( + "main", + "<ct=main mt=main>\n" + + "---1---\n" + + "[imp: <ct=imp mt=main>]\n" + + "---2---\n" + + "[impM: <ct=imp mt=main>\n" + + "{<ct=main mt=main>}\n" + + "[inc: <ct=inc mt=main>\n" + + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n" + + "[incInc: <ct=inc mt=main>\n" + + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n" + + "]\n" + + "]\n" + + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n" + + "]\n" + + "---3---\n" + + "[inc: <ct=inc mt=main>\n" + + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n" + + "[incInc: <ct=inc mt=main>\n" + + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n" + + "]\n" + + "]\n" + + "---4---\n" + + "[incM: <ct=inc mt=main> {<ct=main mt=main>}]\n" + + "---5---\n" + + "[inc2: <ct=inc2 mt=main>\n" + + "[impM: <ct=imp mt=main>\n" + + "{<ct=inc2 mt=main>}\n" + + "[inc: <ct=inc mt=main>\n" + + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n" + + "]\n" + + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n" + + "]\n" + + "]\n" + + "---6---\n" + + "[impM2: <ct=imp mt=main>\n" + + "{<ct=main mt=main>}\n" + + "[imp2M: <ct=imp2 mt=main> {<ct=imp mt=main>}]\n" + + "]\n" + + "---7---\n" + + "[inc3: <ct=inc3 mt=main>\n" + + "[mainM: <ct=main mt=main> {<ct=inc3 mt=main>} <ct=main mt=main>]\n" + + "]\n" + + "[mainM: " + + "<ct=main mt=main> " + + "{<ct=main mt=main> <ct=inc4 mt=main> <ct=main mt=main>} " + + "<ct=main mt=main>" + + "]\n" + + "<ct=main mt=main>\n" + + "---8---\n" + + "mainF: <ct=main mt=main>, impF: <ct=imp mt=main>, incF: <ct=inc mt=main>\n" + .replaceAll("<t=\\w+", "<t=main")); + } + + @Test + public void testNotStarted() throws IOException, TemplateException { + Template t = new Template("foo", "", createConfiguration(Configuration.VERSION_3_0_0)); + final Environment env = t.createProcessingEnvironment(null, null); + assertSame(t, env.getMainTemplate()); + assertSame(t, env.getCurrentTemplate()); + } + + private Configuration createConfiguration(Version iciVersion) { + Configuration cfg = new Configuration(iciVersion); + cfg.setTemplateLoader(TEMPLATES); + cfg.setWhitespaceStripping(false); + return cfg; + } + + @Override + protected Object createDataModel() { + return Collections.singletonMap("tNames", new TemplateDirectiveModel() { + + @Override + public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) + throws TemplateException, IOException { + Writer out = env.getOut(); + final String r = "<ct=" + env.getCurrentTemplate().getName() + " mt=" + + env.getMainTemplate().getName() + ">"; + out.write(r); + env.setGlobalVariable("lastTNamesResult", new SimpleScalar(r)); + } + + }); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java b/src/test/java/org/apache/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java new file mode 100644 index 0000000..15be994 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/EpochMillisDivTemplateDateFormatFactory.java @@ -0,0 +1,102 @@ +/* + * 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.core; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.InvalidFormatParametersException; +import org.apache.freemarker.core.TemplateDateFormat; +import org.apache.freemarker.core.TemplateDateFormatFactory; +import org.apache.freemarker.core.TemplateFormatUtil; +import org.apache.freemarker.core.UnformattableValueException; +import org.apache.freemarker.core.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.UnparsableValueException; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._StringUtil; + +public class EpochMillisDivTemplateDateFormatFactory extends TemplateDateFormatFactory { + + public static final EpochMillisDivTemplateDateFormatFactory INSTANCE = new EpochMillisDivTemplateDateFormatFactory(); + + private EpochMillisDivTemplateDateFormatFactory() { + // Defined to decrease visibility + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + int divisor; + try { + divisor = Integer.parseInt(params); + } catch (NumberFormatException e) { + if (params.length() == 0) { + throw new InvalidFormatParametersException( + "A format parameter is required, which specifies the divisor."); + } + throw new InvalidFormatParametersException( + "The format paramter must be an integer, but was (shown quoted): " + _StringUtil.jQuote(params)); + } + return new EpochMillisDivTemplateDateFormat(divisor); + } + + private static class EpochMillisDivTemplateDateFormat extends TemplateDateFormat { + + private final int divisor; + + private EpochMillisDivTemplateDateFormat(int divisor) { + this.divisor = divisor; + } + + @Override + public String formatToPlainText(TemplateDateModel dateModel) + throws UnformattableValueException, TemplateModelException { + return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime() / divisor); + } + + @Override + public boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return false; + } + + @Override + public Date parse(String s, int dateType) throws UnparsableValueException { + try { + return new Date(Long.parseLong(s)); + } catch (NumberFormatException e) { + throw new UnparsableValueException("Malformed long"); + } + } + + @Override + public String getDescription() { + return "millis since the epoch"; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/EpochMillisTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/EpochMillisTemplateDateFormatFactory.java b/src/test/java/org/apache/freemarker/core/EpochMillisTemplateDateFormatFactory.java new file mode 100644 index 0000000..dd2c311 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/EpochMillisTemplateDateFormatFactory.java @@ -0,0 +1,92 @@ +/* + * 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.core; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.InvalidFormatParametersException; +import org.apache.freemarker.core.TemplateDateFormat; +import org.apache.freemarker.core.TemplateDateFormatFactory; +import org.apache.freemarker.core.TemplateFormatUtil; +import org.apache.freemarker.core.UnformattableValueException; +import org.apache.freemarker.core.UnparsableValueException; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; + +public class EpochMillisTemplateDateFormatFactory extends TemplateDateFormatFactory { + + public static final EpochMillisTemplateDateFormatFactory INSTANCE + = new EpochMillisTemplateDateFormatFactory(); + + private EpochMillisTemplateDateFormatFactory() { + // Defined to decrease visibility + } + + @Override + public TemplateDateFormat get(String params, int dateType, + Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) + throws InvalidFormatParametersException { + TemplateFormatUtil.checkHasNoParameters(params); + return EpochMillisTemplateDateFormat.INSTANCE; + } + + private static class EpochMillisTemplateDateFormat extends TemplateDateFormat { + + private static final EpochMillisTemplateDateFormat INSTANCE + = new EpochMillisTemplateDateFormat(); + + private EpochMillisTemplateDateFormat() { } + + @Override + public String formatToPlainText(TemplateDateModel dateModel) + throws UnformattableValueException, TemplateModelException { + return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime()); + } + + @Override + public boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return false; + } + + @Override + public Date parse(String s, int dateType) throws UnparsableValueException { + try { + return new Date(Long.parseLong(s)); + } catch (NumberFormatException e) { + throw new UnparsableValueException("Malformed long"); + } + } + + @Override + public String getDescription() { + return "millis since the epoch"; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/ExceptionTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/ExceptionTest.java b/src/test/java/org/apache/freemarker/core/ExceptionTest.java index 174a464..c9e66e8 100644 --- a/src/test/java/org/apache/freemarker/core/ExceptionTest.java +++ b/src/test/java/org/apache/freemarker/core/ExceptionTest.java @@ -32,7 +32,6 @@ import java.io.StringWriter; import java.util.Collections; import java.util.Locale; -import org.apache.freemarker.core.ast.ParseException; import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader; import org.apache.freemarker.core.util._NullWriter; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java b/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java new file mode 100644 index 0000000..9842e61 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java @@ -0,0 +1,341 @@ +/* + * 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.core; + +import static org.apache.freemarker.test.hamcerst.Matchers.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.text.ParseException; +import java.util.Locale; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.ExtendedDecimalFormatParser; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.test.TemplateTest; +import org.junit.Test; + +public class ExtendedDecimalFormatTest extends TemplateTest { + + private static final Locale LOC = Locale.US; + + @Test + public void testNonExtended() throws ParseException { + for (String fStr : new String[] { "0.00", "0.###", "#,#0.###", "#0.####", "0.0;m", "0.0;", + "0'x'", "0'x';'m'", "0';'", "0';';m", "0';';'#'m';'", "0';;'", "" }) { + assertFormatsEquivalent(new DecimalFormat(fStr), ExtendedDecimalFormatParser.parse(fStr, LOC)); + } + + try { + new DecimalFormat(";"); + fail(); + } catch (IllegalArgumentException e) { + // Expected + } + try { + ExtendedDecimalFormatParser.parse(";", LOC); + } catch (ParseException e) { + // Expected + } + } + + @Test + public void testNonExtended2() throws ParseException { + assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0.0;m"), ExtendedDecimalFormatParser.parse("0.0;m;", LOC)); + assertFormatsEquivalent(new DecimalFormat(""), ExtendedDecimalFormatParser.parse(";;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0'x'"), ExtendedDecimalFormatParser.parse("0'x';;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0'x';'m'"), ExtendedDecimalFormatParser.parse("0'x';'m';", LOC)); + assertFormatsEquivalent(new DecimalFormat("0';'"), ExtendedDecimalFormatParser.parse("0';';;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0';';m"), ExtendedDecimalFormatParser.parse("0';';m;", LOC)); + assertFormatsEquivalent(new DecimalFormat("0';';'#'m';'"), ExtendedDecimalFormatParser.parse("0';';'#'m';';", + LOC)); + assertFormatsEquivalent(new DecimalFormat("0';;'"), ExtendedDecimalFormatParser.parse("0';;';;", LOC)); + + try { + new DecimalFormat(";m"); + fail(); + } catch (IllegalArgumentException e) { + // Expected + } + try { + new DecimalFormat("; ;"); + fail(); + } catch (IllegalArgumentException e) { + // Expected + } + try { + ExtendedDecimalFormatParser.parse("; ;", LOC); + fail(); + } catch (ParseException e) { + // Expected + } + try { + ExtendedDecimalFormatParser.parse(";m", LOC); + fail(); + } catch (ParseException e) { + // Expected + } + try { + ExtendedDecimalFormatParser.parse(";m;", LOC); + fail(); + } catch (ParseException e) { + // Expected + } + } + + @SuppressWarnings("boxing") + @Test + public void testExtendedParamsParsing() throws ParseException { + for (String fs : new String[] { + "00.##;; decimalSeparator='D'", + "00.##;;decimalSeparator=D", + "00.##;; decimalSeparator = D ", "00.##;; decimalSeparator = 'D' " }) { + assertFormatted(fs, 1.125, "01D12"); + } + for (String fs : new String[] { + ",#0.0;; decimalSeparator=D, groupingSeparator=_", + ",#0.0;;decimalSeparator=D,groupingSeparator=_", + ",#0.0;; decimalSeparator = D , groupingSeparator = _ ", + ",#0.0;; decimalSeparator='D', groupingSeparator='_'" + }) { + assertFormatted(fs, 12345, "1_23_45D0"); + } + + assertFormatted("0.0;;infinity=infinity", Double.POSITIVE_INFINITY, "infinity"); + assertFormatted("0.0;;infinity='infinity'", Double.POSITIVE_INFINITY, "infinity"); + assertFormatted("0.0;;infinity=\"infinity\"", Double.POSITIVE_INFINITY, "infinity"); + assertFormatted("0.0;;infinity=''", Double.POSITIVE_INFINITY, ""); + assertFormatted("0.0;;infinity=\"\"", Double.POSITIVE_INFINITY, ""); + assertFormatted("0.0;;infinity='x''y'", Double.POSITIVE_INFINITY, "x'y"); + assertFormatted("0.0;;infinity=\"x'y\"", Double.POSITIVE_INFINITY, "x'y"); + assertFormatted("0.0;;infinity='x\"\"y'", Double.POSITIVE_INFINITY, "x\"\"y"); + assertFormatted("0.0;;infinity=\"x''y\"", Double.POSITIVE_INFINITY, "x''y"); + assertFormatted("0.0;;decimalSeparator=''''", 1, "1'0"); + assertFormatted("0.0;;decimalSeparator=\"'\"", 1, "1'0"); + assertFormatted("0.0;;decimalSeparator='\"'", 1, "1\"0"); + assertFormatted("0.0;;decimalSeparator=\"\"\"\"", 1, "1\"0"); + + try { + ExtendedDecimalFormatParser.parse(";;decimalSeparator=D,", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), + allOf(containsStringIgnoringCase("expected a(n) name"), containsString(" end of "))); + } + try { + ExtendedDecimalFormatParser.parse(";;foo=D,", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), + allOf(containsString("\"foo\""), containsString("name"))); + } + try { + ExtendedDecimalFormatParser.parse(";;decimalSeparator='D", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), + allOf(containsString("quotation"), containsString("closed"))); + } + try { + ExtendedDecimalFormatParser.parse(";;decimalSeparator=\"D", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), + allOf(containsString("quotation"), containsString("closed"))); + } + try { + ExtendedDecimalFormatParser.parse(";;decimalSeparator='D'groupingSeparator=G", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), allOf( + containsString("separator"), containsString("whitespace"), containsString("comma"))); + } + try { + ExtendedDecimalFormatParser.parse(";;decimalSeparator=., groupingSeparator=G", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), allOf( + containsStringIgnoringCase("expected a(n) value"), containsString("., gr[...]"))); + } + try { + ExtendedDecimalFormatParser.parse("0.0;;decimalSeparator=''", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), allOf( + containsStringIgnoringCase("\"decimalSeparator\""), containsString("exactly 1 char"))); + } + try { + ExtendedDecimalFormatParser.parse("0.0;;multipier=ten", LOC); + fail(); + } catch (java.text.ParseException e) { + assertThat(e.getMessage(), allOf( + containsString("\"multipier\""), containsString("\"ten\""), containsString("integer"))); + } + } + + @SuppressWarnings("boxing") + @Test + public void testExtendedParamsEffect() throws ParseException { + assertFormatted("0", + 1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2"); + assertFormatted("0;; roundingMode=halfEven", + 1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2"); + assertFormatted("0;; roundingMode=halfUp", + 1.5, "2", 2.5, "3", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-3", -1.6, "-2"); + assertFormatted("0;; roundingMode=halfDown", + 1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-2"); + assertFormatted("0;; roundingMode=floor", + 1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2"); + assertFormatted("0;; roundingMode=ceiling", + 1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1"); + assertFormatted("0;; roundingMode=up", + 1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2"); + assertFormatted("0;; roundingMode=down", + 1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1"); + assertFormatted("0;; roundingMode=unnecessary", 2, "2"); + try { + assertFormatted("0;; roundingMode=unnecessary", 2.5, "2"); + fail(); + } catch (ArithmeticException e) { + // Expected + } + + assertFormatted("0.##;; multipier=100", 12.345, "1234.5"); + assertFormatted("0.##;; multipier=1000", 12.345, "12345"); + + assertFormatted(",##0.##;; groupingSeparator=_ decimalSeparator=D", 12345.1, "12_345D1", 1, "1"); + + assertFormatted("0.##E0;; exponentSeparator='*10^'", 12345.1, "1.23*10^4"); + + assertFormatted("0.##;; minusSign=m", -1, "m1", 1, "1"); + + assertFormatted("0.##;; infinity=foo", Double.POSITIVE_INFINITY, "foo", Double.NEGATIVE_INFINITY, "-foo"); + + assertFormatted("0.##;; nan=foo", Double.NaN, "foo"); + + assertFormatted("0%;; percent='c'", 0.75, "75c"); + + assertFormatted("0\u2030;; perMill='m'", 0.75, "750m"); + + assertFormatted("0.00;; zeroDigit='@'", 10.5, "[email protected]@"); + + assertFormatted("0;; currencyCode=USD", 10, "10"); + assertFormatted("0 \u00A4;; currencyCode=USD", 10, "10 $"); + assertFormatted("0 \u00A4\u00A4;; currencyCode=USD", 10, "10 USD"); + assertFormatted(Locale.GERMANY, "0 \u00A4;; currencyCode=EUR", 10, "10 \u20AC"); + assertFormatted(Locale.GERMANY, "0 \u00A4\u00A4;; currencyCode=EUR", 10, "10 EUR"); + try { + assertFormatted("0;; currencyCode=USDX", 10, "10"); + } catch (ParseException e) { + assertThat(e.getMessage(), containsString("ISO 4217")); + } + assertFormatted("0 \u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 bucks"); + // Order doesn't mater: + assertFormatted("0 \u00A4;; currencySymbol=bucks currencyCode=USD", 10, "10 bucks"); + // International symbol isn't affected: + assertFormatted("0 \u00A4\u00A4;; currencyCode=USD currencySymbol=bucks", 10, "10 USD"); + + assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m", 10.5, "10m5 $"); + assertFormatted("0.0 kg;; monetaryDecimalSeparator=m", 10.5, "10.5 kg"); + assertFormatted("0.0 \u00A4;; decimalSeparator=d", 10.5, "10.5 $"); + assertFormatted("0.0 kg;; decimalSeparator=d", 10.5, "10d5 kg"); + assertFormatted("0.0 \u00A4;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10m5 $"); + assertFormatted("0.0 kg;; monetaryDecimalSeparator=m decimalSeparator=d", 10.5, "10d5 kg"); + } + + @Test + public void testLocale() throws ParseException { + assertEquals("1000.0", ExtendedDecimalFormatParser.parse("0.0", Locale.US).format(1000)); + assertEquals("1000,0", ExtendedDecimalFormatParser.parse("0.0", Locale.FRANCE).format(1000)); + assertEquals("1_000.0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.US).format(1000)); + assertEquals("1_000,0", ExtendedDecimalFormatParser.parse(",000.0;;groupingSeparator=_", Locale.FRANCE).format(1000)); + } + + @Test + public void testTemplates() throws IOException, TemplateException { + Configuration cfg = getConfiguration(); + cfg.setLocale(Locale.US); + + cfg.setNumberFormat(",000.#"); + assertOutput("${1000.15} ${1000.25}", "1,000.2 1,000.2"); + cfg.setNumberFormat(",000.#;; roundingMode=halfUp groupingSeparator=_"); + assertOutput("${1000.15} ${1000.25}", "1_000.2 1_000.3"); + cfg.setLocale(Locale.GERMANY); + assertOutput("${1000.15} ${1000.25}", "1_000,2 1_000,3"); + cfg.setLocale(Locale.US); + assertOutput( + "${1000.15}; " + + "${1000.15?string(',##.#;;groupingSeparator=\" \"')}; " + + "<#setting locale='de_DE'>${1000.15}; " + + "<#setting numberFormat='0.0;;roundingMode=down'>${1000.15}", + "1_000.2; 10 00.2; 1_000,2; 1000,1"); + assertErrorContains("${1?string('#E')}", + TemplateException.class, "\"#E\"", "format string", "exponential"); + assertErrorContains("<#setting numberFormat='#E'>${1}", + TemplateException.class, "\"#E\"", "format string", "exponential"); + assertErrorContains("<#setting numberFormat=';;foo=bar'>${1}", + TemplateException.class, "\"foo\"", "supported"); + assertErrorContains("<#setting numberFormat='0;;roundingMode=unnecessary'>${1.5}", + TemplateException.class, "can't format", "1.5", "UNNECESSARY"); + } + + private void assertFormatted(String formatString, Object... numberAndExpectedOutput) throws ParseException { + assertFormatted(LOC, formatString, numberAndExpectedOutput); + } + + private void assertFormatted(Locale loc, String formatString, Object... numberAndExpectedOutput) throws ParseException { + if (numberAndExpectedOutput.length % 2 != 0) { + throw new IllegalArgumentException(); + } + + DecimalFormat df = ExtendedDecimalFormatParser.parse(formatString, loc); + Number num = null; + for (int i = 0; i < numberAndExpectedOutput.length; i++) { + if (i % 2 == 0) { + num = (Number) numberAndExpectedOutput[i]; + } else { + assertEquals(numberAndExpectedOutput[i], df.format(num)); + } + } + } + + private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual) { + for (int signum : new int[] { 1, -1 }) { + assertFormatsEquivalent(dfExpected, dfActual, signum * 0); + assertFormatsEquivalent(dfExpected, dfActual, signum * 0.5); + assertFormatsEquivalent(dfExpected, dfActual, signum * 0.25); + assertFormatsEquivalent(dfExpected, dfActual, signum * 0.125); + assertFormatsEquivalent(dfExpected, dfActual, signum * 1); + assertFormatsEquivalent(dfExpected, dfActual, signum * 10); + assertFormatsEquivalent(dfExpected, dfActual, signum * 100); + assertFormatsEquivalent(dfExpected, dfActual, signum * 1000); + assertFormatsEquivalent(dfExpected, dfActual, signum * 10000); + assertFormatsEquivalent(dfExpected, dfActual, signum * 100000); + } + } + + private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual, double n) { + assertEquals(dfExpected.format(n), dfActual.format(n)); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/HTMLISOTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/HTMLISOTemplateDateFormatFactory.java b/src/test/java/org/apache/freemarker/core/HTMLISOTemplateDateFormatFactory.java new file mode 100644 index 0000000..40305b8 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/HTMLISOTemplateDateFormatFactory.java @@ -0,0 +1,114 @@ +/* + * 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.core; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.HTMLOutputFormat; +import org.apache.freemarker.core.InvalidFormatParametersException; +import org.apache.freemarker.core.TemplateDateFormat; +import org.apache.freemarker.core.TemplateDateFormatFactory; +import org.apache.freemarker.core.TemplateFormatUtil; +import org.apache.freemarker.core.TemplateValueFormatException; +import org.apache.freemarker.core.UnformattableValueException; +import org.apache.freemarker.core.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.UnparsableValueException; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; + +public class HTMLISOTemplateDateFormatFactory extends TemplateDateFormatFactory { + + public static final HTMLISOTemplateDateFormatFactory INSTANCE = new HTMLISOTemplateDateFormatFactory(); + + private HTMLISOTemplateDateFormatFactory() { + // Defined to decrease visibility + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + TemplateFormatUtil.checkHasNoParameters(params); + return HTMLISOTemplateDateFormat.INSTANCE; + } + + private static class HTMLISOTemplateDateFormat extends TemplateDateFormat { + + private static final HTMLISOTemplateDateFormat INSTANCE = new HTMLISOTemplateDateFormat(); + + private _DateUtil.TrivialDateToISO8601CalendarFactory calendarFactory; + + private CalendarFieldsToDateConverter calToDateConverter; + + private HTMLISOTemplateDateFormat() { } + + @Override + public String formatToPlainText(TemplateDateModel dateModel) + throws UnformattableValueException, TemplateModelException { + if (calendarFactory == null) { + calendarFactory = new _DateUtil.TrivialDateToISO8601CalendarFactory(); + } + return _DateUtil.dateToISO8601String( + TemplateFormatUtil.getNonNullDate(dateModel), + true, true, true, _DateUtil.ACCURACY_SECONDS, _DateUtil.UTC, + calendarFactory); + } + + @Override + public boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return false; + } + + @Override + public Date parse(String s, int dateType) throws UnparsableValueException { + try { + if (calToDateConverter == null) { + calToDateConverter = new _DateUtil.TrivialCalendarFieldsToDateConverter(); + } + return _DateUtil.parseISO8601DateTime(s, _DateUtil.UTC, calToDateConverter); + } catch (DateParseException e) { + throw new UnparsableValueException("Malformed ISO date-time", e); + } + } + + @Override + public Object format(TemplateDateModel dateModel) throws TemplateValueFormatException, TemplateModelException { + return HTMLOutputFormat.INSTANCE.fromMarkup( + formatToPlainText(dateModel).replace("T", "<span class='T'>T</span>")); + } + + @Override + public String getDescription() { + return "ISO UTC HTML"; + } + + } + +} + \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/HTMLOutputFormatTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/HTMLOutputFormatTest.java b/src/test/java/org/apache/freemarker/core/HTMLOutputFormatTest.java new file mode 100644 index 0000000..cb02f88 --- /dev/null +++ b/src/test/java/org/apache/freemarker/core/HTMLOutputFormatTest.java @@ -0,0 +1,187 @@ +/* + * 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.core; + +import static org.apache.freemarker.core.HTMLOutputFormat.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.StringWriter; + +import org.apache.freemarker.core.CommonMarkupOutputFormat; +import org.apache.freemarker.core.TemplateHTMLOutputModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.junit.Test; + +/** + * This actually more a {@link CommonMarkupOutputFormat} test. + */ +public class HTMLOutputFormatTest { + + @Test + public void testOutputMO() throws TemplateModelException, IOException { + StringWriter out = new StringWriter(); + + INSTANCE.output(INSTANCE.fromMarkup("<p>Test "), out); + INSTANCE.output(INSTANCE.fromPlainTextByEscaping("foo & bar "), out); + INSTANCE.output(INSTANCE.fromPlainTextByEscaping("baaz "), out); + INSTANCE.output(INSTANCE.fromPlainTextByEscaping("<b>A</b> <b>B</b> <b>C</b>"), out); + INSTANCE.output(INSTANCE.fromPlainTextByEscaping(""), out); + INSTANCE.output(INSTANCE.fromPlainTextByEscaping("\"' x's \"y\" \""), out); + INSTANCE.output(INSTANCE.fromMarkup("</p>"), out); + + assertEquals( + "<p>Test " + + "foo & bar " + + "baaz " + + "<b>A</b> <b>B</b> <b>C</b>" + + ""' x's "y" "" + + "</p>", + out.toString()); + } + + @Test + public void testOutputString() throws TemplateModelException, IOException { + StringWriter out = new StringWriter(); + + INSTANCE.output("a", out); + INSTANCE.output("<", out); + INSTANCE.output("b'c", out); + + assertEquals("a<b'c", out.toString()); + } + + @Test + public void testFromPlainTextByEscaping() throws TemplateModelException { + String plainText = "a&b"; + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping(plainText); + assertSame(plainText, mo.getPlainTextContent()); + assertNull(mo.getMarkupContent()); // Not the MO's duty to calculate it! + } + + @Test + public void testFromMarkup() throws TemplateModelException { + String markup = "a&b"; + TemplateHTMLOutputModel mo = INSTANCE.fromMarkup(markup); + assertSame(markup, mo.getMarkupContent()); + assertNull(mo.getPlainTextContent()); // Not the MO's duty to calculate it! + } + + @Test + public void testGetMarkup() throws TemplateModelException { + { + String markup = "a&b"; + TemplateHTMLOutputModel mo = INSTANCE.fromMarkup(markup); + assertSame(markup, INSTANCE.getMarkupString(mo)); + } + + { + String safe = "abc"; + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping(safe); + assertSame(safe, INSTANCE.getMarkupString(mo)); + } + { + String safe = ""; + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping(safe); + assertSame(safe, INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("<abc"); + assertEquals("<abc", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("abc>"); + assertEquals("abc>", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("<abc>"); + assertEquals("<abc>", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("a&bc"); + assertEquals("a&bc", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("a&b&c"); + assertEquals("a&b&c", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("a<&>b&c"); + assertEquals("a<&>b&c", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("\"<a<&>b&c>\""); + assertEquals(""<a<&>b&c>"", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("<"); + assertEquals("<", INSTANCE.getMarkupString(mo)); + } + { + TemplateHTMLOutputModel mo = INSTANCE.fromPlainTextByEscaping("'"); + String mc = INSTANCE.getMarkupString(mo); + assertEquals("'", mc); + assertSame(mc, INSTANCE.getMarkupString(mo)); // cached + } + } + + @Test + public void testConcat() throws Exception { + assertMO( + "ab", null, + INSTANCE.concat(new TemplateHTMLOutputModel("a", null), new TemplateHTMLOutputModel("b", null))); + assertMO( + null, "ab", + INSTANCE.concat(new TemplateHTMLOutputModel(null, "a"), new TemplateHTMLOutputModel(null, "b"))); + assertMO( + null, "<a><b>", + INSTANCE.concat(new TemplateHTMLOutputModel(null, "<a>"), new TemplateHTMLOutputModel("<b>", null))); + assertMO( + null, "<a><b>", + INSTANCE.concat(new TemplateHTMLOutputModel("<a>", null), new TemplateHTMLOutputModel(null, "<b>"))); + } + + @Test + public void testEscaplePlainText() { + assertEquals("", INSTANCE.escapePlainText("")); + assertEquals("a", INSTANCE.escapePlainText("a")); + assertEquals("<a&b'c"d>", INSTANCE.escapePlainText("<a&b'c\"d>")); + assertEquals("a&b", INSTANCE.escapePlainText("a&b")); + assertEquals("<>", INSTANCE.escapePlainText("<>")); + } + + @Test + public void testIsEmpty() throws Exception { + assertTrue(INSTANCE.isEmpty(INSTANCE.fromMarkup(""))); + assertTrue(INSTANCE.isEmpty(INSTANCE.fromPlainTextByEscaping(""))); + assertFalse(INSTANCE.isEmpty(INSTANCE.fromMarkup(" "))); + assertFalse(INSTANCE.isEmpty(INSTANCE.fromPlainTextByEscaping(" "))); + } + + private void assertMO(String pc, String mc, TemplateHTMLOutputModel mo) { + assertEquals(pc, mo.getPlainTextContent()); + assertEquals(mc, mo.getMarkupContent()); + } + + @Test + public void testGetMimeType() { + assertEquals("text/html", INSTANCE.getMimeType()); + } + +}
