blob: 792b9b274d3cbb13c69ccd08de5f7afda545a110 [file] [log] [blame]
/*
* 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 freemarker.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
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.junit.Before;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
import freemarker.cache.ConditionalTemplateConfigurationFactory;
import freemarker.cache.FileNameGlobMatcher;
import freemarker.template.Configuration;
import freemarker.template.SimpleDate;
import freemarker.template.Template;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModelException;
import freemarker.test.TemplateTest;
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_2_3_24);
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));
testIcIAndEscapingWhenCustFormsAreAccepted(cfg);
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_23);
testIcIAndEscapingWhenCustFormsAreAccepted(cfg);
cfg.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
cfg.setDateTimeFormat("@epoch");
assertErrorContains("${d}", "\"@epoch\"");
cfg.setDateTimeFormat("'@'yyyy");
assertOutput("${d}", "@1970");
cfg.setDateTimeFormat("@@yyyy");
assertOutput("${d}", "@@1970");
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_24);
cfg.setDateTimeFormat("@epoch");
assertErrorContains("${d}", "custom", "\"epoch\"");
cfg.setDateTimeFormat("'@'yyyy");
assertOutput("${d}", "@1970");
cfg.setDateTimeFormat("@@yyyy");
assertOutput("${d}", "@@1970");
}
protected void testIcIAndEscapingWhenCustFormsAreAccepted(Configuration cfg) throws IOException, TemplateException {
cfg.setDateTimeFormat("@epoch");
assertOutput("${d}", "12345678");
cfg.setDateTimeFormat("'@'yyyy");
assertOutput("${d}", "@1970");
cfg.setDateTimeFormat("@@yyyy");
assertOutput("${d}", "@@1970");
}
@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;
}
public Date getAsDate() throws TemplateModelException {
return date;
}
public int getDateType() {
return DATETIME;
}
}
}