http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java new file mode 100644 index 0000000..05a4d91 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormat.java @@ -0,0 +1,270 @@ +/* + * 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.valueformat.impl; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._StringUtil; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.valueformat.UnparsableValueException; + +abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat { + + private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE + = "Less than seconds accuracy isn't allowed by the XML Schema format"; + private final ISOLikeTemplateDateFormatFactory factory; + private final Environment env; + protected final int dateType; + protected final boolean zonelessInput; + protected final TimeZone timeZone; + protected final Boolean forceUTC; + protected final Boolean showZoneOffset; + protected final int accuracy; + + /** + * @param formatString The value of the ..._format setting, like "iso nz". + * @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has + * indicated the exact formatter class (like "iso" or "xs") + */ + public ISOLikeTemplateDateFormat( + final String formatString, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + this.factory = factory; + this.env = env; + if (dateType == TemplateDateModel.UNKNOWN) { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + + this.dateType = dateType; + this.zonelessInput = zonelessInput; + + final int ln = formatString.length(); + boolean afterSeparator = false; + int i = parsingStart; + int accuracy = _DateUtil.ACCURACY_MILLISECONDS; + Boolean showZoneOffset = null; + Boolean forceUTC = Boolean.FALSE; + while (i < ln) { + final char c = formatString.charAt(i++); + if (c == '_' || c == ' ') { + afterSeparator = true; + } else { + if (!afterSeparator) { + throw new InvalidFormatParametersException( + "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ")."); + } + + switch (c) { + case 'h': + case 'm': + case 's': + if (accuracy != _DateUtil.ACCURACY_MILLISECONDS) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as accuracy was already specified earlier " + + "(at char pos. " + i + ")."); + } + switch (c) { + case 'h': + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_HOURS; + break; + case 'm': + if (i < ln && formatString.charAt(i) == 's') { + i++; + accuracy = _DateUtil.ACCURACY_MILLISECONDS_FORCED; + } else { + if (isXSMode()) { + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); + } + accuracy = _DateUtil.ACCURACY_MINUTES; + } + break; + case 's': + accuracy = _DateUtil.ACCURACY_SECONDS; + break; + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'u') { + checkForceUTCNotSet(forceUTC); + i++; + forceUTC = Boolean.TRUE; + break; + } + // Falls through + case 'n': + if (showZoneOffset != null) { + throw new InvalidFormatParametersException( + "Character \"" + c + "\" is unexpected as zone offset visibility was already " + + "specified earlier. (at char pos. " + i + ")."); + } + switch (c) { + case 'n': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.FALSE; + } else { + throw new InvalidFormatParametersException( + "\"n\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + case 'f': + if (i < ln && formatString.charAt(i) == 'z') { + i++; + showZoneOffset = Boolean.TRUE; + } else { + throw new InvalidFormatParametersException( + "\"f\" must be followed by \"z\" (at char pos. " + i + ")."); + } + break; + } + break; + case 'u': + checkForceUTCNotSet(forceUTC); + forceUTC = null; // means UTC will be used except for zonelessInput + break; + default: + throw new InvalidFormatParametersException( + "Unexpected character, " + _StringUtil.jQuote(String.valueOf(c)) + + ". Expected the beginning of one of: h, m, s, ms, nz, fz, u" + + " (at char pos. " + i + ")."); + } // switch + afterSeparator = false; + } // else + } // while + + this.accuracy = accuracy; + this.showZoneOffset = showZoneOffset; + this.forceUTC = forceUTC; + this.timeZone = timeZone; + } + + private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException { + if (fourceUTC != Boolean.FALSE) { + throw new InvalidFormatParametersException( + "The UTC usage option was already set earlier."); + } + } + + @Override + public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException { + final Date date = TemplateFormatUtil.getNonNullDate(dateModel); + return format( + date, + dateType != TemplateDateModel.TIME, + dateType != TemplateDateModel.DATE, + showZoneOffset == null + ? !zonelessInput + : showZoneOffset.booleanValue(), + accuracy, + (forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? _DateUtil.UTC : timeZone, + factory.getISOBuiltInCalendar(env)); + } + + protected abstract String format(Date date, + boolean datePart, boolean timePart, boolean offsetPart, + int accuracy, + TimeZone timeZone, + DateToISO8601CalendarFactory calendarFactory); + + @Override + @SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN", + justification = "Known to use the singleton Boolean-s only") + public final Date parse(String s, int dateType) throws UnparsableValueException { + CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env); + TimeZone tz = forceUTC != Boolean.FALSE ? _DateUtil.UTC : timeZone; + try { + if (dateType == TemplateDateModel.DATE) { + return parseDate(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.TIME) { + return parseTime(s, tz, calToDateConverter); + } else if (dateType == TemplateDateModel.DATETIME) { + return parseDateTime(s, tz, calToDateConverter); + } else { + throw new BugException("Unexpected date type: " + dateType); + } + } catch (DateParseException e) { + throw new UnparsableValueException(e.getMessage(), e); + } + } + + protected abstract Date parseDate( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + protected abstract Date parseDateTime( + String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException; + + @Override + public final String getDescription() { + switch (dateType) { + case TemplateDateModel.DATE: return getDateDescription(); + case TemplateDateModel.TIME: return getTimeDescription(); + case TemplateDateModel.DATETIME: return getDateTimeDescription(); + default: return "<error: wrong format dateType>"; + } + } + + protected abstract String getDateDescription(); + protected abstract String getTimeDescription(); + protected abstract String getDateTimeDescription(); + + @Override + public final boolean isLocaleBound() { + return false; + } + + @Override + public boolean isTimeZoneBound() { + return true; + } + + protected abstract boolean isXSMode(); + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java new file mode 100644 index 0000000..83349d8 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOLikeTemplateDateFormatFactory.java @@ -0,0 +1,54 @@ +/* + * 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.valueformat.impl; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.util._DateUtil.TrivialCalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.TrivialDateToISO8601CalendarFactory; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; + +abstract class ISOLikeTemplateDateFormatFactory extends TemplateDateFormatFactory { + + private static final Object DATE_TO_CAL_CONVERTER_KEY = new Object(); + private static final Object CAL_TO_DATE_CONVERTER_KEY = new Object(); + + protected ISOLikeTemplateDateFormatFactory() { } + + public DateToISO8601CalendarFactory getISOBuiltInCalendar(Environment env) { + DateToISO8601CalendarFactory r = (DateToISO8601CalendarFactory) env.getCustomState(DATE_TO_CAL_CONVERTER_KEY); + if (r == null) { + r = new TrivialDateToISO8601CalendarFactory(); + env.setCustomState(DATE_TO_CAL_CONVERTER_KEY, r); + } + return r; + } + + public CalendarFieldsToDateConverter getCalendarFieldsToDateCalculator(Environment env) { + CalendarFieldsToDateConverter r = (CalendarFieldsToDateConverter) env.getCustomState(CAL_TO_DATE_CONVERTER_KEY); + if (r == null) { + r = new TrivialCalendarFieldsToDateConverter(); + env.setCustomState(CAL_TO_DATE_CONVERTER_KEY, r); + } + return r; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java new file mode 100644 index 0000000..4341755 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormat.java @@ -0,0 +1,90 @@ +/* + * 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.valueformat.impl; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat { + + ISOTemplateDateFormat( + String settingValue, int parsingStart, + int dateType, boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, + Environment env) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { + super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env); + } + + @Override + protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, + TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { + return _DateUtil.dateToISO8601String( + date, datePart, timePart, timePart && offsetPart, accuracy, timeZone, calendarFactory); + } + + @Override + protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Date(s, tz, calToDateConverter); + } + + @Override + protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseISO8601Time(s, tz, calToDateConverter); + } + + @Override + protected Date parseDateTime(String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { + return _DateUtil.parseISO8601DateTime(s, tz, calToDateConverter); + } + + @Override + protected String getDateDescription() { + return "ISO 8601 (subset) date"; + } + + @Override + protected String getTimeDescription() { + return "ISO 8601 (subset) time"; + } + + @Override + protected String getDateTimeDescription() { + return "ISO 8601 (subset) date-time"; + } + + @Override + protected boolean isXSMode() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java new file mode 100644 index 0000000..d25e754 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/ISOTemplateDateFormatFactory.java @@ -0,0 +1,56 @@ +/* + * 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.valueformat.impl; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +import java.util.Locale; +import java.util.TimeZone; + +/** + * Creates {@link TemplateDateFormat}-s that follows ISO 8601 extended format that is also compatible with the XML + * Schema format (as far as you don't have dates in the BC era). Examples of possible outputs: {@code + * "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"}, {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone + * offset; this is not required by ISO 8601, but included for compatibility with the XML Schema format. Regarding the + * B.C. issue, those dates will be one year off when read back according the XML Schema format, because of a mismatch + * between that format and ISO 8601:2000 Second Edition. + */ +public final class ISOTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory { + + public static final ISOTemplateDateFormatFactory INSTANCE = new ISOTemplateDateFormatFactory(); + + private ISOTemplateDateFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching) + return new ISOTemplateDateFormat( + params, 3, + dateType, zonelessInput, + timeZone, this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java new file mode 100644 index 0000000..99ad68c --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormat.java @@ -0,0 +1,75 @@ +/* + * 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.valueformat.impl; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.UnparsableValueException; + +/** + * Java {@link DateFormat}-based format. + */ +class JavaTemplateDateFormat extends TemplateDateFormat { + + private final DateFormat javaDateFormat; + + public JavaTemplateDateFormat(DateFormat javaDateFormat) { + this.javaDateFormat = javaDateFormat; + } + + @Override + public String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException { + return javaDateFormat.format(TemplateFormatUtil.getNonNullDate(dateModel)); + } + + @Override + public Date parse(String s, int dateType) throws UnparsableValueException { + try { + return javaDateFormat.parse(s); + } catch (ParseException e) { + throw new UnparsableValueException(e.getMessage(), e); + } + } + + @Override + public String getDescription() { + return javaDateFormat instanceof SimpleDateFormat + ? ((SimpleDateFormat) javaDateFormat).toPattern() + : javaDateFormat.toString(); + } + + @Override + public boolean isLocaleBound() { + return true; + } + + @Override + public boolean isTimeZoneBound() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.java new file mode 100644 index 0000000..064eabd --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateDateFormatFactory.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.valueformat.impl; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.slf4j.Logger; + +/** + * Deals with {@link TemplateDateFormat}-s that wrap a Java {@link DateFormat}. The parameter string is usually a + * {@link java.text.SimpleDateFormat} pattern, but it also recognized the names "short", "medium", "long" + * and "full", which correspond to formats defined by {@link DateFormat} with similar names. + * + * <p>Note that the resulting {@link java.text.SimpleDateFormat}-s are globally cached, and threading issues are + * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't + * eliminate it. + */ +public class JavaTemplateDateFormatFactory extends TemplateDateFormatFactory { + + public static final JavaTemplateDateFormatFactory INSTANCE = new JavaTemplateDateFormatFactory(); + + private static final Logger LOG = _CoreLogs.RUNTIME; + + private static final ConcurrentHashMap<CacheKey, DateFormat> GLOBAL_FORMAT_CACHE = new ConcurrentHashMap<>(); + private static final int LEAK_ALERT_DATE_FORMAT_CACHE_SIZE = 1024; + + private JavaTemplateDateFormatFactory() { + // Can't be instantiated + } + + /** + * @param zonelessInput + * Has no effect in this implementation. + */ + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + return new JavaTemplateDateFormat(getJavaDateFormat(dateType, params, locale, timeZone)); + } + + /** + * Returns a "private" copy (not in the global cache) for the given format. + */ + private DateFormat getJavaDateFormat(int dateType, String nameOrPattern, Locale locale, TimeZone timeZone) + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + + // Get DateFormat from global cache: + CacheKey cacheKey = new CacheKey(dateType, nameOrPattern, locale, timeZone); + DateFormat jFormat; + + jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey); + if (jFormat == null) { + // Add format to global format cache. + StringTokenizer tok = new StringTokenizer(nameOrPattern, "_"); + int tok1Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : DateFormat.DEFAULT; + if (tok1Style != -1) { + switch (dateType) { + case TemplateDateModel.UNKNOWN: { + throw new UnknownDateTypeFormattingUnsupportedException(); + } + case TemplateDateModel.TIME: { + jFormat = DateFormat.getTimeInstance(tok1Style, cacheKey.locale); + break; + } + case TemplateDateModel.DATE: { + jFormat = DateFormat.getDateInstance(tok1Style, cacheKey.locale); + break; + } + case TemplateDateModel.DATETIME: { + int tok2Style = tok.hasMoreTokens() ? parseDateStyleToken(tok.nextToken()) : tok1Style; + if (tok2Style != -1) { + jFormat = DateFormat.getDateTimeInstance(tok1Style, tok2Style, cacheKey.locale); + } + break; + } + } + } + if (jFormat == null) { + try { + jFormat = new SimpleDateFormat(nameOrPattern, cacheKey.locale); + } catch (IllegalArgumentException e) { + final String msg = e.getMessage(); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid SimpleDateFormat pattern", e); + } + } + jFormat.setTimeZone(cacheKey.timeZone); + + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) { + boolean triggered = false; + synchronized (JavaTemplateDateFormatFactory.class) { + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_DATE_FORMAT_CACHE_SIZE) { + triggered = true; + GLOBAL_FORMAT_CACHE.clear(); + } + } + if (triggered) { + LOG.warn("Global Java DateFormat cache has exceeded {} entries => cache flushed. " + + "Typical cause: Some template generates high variety of format pattern strings.", + LEAK_ALERT_DATE_FORMAT_CACHE_SIZE); + } + } + + DateFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat); + if (prevJFormat != null) { + jFormat = prevJFormat; + } + } // if cache miss + + return (DateFormat) jFormat.clone(); // For thread safety + } + + private static final class CacheKey { + private final int dateType; + private final String pattern; + private final Locale locale; + private final TimeZone timeZone; + + CacheKey(int dateType, String pattern, Locale locale, TimeZone timeZone) { + this.dateType = dateType; + this.pattern = pattern; + this.locale = locale; + this.timeZone = timeZone; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CacheKey) { + CacheKey fk = (CacheKey) o; + return dateType == fk.dateType && fk.pattern.equals(pattern) && fk.locale.equals(locale) + && fk.timeZone.equals(timeZone); + } + return false; + } + + @Override + public int hashCode() { + return dateType ^ pattern.hashCode() ^ locale.hashCode() ^ timeZone.hashCode(); + } + } + + private int parseDateStyleToken(String token) { + if ("short".equals(token)) { + return DateFormat.SHORT; + } + if ("medium".equals(token)) { + return DateFormat.MEDIUM; + } + if ("long".equals(token)) { + return DateFormat.LONG; + } + if ("full".equals(token)) { + return DateFormat.FULL; + } + return -1; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java new file mode 100644 index 0000000..26ee41c --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormat.java @@ -0,0 +1,64 @@ +/* + * 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.valueformat.impl; + +import java.text.NumberFormat; + +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.UnformattableValueException; + +final class JavaTemplateNumberFormat extends TemplateNumberFormat { + + private final String formatString; + private final NumberFormat javaNumberFormat; + + public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatString) { + this.formatString = formatString; + this.javaNumberFormat = javaNumberFormat; + } + + @Override + public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException, TemplateModelException { + Number number = TemplateFormatUtil.getNonNullNumber(numberModel); + try { + return javaNumberFormat.format(number); + } catch (ArithmeticException e) { + throw new UnformattableValueException( + "This format can't format the " + number + " number. Reason: " + e.getMessage(), e); + } + } + + @Override + public boolean isLocaleBound() { + return true; + } + + public NumberFormat getJavaNumberFormat() { + return javaNumberFormat; + } + + @Override + public String getDescription() { + return formatString; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java new file mode 100644 index 0000000..f511b81 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/JavaTemplateNumberFormatFactory.java @@ -0,0 +1,133 @@ +/* + * 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.valueformat.impl; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; +import org.slf4j.Logger; + +/** + * Deals with {@link TemplateNumberFormat}-s that wrap a Java {@link NumberFormat}. The parameter string is usually + * a {@link java.text.DecimalFormat} pattern, with the extensions described in the Manual (see "Extended Jav decimal + * format"). There are some names that aren't parsed as patterns: "number", "currency", "percent", which + * corresponds to the predefined formats with similar name in {@link NumberFormat}-s, and also "computer" that + * behaves like {@code someNumber?c} in templates. + * + * <p>Note that the resulting {@link java.text.DecimalFormat}-s are globally cached, and threading issues are + * addressed by cloning the cached instance before returning it. So it just makes object creation faster, but doesn't + * eliminate it. + */ +public class JavaTemplateNumberFormatFactory extends TemplateNumberFormatFactory { + + public static final JavaTemplateNumberFormatFactory INSTANCE = new JavaTemplateNumberFormatFactory(); + + private static final Logger LOG = _CoreLogs.RUNTIME; + + private static final ConcurrentHashMap<CacheKey, NumberFormat> GLOBAL_FORMAT_CACHE + = new ConcurrentHashMap<>(); + private static final int LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE = 1024; + + private JavaTemplateNumberFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateNumberFormat get(String params, Locale locale, Environment env) + throws InvalidFormatParametersException { + CacheKey cacheKey = new CacheKey(params, locale); + NumberFormat jFormat = GLOBAL_FORMAT_CACHE.get(cacheKey); + if (jFormat == null) { + if ("number".equals(params)) { + jFormat = NumberFormat.getNumberInstance(locale); + } else if ("currency".equals(params)) { + jFormat = NumberFormat.getCurrencyInstance(locale); + } else if ("percent".equals(params)) { + jFormat = NumberFormat.getPercentInstance(locale); + } else if ("computer".equals(params)) { + jFormat = env.getCNumberFormat(); + } else { + try { + jFormat = ExtendedDecimalFormatParser.parse(params, locale); + } catch (ParseException e) { + String msg = e.getMessage(); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid DecimalFormat pattern", e); + } + } + + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) { + boolean triggered = false; + synchronized (JavaTemplateNumberFormatFactory.class) { + if (GLOBAL_FORMAT_CACHE.size() >= LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE) { + triggered = true; + GLOBAL_FORMAT_CACHE.clear(); + } + } + if (triggered) { + LOG.warn("Global Java NumberFormat cache has exceeded {} entries => cache flushed. " + + "Typical cause: Some template generates high variety of format pattern strings.", + LEAK_ALERT_NUMBER_FORMAT_CACHE_SIZE); + } + } + + NumberFormat prevJFormat = GLOBAL_FORMAT_CACHE.putIfAbsent(cacheKey, jFormat); + if (prevJFormat != null) { + jFormat = prevJFormat; + } + } // if cache miss + + // JFormat-s aren't thread-safe; must clone it + jFormat = (NumberFormat) jFormat.clone(); + + return new JavaTemplateNumberFormat(jFormat, params); + } + + private static final class CacheKey { + private final String pattern; + private final Locale locale; + + CacheKey(String pattern, Locale locale) { + this.pattern = pattern; + this.locale = locale; + } + + @Override + public boolean equals(Object o) { + if (o instanceof CacheKey) { + CacheKey fk = (CacheKey) o; + return fk.pattern.equals(pattern) && fk.locale.equals(locale); + } + return false; + } + + @Override + public int hashCode() { + return pattern.hashCode() ^ locale.hashCode(); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java new file mode 100644 index 0000000..4ca415d --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormat.java @@ -0,0 +1,94 @@ +/* + * 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.valueformat.impl; + +import java.util.Date; +import java.util.TimeZone; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.util._DateUtil; +import org.apache.freemarker.core.util._DateUtil.CalendarFieldsToDateConverter; +import org.apache.freemarker.core.util._DateUtil.DateParseException; +import org.apache.freemarker.core.util._DateUtil.DateToISO8601CalendarFactory; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +/** + * XML Schema format. + */ +class XSTemplateDateFormat extends ISOLikeTemplateDateFormat { + + XSTemplateDateFormat( + String settingValue, int parsingStart, + int dateType, + boolean zonelessInput, + TimeZone timeZone, + ISOLikeTemplateDateFormatFactory factory, + Environment env) + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory, env); + } + + @Override + protected String format(Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, + TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { + return _DateUtil.dateToXSString( + date, datePart, timePart, offsetPart, accuracy, timeZone, calendarFactory); + } + + @Override + protected Date parseDate(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseXSDate(s, tz, calToDateConverter); + } + + @Override + protected Date parseTime(String s, TimeZone tz, CalendarFieldsToDateConverter calToDateConverter) + throws DateParseException { + return _DateUtil.parseXSTime(s, tz, calToDateConverter); + } + + @Override + protected Date parseDateTime(String s, TimeZone tz, + CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { + return _DateUtil.parseXSDateTime(s, tz, calToDateConverter); + } + + @Override + protected String getDateDescription() { + return "W3C XML Schema date"; + } + + @Override + protected String getTimeDescription() { + return "W3C XML Schema time"; + } + + @Override + protected String getDateTimeDescription() { + return "W3C XML Schema dateTime"; + } + + @Override + protected boolean isXSMode() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.java new file mode 100644 index 0000000..e9caa40 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/valueformat/impl/XSTemplateDateFormatFactory.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.valueformat.impl; + +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +import java.util.Locale; +import java.util.TimeZone; + +/** + * Creates {@link TemplateDateFormat}-s that follows the W3C XML Schema date, time and dateTime syntax. + */ +public final class XSTemplateDateFormatFactory extends ISOLikeTemplateDateFormatFactory { + + public static final XSTemplateDateFormatFactory INSTANCE = new XSTemplateDateFormatFactory(); + + private XSTemplateDateFormatFactory() { + // Not meant to be instantiated + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { + // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching) + return new XSTemplateDateFormat( + params, 2, + dateType, zonelessInput, + timeZone, this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/manual/en_US/FM3-CHANGE-LOG.txt ---------------------------------------------------------------------- diff --git a/src/manual/en_US/FM3-CHANGE-LOG.txt b/src/manual/en_US/FM3-CHANGE-LOG.txt index a471514..94b62ad 100644 --- a/src/manual/en_US/FM3-CHANGE-LOG.txt +++ b/src/manual/en_US/FM3-CHANGE-LOG.txt @@ -71,6 +71,7 @@ the FreeMarer 3 changelog here: to org.apache.freemarker.core.templateresolver (because later we will have a class called TemplateResolver, which is the central class of loading and caching and template name rules). OutputFormat realted classes were moved to org.apache.freemarker.core.outputformat. + ValueFormat related classes were moved to org.apache.freemarker.core.valueformat. freemarker.ext.beans were moved under org.apache.freemarker.core.model.impl.beans for now (but later we only want a DefaultObject wrapper, no BeansWrapper, so this will change) and freemarker.ext.dom was moved to org.apache.freemarker.core.model.impl.dom. http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/ConfigurationTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/ConfigurationTest.java b/src/test/java/org/apache/freemarker/core/ConfigurationTest.java index cd8594b..93bef63 100644 --- a/src/test/java/org/apache/freemarker/core/ConfigurationTest.java +++ b/src/test/java/org/apache/freemarker/core/ConfigurationTest.java @@ -85,6 +85,8 @@ import org.apache.freemarker.core.util._DateUtil; import org.apache.freemarker.core.util._NullArgumentException; import org.apache.freemarker.core.util._NullWriter; import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; import org.junit.Test; import com.google.common.collect.ImmutableList; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/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 index 3e7cb41..8bcca28 100644 --- a/src/test/java/org/apache/freemarker/core/DateFormatTest.java +++ b/src/test/java/org/apache/freemarker/core/DateFormatTest.java @@ -41,6 +41,10 @@ import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactor import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory; import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory; import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException; import org.apache.freemarker.test.TemplateTest; import org.junit.Before; import org.junit.Test; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/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 deleted file mode 100644 index cc9e136..0000000 --- a/src/test/java/org/apache/freemarker/core/ExtendedDecimalFormatTest.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - * 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.containsStringIgnoringCase; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.text.DecimalFormat; -import java.text.ParseException; -import java.util.Locale; - -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, "A@.E@"); - - 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/ef968757/src/test/java/org/apache/freemarker/core/NumberFormatTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/NumberFormatTest.java b/src/test/java/org/apache/freemarker/core/NumberFormatTest.java deleted file mode 100644 index 0d5650a..0000000 --- a/src/test/java/org/apache/freemarker/core/NumberFormatTest.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * 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.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; - -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.TemplateNumberModel; -import org.apache.freemarker.core.model.impl.SimpleNumber; -import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory; -import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher; -import org.apache.freemarker.core.userpkg.BaseNTemplateNumberFormatFactory; -import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory; -import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory; -import org.apache.freemarker.core.userpkg.PrintfGTemplateNumberFormatFactory; -import org.apache.freemarker.test.TemplateTest; -import org.junit.Before; -import org.junit.Test; - -import com.google.common.collect.ImmutableMap; - -@SuppressWarnings("boxing") -public class NumberFormatTest extends TemplateTest { - - @Before - public void setup() { - Configuration cfg = getConfiguration(); - cfg.setLocale(Locale.US); - - cfg.setCustomNumberFormats(ImmutableMap.of( - "hex", HexTemplateNumberFormatFactory.INSTANCE, - "loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE, - "base", BaseNTemplateNumberFormatFactory.INSTANCE, - "printfG", PrintfGTemplateNumberFormatFactory.INSTANCE)); - } - - @Test - public void testUnknownCustomFormat() throws Exception { - { - getConfiguration().setNumberFormat("@noSuchFormat"); - Throwable exc = assertErrorContains("${1}", "\"@noSuchFormat\"", "\"noSuchFormat\""); - assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class)); - } - - { - getConfiguration().setNumberFormat("number"); - Throwable exc = assertErrorContains("${1?string('@noSuchFormat2')}", - "\"@noSuchFormat2\"", "\"noSuchFormat2\""); - assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class)); - } - } - - @Test - public void testStringBI() throws Exception { - assertOutput("${11} ${11?string.@hex} ${12} ${12?string.@hex}", "11 b 12 c"); - } - - @Test - public void testSetting() throws Exception { - getConfiguration().setNumberFormat("@hex"); - assertOutput("${11?string.number} ${11} ${12?string.number} ${12}", "11 b 12 c"); - } - - @Test - public void testSetting2() throws Exception { - assertOutput( - "<#setting numberFormat='@hex'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}" - + "<#setting numberFormat='@loc'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}", - "11 b 12 c d" - + "11 11_en_US 12 12_en_US 13_en_US"); - } - - @Test - public void testUnformattableNumber() throws Exception { - getConfiguration().setNumberFormat("@hex"); - assertErrorContains("${1.1}", "hexadecimal int", "doesn't fit into an int"); - } - - @Test - public void testLocaleSensitive() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setNumberFormat("@loc"); - assertOutput("${1.1}", "1.1_en_US"); - cfg.setLocale(Locale.GERMANY); - assertOutput("${1.1}", "1.1_de_DE"); - } - - @Test - public void testLocaleSensitive2() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setNumberFormat("@loc"); - assertOutput("${1.1} <#setting locale='de_DE'>${1.1}", "1.1_en_US 1.1_de_DE"); - } - - @Test - public void testCustomParameterized() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setNumberFormat("@base 2"); - assertOutput("${11}", "1011"); - assertOutput("${11?string}", "1011"); - assertOutput("${11?string.@base_3}", "102"); - - assertErrorContains("${11?string.@base_xyz}", "\"@base_xyz\"", "\"xyz\""); - cfg.setNumberFormat("@base"); - assertErrorContains("${11}", "\"@base\"", "format parameter is required"); - } - - @Test - public void testCustomWithFallback() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setNumberFormat("@base 2|0.0#"); - assertOutput("${11}", "1011"); - assertOutput("${11.34}", "11.34"); - assertOutput("${11?string('@base 3|0.00')}", "102"); - assertOutput("${11.2?string('@base 3|0.00')}", "11.20"); - } - - @Test - public void testEnvironmentGetters() throws Exception { - Template t = new Template(null, "", getConfiguration()); - Environment env = t.createProcessingEnvironment(null, null); - - TemplateNumberFormat defF = env.getTemplateNumberFormat(); - // - TemplateNumberFormat explF = env.getTemplateNumberFormat("0.00"); - assertEquals("1.25", explF.formatToPlainText(new SimpleNumber(1.25))); - // - TemplateNumberFormat expl2F = env.getTemplateNumberFormat("@loc"); - assertEquals("1.25_en_US", expl2F.formatToPlainText(new SimpleNumber(1.25))); - - TemplateNumberFormat explFFr = env.getTemplateNumberFormat("0.00", Locale.FRANCE); - assertNotSame(explF, explFFr); - assertEquals("1,25", explFFr.formatToPlainText(new SimpleNumber(1.25))); - // - TemplateNumberFormat expl2FFr = env.getTemplateNumberFormat("@loc", Locale.FRANCE); - assertEquals("1.25_fr_FR", expl2FFr.formatToPlainText(new SimpleNumber(1.25))); - - assertSame(env.getTemplateNumberFormat(), defF); - // - assertSame(env.getTemplateNumberFormat("0.00"), explF); - // - assertSame(env.getTemplateNumberFormat("@loc"), expl2F); - } - - /** - * ?string formats lazily (at least in 2.3.x), so it must make a snapshot of the format inputs when it's called. - */ - @Test - public void testStringBIDoesSnapshot() throws Exception { - // TemplateNumberModel-s shouldn't change, but we have to keep BC when that still happens. - final MutableTemplateNumberModel nm = new MutableTemplateNumberModel(); - nm.setNumber(123); - addToDataModel("n", nm); - addToDataModel("incN", new TemplateDirectiveModel() { - - @Override - public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) - throws TemplateException, IOException { - nm.setNumber(nm.getAsNumber().intValue() + 1); - } - }); - assertOutput( - "<#assign s1 = n?string>" - + "<#setting numberFormat='@loc'>" - + "<#assign s2 = n?string>" - + "<#setting numberFormat='@hex'>" - + "<#assign s3 = n?string>" - + "${s1} ${s2} ${s3}", - "123 123_en_US 7b"); - assertOutput( - "<#assign s1 = n?string>" - + "<@incN />" - + "<#assign s2 = n?string>" - + "${s1} ${s2}", - "123 124"); - } - - @Test - public void testNullInModel() throws Exception { - addToDataModel("n", new MutableTemplateNumberModel()); - assertErrorContains("${n}", "nothing inside it"); - assertErrorContains("${n?string}", "nothing inside it"); - } - - @Test - public void testAtPrefix() throws Exception { - Configuration cfg = getConfiguration(); - - cfg.setNumberFormat("@hex"); - assertOutput("${10}", "a"); - cfg.setNumberFormat("'@'0"); - assertOutput("${10}", "@10"); - cfg.setNumberFormat("@@0"); - assertOutput("${10}", "@@10"); - - cfg.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>emptyMap()); - cfg.setNumberFormat("@hex"); - assertErrorContains("${10}", "custom", "\"hex\""); - cfg.setNumberFormat("'@'0"); - assertOutput("${10}", "@10"); - cfg.setNumberFormat("@@0"); - assertOutput("${10}", "@@10"); - } - - @Test - public void testAlieses() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setCustomNumberFormats(ImmutableMap.of( - "f", new AliasTemplateNumberFormatFactory("0.#'f'"), - "d", new AliasTemplateNumberFormatFactory("0.0#"), - "hex", HexTemplateNumberFormatFactory.INSTANCE)); - - TemplateConfiguration tc = new TemplateConfiguration(); - tc.setCustomNumberFormats(ImmutableMap.of( - "d", new AliasTemplateNumberFormatFactory("0.#'d'"), - "i", new AliasTemplateNumberFormatFactory("@hex"))); - cfg.setTemplateConfigurations(new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*2*"), tc)); - - String commonFtl = "${1?string.@f} ${1?string.@d} " - + "<#setting locale='fr_FR'>${1.5?string.@d} " - + "<#attempt>${10?string.@i}<#recover>E</#attempt>"; - addTemplate("t1.ftl", commonFtl); - addTemplate("t2.ftl", commonFtl); - - assertOutputForNamed("t1.ftl", "1f 1.0 1,5 E"); - assertOutputForNamed("t2.ftl", "1f 1d 1,5d a"); - } - - @Test - public void testAlieses2() throws Exception { - Configuration cfg = getConfiguration(); - cfg.setCustomNumberFormats(ImmutableMap.of( - "n", new AliasTemplateNumberFormatFactory("0.0", - ImmutableMap.of( - new Locale("en"), "0.0'_en'", - Locale.UK, "0.0'_en_GB'", - Locale.FRANCE, "0.0'_fr_FR'")))); - cfg.setNumberFormat("@n"); - assertOutput( - "<#setting locale='en_US'>${1} " - + "<#setting locale='en_GB'>${1} " - + "<#setting locale='en_GB_Win'>${1} " - + "<#setting locale='fr_FR'>${1} " - + "<#setting locale='hu_HU'>${1}", - "1.0_en 1.0_en_GB 1.0_en_GB 1,0_fr_FR 1,0"); - } - - @Test - public void testMarkupFormat() throws IOException, TemplateException { - getConfiguration().setNumberFormat("@printfG_3"); - - String commonFTL = "${1234567} ${'cat:' + 1234567} ${0.0000123}"; - String commonOutput = "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>"; - assertOutput(commonFTL, commonOutput); - assertOutput("<#ftl outputFormat='HTML'>" + commonFTL, commonOutput); - assertOutput("<#escape x as x?html>" + commonFTL + "</#escape>", commonOutput); - assertOutput("<#escape x as x?xhtml>" + commonFTL + "</#escape>", commonOutput); - assertOutput("<#escape x as x?xml>" + commonFTL + "</#escape>", commonOutput); - assertOutput("${\"" + commonFTL + "\"}", "1.23*10<sup>6</sup> cat:1.23*10<sup>6</sup> 1.23*10<sup>-5</sup>"); - assertErrorContains("<#ftl outputFormat='plainText'>" + commonFTL, "HTML", "plainText", "conversion"); - } - - @Test - public void testPrintG() throws IOException, TemplateException { - for (Number n : new Number[] { - 1234567, 1234567L, 1234567d, 1234567f, BigInteger.valueOf(1234567), BigDecimal.valueOf(1234567) }) { - addToDataModel("n", n); - - assertOutput("${n?string.@printfG}", "1.23457E+06"); - assertOutput("${n?string.@printfG_3}", "1.23E+06"); - assertOutput("${n?string.@printfG_7}", "1234567"); - assertOutput("${0.0000123?string.@printfG}", "1.23000E-05"); - } - } - - private static class MutableTemplateNumberModel implements TemplateNumberModel { - - private Number number; - - public void setNumber(Number number) { - this.number = number; - } - - @Override - public Number getAsNumber() throws TemplateModelException { - return number; - } - - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java index 6af8a25..1348ac0 100644 --- a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java +++ b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java @@ -61,6 +61,8 @@ import org.apache.freemarker.core.userpkg.HexTemplateNumberFormatFactory; import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory; import org.apache.freemarker.core.userpkg.LocaleSensitiveTemplateNumberFormatFactory; import org.apache.freemarker.core.util._NullArgumentException; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; import org.junit.Test; import com.google.common.collect.ImmutableList; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/ef968757/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java b/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java index be49664..4e82c71 100644 --- a/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java +++ b/src/test/java/org/apache/freemarker/core/userpkg/AppMetaTemplateDateFormatFactory.java @@ -23,13 +23,13 @@ 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.valueformat.InvalidFormatParametersException; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; +import org.apache.freemarker.core.valueformat.TemplateFormatUtil; +import org.apache.freemarker.core.valueformat.UnformattableValueException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; +import org.apache.freemarker.core.valueformat.UnparsableValueException; import org.apache.freemarker.core.model.TemplateDateModel; import org.apache.freemarker.core.model.TemplateModelException;