diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 0e3617ed..701d3332 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -1062,8 +1062,8 @@ String formatNumber(TemplateNumberModel number, Expression exp) throws TemplateE /** * Format number with the number format specified as the parameter. */ - String formatNumber(TemplateNumberModel number, String formatDesc, Expression exp) throws TemplateException { - return formatNumber(number, getTemplateNumberFormat(formatDesc, exp), exp); + String formatNumber(TemplateNumberModel number, String formatString, Expression exp) throws TemplateException { + return formatNumber(number, getTemplateNumberFormat(formatString, exp), exp); } String formatNumber(TemplateNumberModel number, TemplateNumberFormat format, Expression exp) @@ -1093,7 +1093,7 @@ String formatNumber(Number number, BackwardCompatibleTemplateNumberFormat format * * @since 2.3.24 */ - public TemplateNumberFormat getTemplateNumberFormat() throws InvalidFormatDescriptorException { + public TemplateNumberFormat getTemplateNumberFormat() throws InvalidFormatStringException { TemplateNumberFormat format = cachedTemplateNumberFormat; if (format == null) { format = getTemplateNumberFormat(getNumberFormat()); @@ -1103,38 +1103,43 @@ public TemplateNumberFormat getTemplateNumberFormat() throws InvalidFormatDescri } /** - * Returns the number format for the given format descriptor. The returned value shouldn't be stored for later - * reuse, as the returned instance can be different when this method is called later again, for example if the - * locale has been changed since then. + * Returns the number format for the given format string. The returned value shouldn't be stored for later reuse, as + * the returned instance can be different when this method is called later again, for example if the locale has been + * changed since then. + * + * @param formatString + * A string that you could also use as the value of the {@code numberFormat} configuration setting. * * @since 2.3.24 */ - public TemplateNumberFormat getTemplateNumberFormat(String formatDesc) throws InvalidFormatDescriptorException { + public TemplateNumberFormat getTemplateNumberFormat(String formatString) throws InvalidFormatStringException { if (cachedTemplateNumberFormats == null) { cachedTemplateNumberFormats = new HashMap(); } { - TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatDesc); + TemplateNumberFormat format = cachedTemplateNumberFormats.get(formatString); if (format != null) { return format; } } TemplateNumberFormat format; - int ln = formatDesc.length(); - char c; - if (ln > 0 && formatDesc.charAt(0) == '@') { - String name; + int ln = formatString.length(); + if (ln > 0 && formatString.charAt(0) == '@') { + final String name; + final String params; { int endIdx; - findNameEnd: for (endIdx = 1; endIdx < ln; endIdx++) { - c = formatDesc.charAt(endIdx); + findParamsStart: for (endIdx = 1; endIdx < ln; endIdx++) { + char c = formatString.charAt(endIdx); if (c == ' ' || c == '_') { - break findNameEnd; + endIdx++; + break findParamsStart; } } - name = formatDesc.substring(1, endIdx); + name = formatString.substring(1, endIdx); + params = endIdx != ln ? formatString.substring(endIdx) : ""; } LocalTemplateNumberFormatFactory localFormatFactory = @@ -1142,8 +1147,8 @@ public TemplateNumberFormat getTemplateNumberFormat(String formatDesc) throws In if (localFormatFactory == null) { TemplateNumberFormatFactory formatFactory = getCustomNumberFormat(name); if (formatFactory == null) { - throw new InvalidFormatDescriptorException( - "No custom number format was defined with name " + StringUtil.jQuote(name), formatDesc); + throw new UndefinedCustomFormatException( + "No custom number format was defined with name " + StringUtil.jQuote(name)); } localFormatFactory = formatFactory.createLocalFactory(this, getLocale()); if (cachedLocalTemplateNumberFormatFactories == null) { @@ -1152,16 +1157,16 @@ public TemplateNumberFormat getTemplateNumberFormat(String formatDesc) throws In cachedLocalTemplateNumberFormatFactories.put(name, localFormatFactory); } - format = localFormatFactory.get(formatDesc); + format = localFormatFactory.get(params); } else { if (javaLocTempNumberFormatFactory == null) { javaLocTempNumberFormatFactory = new JavaLocalTemplateNumberFormatFactory(this); javaLocTempNumberFormatFactory.setLocale(getLocale()); } - format = javaLocTempNumberFormatFactory.get(formatDesc); + format = javaLocTempNumberFormatFactory.get(formatString); } - cachedTemplateNumberFormats.put(formatDesc, format); + cachedTemplateNumberFormats.put(formatString, format); return format; } @@ -1184,7 +1189,7 @@ TemplateNumberFormat getTemplateNumberFormat(Expression exp) TemplateNumberFormat format; try { format = getTemplateNumberFormat(); - } catch (InvalidFormatDescriptorException e) { + } catch (InvalidFormatStringException e) { throw new _MiscTemplateException(exp, e, this, "Failed to get number format object for the current number format string, ", new _DelayedJQuote(getNumberFormat()), "; see cause exception"); @@ -1192,14 +1197,14 @@ TemplateNumberFormat getTemplateNumberFormat(Expression exp) return format; } - TemplateNumberFormat getTemplateNumberFormat(String formatDesc, Expression exp) + TemplateNumberFormat getTemplateNumberFormat(String formatString, Expression exp) throws _MiscTemplateException { TemplateNumberFormat format; try { - format = getTemplateNumberFormat(formatDesc); - } catch (InvalidFormatDescriptorException e) { + format = getTemplateNumberFormat(formatString); + } catch (InvalidFormatStringException e) { throw new _MiscTemplateException(exp, e, this, - "Failed to get number format object for the ", new _DelayedJQuote(formatDesc), + "Failed to get number format object for the ", new _DelayedJQuote(formatString), " number format string; see cause exception"); } return format; @@ -1274,13 +1279,13 @@ String formatDate(TemplateDateModel tdm, Expression tdmSourceExpr) throws Templa } } - String formatDate(TemplateDateModel tdm, String formatDescriptor, Expression tdmSourceExpr) + String formatDate(TemplateDateModel tdm, String formatString, Expression tdmSourceExpr) throws TemplateModelException { Date date = EvalUtil.modelToDate(tdm, tdmSourceExpr); boolean isSQLDateOrTime = isSQLDateOrTimeClass(date.getClass()); try { return getTemplateDateFormat( - tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatDescriptor, null) + tdm.getDateType(), isSQLDateOrTime, shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, null) .format(tdm); } catch (UnknownDateTypeFormattingUnsupportedException e) { throw MessageUtil.newCantFormatUnknownTypeDateException(tdmSourceExpr, e); @@ -1353,19 +1358,19 @@ private TemplateDateFormat getTemplateDateFormat( } /** - * @param dateType {@link TemplateDateModel#UNKNOWN} is accepted or not depending on the {@code formatDescriptor} + * @param dateType {@link TemplateDateModel#UNKNOWN} is accepted or not depending on the {@code formatString} * value. When it isn't, a {@link TemplateModelException} will be thrown. * @param dateClass The exact class of the date object, such as {@link java.sql.Timestamp}. - * @param formatDescriptor Like "iso m" or "dd.MM.yyyy HH:mm" + * @param formatString Like "iso m" or "dd.MM.yyyy HH:mm" */ TemplateDateFormat getTemplateDateFormat( - int dateType, Class/**/ dateClass, String formatDescriptor, Expression dateSourceExpr) + int dateType, Class/**/ dateClass, String formatString, Expression dateSourceExpr) throws TemplateModelException { try { boolean isSQLDateOrTime = isSQLDateOrTimeClass(dateClass); return getTemplateDateFormat( dateType, isSQLDateOrTime, - shouldUseSQLDTTimeZone(isSQLDateOrTime), formatDescriptor, null); + shouldUseSQLDTTimeZone(isSQLDateOrTime), formatString, null); } catch (UnknownDateTypeFormattingUnsupportedException e) { throw MessageUtil.newCantFormatUnknownTypeDateException(dateSourceExpr, e); } @@ -1376,24 +1381,25 @@ TemplateDateFormat getTemplateDateFormat( * See the similar parameter of {@link LocalTemplateDateFormatFactory#get(int, boolean, String)} * @param zonelessInput * See the similar parameter of {@link LocalTemplateDateFormatFactory#get(int, boolean, String)} - * @param formatDescriptor + * @param formatString * The string that describes the date format. See the similar parameter of * {@link LocalTemplateDateFormatFactory#get(int, boolean, String)} - * @param formatDescriptorCfgSettingName - * The name of the configuration setting where the {@code formatDescriptor} comes from, or {@code null} - * if the format descriptor was specified directly for this formatting call. + * @param formatStringCfgSettingName + * The name of the configuration setting where the {@code formatZtring} comes from, or {@code null} + * if the format string was specified directly for this formatting call. */ private TemplateDateFormat getTemplateDateFormat( - int dateType, boolean zonelessInput, boolean useSQLDTTZ, String formatDescriptor, - String formatDescriptorCfgSettingName) + int dateType, boolean zonelessInput, boolean useSQLDTTZ, String formatString, + String formatStringCfgSettingName) throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException { - final int formatDescriptionLen = formatDescriptor.length(); + final int formatStringLen = formatString.length(); + final String formatParams; // As of Java 8, 'x' and 'i' (in lower case) are illegal date format letters, so this is backward-compatible. LocalTemplateDateFormatFactory templateDateFormatFactory; - if (formatDescriptionLen > 1 - && formatDescriptor.charAt(0) == 'x' - && formatDescriptor.charAt(1) == 's') { + if (formatStringLen > 1 + && formatString.charAt(0) == 'x' + && formatString.charAt(1) == 's') { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTXSLocTempDateFormatFactory : cachedXSLocTempDateFormatFactory; if (templateDateFormatFactory == null) { @@ -1407,10 +1413,11 @@ private TemplateDateFormat getTemplateDateFormat( cachedXSLocTempDateFormatFactory = (XSLocalTemplateDateFormatFactory) templateDateFormatFactory; } } - } else if (formatDescriptionLen > 2 - && formatDescriptor.charAt(0) == 'i' - && formatDescriptor.charAt(1) == 's' - && formatDescriptor.charAt(2) == 'o') { + formatParams = formatString; // for speed, we don't remove the prefix + } else if (formatStringLen > 2 + && formatString.charAt(0) == 'i' + && formatString.charAt(1) == 's' + && formatString.charAt(2) == 'o') { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTISOLocTempDateFormatFactory : cachedISOLocTempDateFormatFactory; if (templateDateFormatFactory == null) { @@ -1424,6 +1431,7 @@ private TemplateDateFormat getTemplateDateFormat( cachedISOLocTempDateFormatFactory = (ISOLocalTemplateDateFormatFactory) templateDateFormatFactory; } } + formatParams = formatString; // for speed, we don't remove the prefix } else { templateDateFormatFactory = useSQLDTTZ ? cachedSQLDTJavaLocTempDateFormatFactory : cachedJavaLocTempDateFormatFactory; @@ -1438,19 +1446,20 @@ private TemplateDateFormat getTemplateDateFormat( cachedJavaLocTempDateFormatFactory = (JavaLocalTemplateDateFormatFactory) templateDateFormatFactory; } } + formatParams = formatString; } try { - return templateDateFormatFactory.get(dateType, zonelessInput, formatDescriptor); - } catch (InvalidFormatDescriptorException e) { + return templateDateFormatFactory.get(dateType, zonelessInput, formatParams); + } catch (InvalidFormatParametersException e) { throw new _TemplateModelException(e.getCause(), - (formatDescriptorCfgSettingName == null - ? (Object) "Malformed date/time format descriptor: " + (formatStringCfgSettingName == null + ? (Object) "Malformed date/time format string: " : new Object[] { - "The value of the \"", formatDescriptorCfgSettingName, - "\" FreeMarker configuration setting is a malformed date/time format descriptor: " + "The value of the \"", formatStringCfgSettingName, + "\" FreeMarker configuration setting is a malformed date/time format string: " }), - new _DelayedJQuote(e.getFormatDescriptor()), ". Reason given: ", + new _DelayedJQuote(formatString), ". Reason given: ", e.getMessage()); } } diff --git a/src/main/java/freemarker/core/ISOLikeTemplateDateFormat.java b/src/main/java/freemarker/core/ISOLikeTemplateDateFormat.java index ab522f12..3a121f1f 100644 --- a/src/main/java/freemarker/core/ISOLikeTemplateDateFormat.java +++ b/src/main/java/freemarker/core/ISOLikeTemplateDateFormat.java @@ -40,16 +40,16 @@ abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat { protected final int accuracy; /** - * @param formatDesc The value of the ..._format setting, like "iso nz". + * @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 formatDesc, int parsingStart, + final String formatString, int parsingStart, int dateType, boolean zonelessInput, TimeZone timeZone, ISOLikeLocalTemplateDateFormatFactory factory) - throws InvalidFormatDescriptorException, UnknownDateTypeFormattingUnsupportedException { + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { this.factory = factory; if (dateType == TemplateDateModel.UNKNOWN) { throw new UnknownDateTypeFormattingUnsupportedException(); @@ -58,20 +58,20 @@ public ISOLikeTemplateDateFormat( this.dateType = dateType; this.zonelessInput = zonelessInput; - final int ln = formatDesc.length(); + 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 = formatDesc.charAt(i++); + final char c = formatString.charAt(i++); if (c == '_' || c == ' ') { afterSeparator = true; } else { if (!afterSeparator) { - throw new InvalidFormatDescriptorException( - "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ").", formatDesc); + throw new InvalidFormatParametersException( + "Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ")."); } switch (c) { @@ -79,27 +79,26 @@ public ISOLikeTemplateDateFormat( case 'm': case 's': if (accuracy != DateUtil.ACCURACY_MILLISECONDS) { - throw new InvalidFormatDescriptorException( + throw new InvalidFormatParametersException( "Character \"" + c + "\" is unexpected as accuracy was already specified earlier " - + "(at char pos. " + i + ").", - formatDesc); + + "(at char pos. " + i + ")."); } switch (c) { case 'h': if (isXSMode()) { - throw new InvalidFormatDescriptorException( - XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, formatDesc); + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); } accuracy = DateUtil.ACCURACY_HOURS; break; case 'm': - if (i < ln && formatDesc.charAt(i) == 's') { + if (i < ln && formatString.charAt(i) == 's') { i++; accuracy = DateUtil.ACCURACY_MILLISECONDS_FORCED; } else { if (isXSMode()) { - throw new InvalidFormatDescriptorException( - XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, formatDesc); + throw new InvalidFormatParametersException( + XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE); } accuracy = DateUtil.ACCURACY_MINUTES; } @@ -110,8 +109,8 @@ public ISOLikeTemplateDateFormat( } break; case 'f': - if (i < ln && formatDesc.charAt(i) == 'u') { - checkForceUTCNotSet(forceUTC, formatDesc); + if (i < ln && formatString.charAt(i) == 'u') { + checkForceUTCNotSet(forceUTC); i++; forceUTC = Boolean.TRUE; break; @@ -119,41 +118,40 @@ public ISOLikeTemplateDateFormat( // Falls through case 'n': if (showZoneOffset != null) { - throw new InvalidFormatDescriptorException( + throw new InvalidFormatParametersException( "Character \"" + c + "\" is unexpected as zone offset visibility was already " - + "specified earlier. (at char pos. " + i + ").", formatDesc); + + "specified earlier. (at char pos. " + i + ")."); } switch (c) { case 'n': - if (i < ln && formatDesc.charAt(i) == 'z') { + if (i < ln && formatString.charAt(i) == 'z') { i++; showZoneOffset = Boolean.FALSE; } else { - throw new InvalidFormatDescriptorException( - "\"n\" must be followed by \"z\" (at char pos. " + i + ").", formatDesc); + throw new InvalidFormatParametersException( + "\"n\" must be followed by \"z\" (at char pos. " + i + ")."); } break; case 'f': - if (i < ln && formatDesc.charAt(i) == 'z') { + if (i < ln && formatString.charAt(i) == 'z') { i++; showZoneOffset = Boolean.TRUE; } else { - throw new InvalidFormatDescriptorException( - "\"f\" must be followed by \"z\" (at char pos. " + i + ").", formatDesc); + throw new InvalidFormatParametersException( + "\"f\" must be followed by \"z\" (at char pos. " + i + ")."); } break; } break; case 'u': - checkForceUTCNotSet(forceUTC, formatDesc); + checkForceUTCNotSet(forceUTC); forceUTC = null; // means UTC will be used except for zonelessInput break; default: - throw new InvalidFormatDescriptorException( + 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 + ").", - formatDesc); + + " (at char pos. " + i + ")."); } // switch afterSeparator = false; } // else @@ -165,10 +163,10 @@ public ISOLikeTemplateDateFormat( this.timeZone = timeZone; } - private void checkForceUTCNotSet(Boolean fourceUTC, String formatDesc) throws InvalidFormatDescriptorException { + private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException { if (fourceUTC != Boolean.FALSE) { - throw new InvalidFormatDescriptorException( - "The UTC usage option was already set earlier.", formatDesc); + throw new InvalidFormatParametersException( + "The UTC usage option was already set earlier."); } } diff --git a/src/main/java/freemarker/core/ISOLocalTemplateDateFormatFactory.java b/src/main/java/freemarker/core/ISOLocalTemplateDateFormatFactory.java index 32a4e5a2..21f24800 100644 --- a/src/main/java/freemarker/core/ISOLocalTemplateDateFormatFactory.java +++ b/src/main/java/freemarker/core/ISOLocalTemplateDateFormatFactory.java @@ -23,11 +23,11 @@ public ISOLocalTemplateDateFormatFactory(Environment env) { } @Override - public TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor) - throws InvalidFormatDescriptorException, UnknownDateTypeFormattingUnsupportedException { + public TemplateDateFormat get(int dateType, boolean zonelessInput, String params) + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { // We don't cache these as creating them is cheap (only 10% speedup of ${d?string.xs} with caching) return new ISOTemplateDateFormat( - formatDescriptor, 3, + params, 3, dateType, zonelessInput, getTimeZone(), this); } diff --git a/src/main/java/freemarker/core/ISOTemplateDateFormat.java b/src/main/java/freemarker/core/ISOTemplateDateFormat.java index ec69248c..eef723b9 100644 --- a/src/main/java/freemarker/core/ISOTemplateDateFormat.java +++ b/src/main/java/freemarker/core/ISOTemplateDateFormat.java @@ -31,7 +31,7 @@ final class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat { int dateType, boolean zonelessInput, TimeZone timeZone, ISOLikeLocalTemplateDateFormatFactory factory) - throws InvalidFormatDescriptorException, UnknownDateTypeFormattingUnsupportedException { + throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException { super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory); } diff --git a/src/main/java/freemarker/core/InvalidFormatParametersException.java b/src/main/java/freemarker/core/InvalidFormatParametersException.java new file mode 100644 index 00000000..95d857a5 --- /dev/null +++ b/src/main/java/freemarker/core/InvalidFormatParametersException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky + * + * Licensed 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; + +/** + * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the parameters + * part of the format string (like some kind of pattern) is malformed. + * + * @since 2.3.24 + */ +public final class InvalidFormatParametersException extends InvalidFormatStringException { + + public InvalidFormatParametersException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidFormatParametersException(String message) { + this(message, null); + } + +} diff --git a/src/main/java/freemarker/core/InvalidFormatDescriptorException.java b/src/main/java/freemarker/core/InvalidFormatStringException.java similarity index 60% rename from src/main/java/freemarker/core/InvalidFormatDescriptorException.java rename to src/main/java/freemarker/core/InvalidFormatStringException.java index 13fbf697..034b2431 100644 --- a/src/main/java/freemarker/core/InvalidFormatDescriptorException.java +++ b/src/main/java/freemarker/core/InvalidFormatStringException.java @@ -17,26 +17,18 @@ /** * Used when creating {@link TemplateDateFormat}-s and {@link TemplateNumberFormat}-s to indicate that the format - * descriptor string (often some kind of pattern) is malformed. + * string (like the value of the {@code dateFormat} setting) is malformed. * * @since 2.3.24 */ -public class InvalidFormatDescriptorException extends Exception { +public abstract class InvalidFormatStringException extends Exception { - private final String formatDescriptor; - - public InvalidFormatDescriptorException(String message, String formatDescriptor, Throwable cause) { + public InvalidFormatStringException(String message, Throwable cause) { super(message, cause); - this.formatDescriptor = formatDescriptor; - } - - public InvalidFormatDescriptorException(String message, String formatDescriptor) { - this(message, formatDescriptor, null); } - - public String getFormatDescriptor() { - return formatDescriptor; + public InvalidFormatStringException(String message) { + this(message, null); } } diff --git a/src/main/java/freemarker/core/JavaLocalTemplateDateFormatFactory.java b/src/main/java/freemarker/core/JavaLocalTemplateDateFormatFactory.java index f5320c3c..68507b87 100644 --- a/src/main/java/freemarker/core/JavaLocalTemplateDateFormatFactory.java +++ b/src/main/java/freemarker/core/JavaLocalTemplateDateFormatFactory.java @@ -38,11 +38,12 @@ public JavaLocalTemplateDateFormatFactory(Environment env) { } /** - * @param zonelessInput Has no effect in this implementation. + * @param zonelessInput + * Has no effect in this implementation. */ @Override - public TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor) - throws InvalidFormatDescriptorException, TemplateModelException, + public TemplateDateFormat get(int dateType, boolean zonelessInput, String params) + throws InvalidFormatParametersException, TemplateModelException, UnknownDateTypeFormattingUnsupportedException { Map[] formatCache = this.formatCache; if (formatCache == null) { @@ -56,10 +57,10 @@ public TemplateDateFormat get(int dateType, boolean zonelessInput, String format formatCache[dateType] = formatsForDateType; } - TemplateDateFormat format = formatsForDateType.get(formatDescriptor); + TemplateDateFormat format = formatsForDateType.get(params); if (format == null) { - format = new JavaTemplateDateFormat(getJavaDateFormat(dateType, formatDescriptor)); - formatsForDateType.put(formatDescriptor, format); + format = new JavaTemplateDateFormat(getJavaDateFormat(dateType, params)); + formatsForDateType.put(params, format); } return format; } @@ -68,7 +69,7 @@ public TemplateDateFormat get(int dateType, boolean zonelessInput, String format * Returns a "private" copy (not in the global cache) for the given format. */ private DateFormat getJavaDateFormat(int dateType, String nameOrPattern) - throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatDescriptorException { + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { // Get DateFormat from global cache: DateFormatKey cacheKey = new DateFormatKey( @@ -107,9 +108,8 @@ private DateFormat getJavaDateFormat(int dateType, String nameOrPattern) jDateFormat = new SimpleDateFormat(nameOrPattern, cacheKey.locale); } catch (IllegalArgumentException e) { final String msg = e.getMessage(); - throw new InvalidFormatDescriptorException( - msg != null ? msg : "Invalid SimpleDateFormat pattern", - nameOrPattern, e); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid SimpleDateFormat pattern", e); } } jDateFormat.setTimeZone(cacheKey.timeZone); diff --git a/src/main/java/freemarker/core/JavaLocalTemplateNumberFormatFactory.java b/src/main/java/freemarker/core/JavaLocalTemplateNumberFormatFactory.java index d59b862f..892ab613 100644 --- a/src/main/java/freemarker/core/JavaLocalTemplateNumberFormatFactory.java +++ b/src/main/java/freemarker/core/JavaLocalTemplateNumberFormatFactory.java @@ -40,27 +40,27 @@ class JavaLocalTemplateNumberFormatFactory extends LocalTemplateNumberFormatFact } @Override - public TemplateNumberFormat get(String formatDesc) - throws InvalidFormatDescriptorException { + public TemplateNumberFormat get(String params) + throws InvalidFormatParametersException { Locale locale = getLocale(); - NumberFormatKey fk = new NumberFormatKey(formatDesc, locale); + NumberFormatKey fk = new NumberFormatKey(params, locale); NumberFormat jFormat = GLOBAL_NUMBER_FORMAT_CACHE.get(fk); if (jFormat == null) { - if ("number".equals(formatDesc)) { + if ("number".equals(params)) { jFormat = NumberFormat.getNumberInstance(locale); - } else if ("currency".equals(formatDesc)) { + } else if ("currency".equals(params)) { jFormat = NumberFormat.getCurrencyInstance(locale); - } else if ("percent".equals(formatDesc)) { + } else if ("percent".equals(params)) { jFormat = NumberFormat.getPercentInstance(locale); - } else if ("computer".equals(formatDesc)) { + } else if ("computer".equals(params)) { jFormat = getEnvironment().getCNumberFormat(); } else { try { - jFormat = new DecimalFormat(formatDesc, new DecimalFormatSymbols(locale)); + jFormat = new DecimalFormat(params, new DecimalFormatSymbols(locale)); } catch (IllegalArgumentException e) { String msg = e.getMessage(); - throw new InvalidFormatDescriptorException( - msg != null ? msg : "Invalid DecimalFormat pattern", formatDesc, e); + throw new InvalidFormatParametersException( + msg != null ? msg : "Invalid DecimalFormat pattern", e); } } @@ -87,7 +87,7 @@ public TemplateNumberFormat get(String formatDesc) // JFormat-s aren't thread-safe; must clone it jFormat = (NumberFormat) jFormat.clone(); } - return new JavaTemplateNumberFormat(jFormat, formatDesc); + return new JavaTemplateNumberFormat(jFormat, params); } private static final class NumberFormatKey { diff --git a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java index 6ff3565c..9c0b7a5c 100644 --- a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java +++ b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java @@ -22,17 +22,17 @@ final class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFormat { - private final String formatDesc; + private final String formatString; private final NumberFormat javaNumberFormat; - public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatDesc) { - this.formatDesc = formatDesc; + public JavaTemplateNumberFormat(NumberFormat javaNumberFormat, String formatString) { + this.formatString = formatString; this.javaNumberFormat = javaNumberFormat; } @Override public String format(TemplateNumberModel numberModel) throws UnformattableNumberException, TemplateModelException { - Number number = getNonNullNumber(numberModel); + Number number = TemplateNumberFormatUtil.getNonNullNumber(numberModel); return format(number); } @@ -58,7 +58,7 @@ public NumberFormat getJavaNumberFormat() { @Override public String getDescription() { - return formatDesc; + return formatString; } } diff --git a/src/main/java/freemarker/core/LocalTemplateDateFormatFactory.java b/src/main/java/freemarker/core/LocalTemplateDateFormatFactory.java index 6ad5b812..93363d3b 100644 --- a/src/main/java/freemarker/core/LocalTemplateDateFormatFactory.java +++ b/src/main/java/freemarker/core/LocalTemplateDateFormatFactory.java @@ -21,6 +21,7 @@ import java.util.Locale; import java.util.TimeZone; +import freemarker.template.TemplateDateModel; import freemarker.template.TemplateModelException; /** @@ -96,7 +97,7 @@ public final Locale getLocale() { } /** - * Returns the {@link TemplateDateFormat} for the {@code dateType} and {@code formatDescriptor} given via the + * Returns the {@link TemplateDateFormat} for the {@code dateType} and {@code params} given via the * arguments. The returned formatter can be a new instance or a reused (cached) instance. * *

@@ -105,8 +106,8 @@ public final Locale getLocale() { * matters for it, should be bound to the locale and time zone that was in effect when this method was called. * * @param dateType - * {@line TemplateDateModel#DATE}, {@line TemplateDateModel#TIME}, {@line TemplateDateModel#DATETIME} or - * {@line TemplateDateModel#UNKNOWN}. Supporting {@line TemplateDateModel#UNKNOWN} is not necessary, in + * {@link TemplateDateModel#DATE}, {@link TemplateDateModel#TIME}, {@link TemplateDateModel#DATETIME} or + * {@link TemplateDateModel#UNKNOWN}. Supporting {@link TemplateDateModel#UNKNOWN} is not necessary, in * which case the method should throw an {@link UnknownDateTypeFormattingUnsupportedException} exception. * * @param zonelessInput @@ -123,23 +124,20 @@ public final Locale getLocale() { * configuration settings and such, so you should rely on this rule, just accept what this parameter * says. * - * @param formatDescriptor - * The string used as the {@code ..._format} configuration setting value (among others), like - * {@code "iso m"} or {@code "dd.MM.yyyy HH:mm"}. The implementation is only supposed to understand a - * particular kind of format descriptor, for which FreeMarker routes to this factory. (Like, the - * {@link ISOLocalTemplateDateFormatFactory} is only called for format descriptors that start with - * "iso".) + * @param params + * The string that further describes how the format should look. The format of this string is up to the + * {@link LocalTemplateDateFormatFactory} implementation. Note {@code null}, often an empty string. * - * @throws InvalidFormatDescriptorException - * if the {@code formatDescriptor} is malformed + * @throws InvalidFormatParametersException + * if the {@code params} is malformed * @throws TemplateModelException * if the {@code dateType} is unsupported by the formatter * @throws UnknownDateTypeFormattingUnsupportedException - * if {@code dateType} is {@line TemplateDateModel#UNKNOWN}, and that's unsupported by the formatter + * if {@code dateType} is {@link TemplateDateModel#UNKNOWN}, and that's unsupported by the formatter * implementation. */ - public abstract TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor) - throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException, - InvalidFormatDescriptorException; + public abstract TemplateDateFormat get(int dateType, boolean zonelessInput, String params) + throws TemplateModelException, UnknownDateTypeFormattingUnsupportedException, + InvalidFormatParametersException; } diff --git a/src/main/java/freemarker/core/LocalTemplateNumberFormatFactory.java b/src/main/java/freemarker/core/LocalTemplateNumberFormatFactory.java index 391b341d..03bcb6a6 100644 --- a/src/main/java/freemarker/core/LocalTemplateNumberFormatFactory.java +++ b/src/main/java/freemarker/core/LocalTemplateNumberFormatFactory.java @@ -20,7 +20,7 @@ /** * Creates {@link TemplateNumberFormat}-s for a {@link Environment}. The same factory is used to create all the * {@link TemplateNumberFormat}-s of the same formatter type. Thus factories might want to cache instances internally - * with the {@code formatDescriptor} as key. + * with the {@code params} as key. * *

* {@link LocalTemplateDateFormatFactory}-es need not be thread-safe. Currently (2.3.24) they are (re)used only from @@ -72,8 +72,12 @@ public Environment getEnvironment() { * The locale must be already set to non-{@code null} with {@link #setLocale(Locale)} before calling this method. * The returned formatter, if the locale matters for it, should be bound to the locale that was in effect when this * method was called. + * + * @param params + * The string that further describes how the format should look. The format of this string is up to the + * {@link LocalTemplateDateFormatFactory} implementation. Note {@code null}, often an empty string. */ - public abstract TemplateNumberFormat get(String formatDescriptor) - throws InvalidFormatDescriptorException; + public abstract TemplateNumberFormat get(String params) + throws InvalidFormatParametersException; } diff --git a/src/main/java/freemarker/core/TemplateNumberFormat.java b/src/main/java/freemarker/core/TemplateNumberFormat.java index 2cc1371b..1b2c2ca9 100644 --- a/src/main/java/freemarker/core/TemplateNumberFormat.java +++ b/src/main/java/freemarker/core/TemplateNumberFormat.java @@ -97,17 +97,4 @@ public boolean format( // We don't have parse(...) method, because currently FTL only parses to number with the ArithmeticEngine. - /** - * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throw - * {@link UnformattableNumberException} with a standard error message if that's {@code null}. - */ - protected Number getNonNullNumber(TemplateNumberModel numberModel) - throws TemplateModelException, UnformattableNumberException { - Number number = numberModel.getAsNumber(); - if (number == null) { - throw EvalUtil.newModelHasStoredNullException(Number.class, numberModel, null); - } - return number; - } - } diff --git a/src/main/java/freemarker/core/TemplateNumberFormatUtil.java b/src/main/java/freemarker/core/TemplateNumberFormatUtil.java new file mode 100644 index 00000000..4826b0f8 --- /dev/null +++ b/src/main/java/freemarker/core/TemplateNumberFormatUtil.java @@ -0,0 +1,36 @@ +package freemarker.core; + +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNumberModel; + +/** + * @since 2.3.24 + */ +public final class TemplateNumberFormatUtil { + + private TemplateNumberFormatUtil() { + // Not meant to be instantiated + } + + public static void checkHasNoParameters(String params) throws InvalidFormatParametersException + { + if (params.length() != 0) { + throw new InvalidFormatParametersException( + "This number format doesn't support any parameters."); + } + } + + /** + * Utility method to extract the {@link Number} from an {@link TemplateNumberModel}, and throw + * {@link UnformattableNumberException} with a standard error message if that's {@code null}. + */ + public static Number getNonNullNumber(TemplateNumberModel numberModel) + throws TemplateModelException, UnformattableNumberException { + Number number = numberModel.getAsNumber(); + if (number == null) { + throw EvalUtil.newModelHasStoredNullException(Number.class, numberModel, null); + } + return number; + } + +} diff --git a/src/main/java/freemarker/core/UndefinedCustomFormatException.java b/src/main/java/freemarker/core/UndefinedCustomFormatException.java new file mode 100644 index 00000000..129e9c8f --- /dev/null +++ b/src/main/java/freemarker/core/UndefinedCustomFormatException.java @@ -0,0 +1,16 @@ +package freemarker.core; + +/** + * @since 2.3.24 + */ +public class UndefinedCustomFormatException extends InvalidFormatStringException { + + public UndefinedCustomFormatException(String message, Throwable cause) { + super(message, cause); + } + + public UndefinedCustomFormatException(String message) { + super(message); + } + +} diff --git a/src/main/java/freemarker/core/XSLocalTemplateDateFormatFactory.java b/src/main/java/freemarker/core/XSLocalTemplateDateFormatFactory.java index e990e3b1..e383a4e0 100644 --- a/src/main/java/freemarker/core/XSLocalTemplateDateFormatFactory.java +++ b/src/main/java/freemarker/core/XSLocalTemplateDateFormatFactory.java @@ -23,11 +23,11 @@ public XSLocalTemplateDateFormatFactory(Environment env) { } @Override - public TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor) - throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatDescriptorException { + public TemplateDateFormat get(int dateType, boolean zonelessInput, String params) + 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( - formatDescriptor, 2, + params, 2, dateType, zonelessInput, getTimeZone(), this); } diff --git a/src/main/java/freemarker/core/XSTemplateDateFormat.java b/src/main/java/freemarker/core/XSTemplateDateFormat.java index c8a08025..829550d5 100644 --- a/src/main/java/freemarker/core/XSTemplateDateFormat.java +++ b/src/main/java/freemarker/core/XSTemplateDateFormat.java @@ -35,7 +35,7 @@ final class XSTemplateDateFormat extends ISOLikeTemplateDateFormat { boolean zonelessInput, TimeZone timeZone, ISOLikeLocalTemplateDateFormatFactory factory) - throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatDescriptorException { + throws UnknownDateTypeFormattingUnsupportedException, InvalidFormatParametersException { super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory); } diff --git a/src/test/java/freemarker/core/DateFormatTest.java b/src/test/java/freemarker/core/DateFormatTest.java new file mode 100644 index 00000000..ebdc3194 --- /dev/null +++ b/src/test/java/freemarker/core/DateFormatTest.java @@ -0,0 +1,63 @@ +package freemarker.core; + +import java.util.Date; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableMap; + +import freemarker.template.Configuration; +import freemarker.template.TemplateDateModel; +import freemarker.template.TemplateModelException; +import freemarker.test.TemplateTest; + +@SuppressWarnings("boxing") +public class DateFormatTest extends TemplateTest { + + @Before + public void setup() { + Configuration cfg = getConfiguration(); + cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_24); + cfg.setLocale(Locale.US); + + cfg.setCustomDateFormats(ImmutableMap.of( + "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE)); + } + + @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 testNullInNumberModel() throws Exception { + addToDataModel("n", new MutableTemplateDateModel()); + assertErrorContains("${n}", "nothing inside it"); + assertErrorContains("${n?string}", "nothing inside it"); + } + + 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; + } + + } + +} diff --git a/src/test/java/freemarker/core/EpochMillisTemplateDateFormatFactory.java b/src/test/java/freemarker/core/EpochMillisTemplateDateFormatFactory.java index 0ba0d91b..f8752d8c 100644 --- a/src/test/java/freemarker/core/EpochMillisTemplateDateFormatFactory.java +++ b/src/test/java/freemarker/core/EpochMillisTemplateDateFormatFactory.java @@ -47,7 +47,9 @@ private EpochMillisLocalTemplateDateFormatFactory() { } @Override - public TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor) { + public TemplateDateFormat get(int dateType, boolean zonelessInput, String params) + throws InvalidFormatParametersException { + TemplateNumberFormatUtil.checkHasNoParameters(params); return EpochMillisTemplateDateFormat.INSTANCE; } diff --git a/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java index 7c6bf7ae..60505fa0 100644 --- a/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java +++ b/src/test/java/freemarker/core/HexTemplateNumberFormatFactory.java @@ -43,7 +43,8 @@ private HexLocalizedTemplateNumberFormatFactory() { } @Override - public TemplateNumberFormat get(String formatDescriptor) { + public TemplateNumberFormat get(String params) throws InvalidFormatParametersException { + TemplateNumberFormatUtil.checkHasNoParameters(params); return HexTemplateNumberFormat.INSTANCE; } @@ -63,7 +64,7 @@ private HexTemplateNumberFormat() { } @Override public String format(TemplateNumberModel numberModel) throws UnformattableNumberException, TemplateModelException { - Number n = numberModel.getAsNumber(); + Number n = TemplateNumberFormatUtil.getNonNullNumber(numberModel); try { return Integer.toHexString(NumberUtil.toIntExact(n)); } catch (ArithmeticException e) { diff --git a/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java b/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java index 28080c4c..4435973d 100644 --- a/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java +++ b/src/test/java/freemarker/core/LocaleSensitiveTemplateNumberFormatFactory.java @@ -41,7 +41,8 @@ private static class LocaleSensitiveLocalTemplateNumberFormatFactory extends Loc } @Override - public TemplateNumberFormat get(String formatDescriptor) { + public TemplateNumberFormat get(String params) throws InvalidFormatParametersException { + TemplateNumberFormatUtil.checkHasNoParameters(params); return new LocaleSensitiveTemplateNumberFormat(getLocale()); }