Skip to content
This repository has been archived by the owner on Jan 8, 2020. It is now read-only.

Commit

Permalink
Number/date/time formatter API and code reworkings.
Browse files Browse the repository at this point in the history
  • Loading branch information
ddekany committed Aug 28, 2015
1 parent 9c66dc0 commit 4786a52
Show file tree
Hide file tree
Showing 19 changed files with 548 additions and 70 deletions.
70 changes: 68 additions & 2 deletions src/main/java/freemarker/core/Configurable.java
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ public class Configurable {
private Boolean showErrorTips;
private Boolean apiBuiltinEnabled;
private Boolean logTemplateExceptions;
private Map<String, TemplateDateFormatFactory> customDateFormats;
private Map<String, TemplateNumberFormatFactory> customNumberFormats;

/**
* Creates a top-level configurable, one that doesn't inherit from a parent, and thus stores the default values.
Expand Down Expand Up @@ -378,6 +380,9 @@ protected Configurable(Version incompatibleImprovements) {
setBooleanFormat(C_TRUE_FALSE);

customAttributes = new HashMap();

customDateFormats = Collections.emptyMap();
customNumberFormats = Collections.emptyMap();
}

/**
Expand Down Expand Up @@ -702,7 +707,7 @@ public void setNumberFormat(String numberFormat) {
this.numberFormat = numberFormat;
properties.setProperty(NUMBER_FORMAT_KEY, numberFormat);
}

/**
* Getter pair of {@link #setNumberFormat(String)}.
*/
Expand All @@ -718,7 +723,37 @@ public String getNumberFormat() {
public boolean isNumberFormatSet() {
return numberFormat != null;
}


/**
* Getter pair of {@link #setCustomNumberFormats(Map)}; do not modify the returned {@link Map}!
*
* @since 2.3.24
*/
public Map<String, TemplateNumberFormatFactory> getCustomNumberFormats() {
return customNumberFormats == null ? parent.getCustomNumberFormats() : customNumberFormats;
}

/**
* Associates names with formatter factories, which then can be referred by the {@link #setNumberFormat(String)
* number_format} settings with values starting with <code>@<i>name</i></code>.
*
* @param customNumberFormats
* Can't be {@code null}.
*
* @since 2.3.24
*/
public void setCustomNumberFormats(Map<String, TemplateNumberFormatFactory> customNumberFormats) {
NullArgumentException.check("customNumberFormats", customNumberFormats);
this.customNumberFormats = customNumberFormats;
}

/**
* @since 2.3.24
*/
public boolean isCustomNumberFormatsSet() {
return customNumberFormats != null;
}

/**
* The string value for the boolean {@code true} and {@code false} values, intended for human audience (not for a
* computer language), separated with comma. For example, {@code "yes,no"}. Note that white-space is significant,
Expand Down Expand Up @@ -1000,6 +1035,37 @@ public boolean isDateTimeFormatSet() {
return dateTimeFormat != null;
}

/**
* Getter pair of {@link #setCustomDateFormats(Map)}; do not modify the returned {@link Map}!
*
* @since 2.3.24
*/
public Map<String, TemplateDateFormatFactory> getCustomDateFormats() {
return customDateFormats == null ? parent.getCustomDateFormats() : customDateFormats;
}

/**
* Associates names with formatter factories, which then can be referred by the {@link #setDateTimeFormat(String)
* date_format}, {@link #setDateTimeFormat(String) time_format}, and {@link #setDateTimeFormat(String)
* datetime_format} settings with values starting with <code>@<i>name</i></code>.
*
* @param customDateFormats
* Can't be {@code null}.
*
* @since 2.3.24
*/
public void setCustomDateFormats(Map<String, TemplateDateFormatFactory> customDateFormats) {
NullArgumentException.check("customDateFormats", customDateFormats);
this.customDateFormats = customDateFormats;
}

/**
* @since 2.3.24
*/
public boolean isCustomDateFormatsSet() {
return this.customDateFormats != null;
}

/**
* Sets the exception handler used to handle exceptions occurring inside templates.
* The default is {@link TemplateExceptionHandler#DEBUG_HANDLER}. The recommended values are:
Expand Down
19 changes: 9 additions & 10 deletions src/main/java/freemarker/core/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
Expand Down Expand Up @@ -91,7 +90,7 @@ public final class Environment extends Configurable {
private static final Logger LOG = Logger.getLogger("freemarker.runtime");
private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.attempt");

private static final Map JAVA_NUMBER_FORMATS = new HashMap();
private static final Map<NumberFormatKey, NumberFormat> GLOBAL_CACHED_NUMBER_FORMATS = new HashMap();

// Do not use this object directly; clone it first! DecimalFormat isn't
// thread-safe.
Expand All @@ -110,7 +109,7 @@ public final class Environment extends Configurable {
private final ArrayList recoveredErrorStack = new ArrayList();

private NumberFormat cachedNumberFormat;
private Map cachedNumberFormats;
private Map<String, NumberFormat> cachedNumberFormats;

/**
* Stores the date/time/date-time formatters that are used when no format is explicitly given at the place of
Expand Down Expand Up @@ -1104,19 +1103,19 @@ void clearLastReturnValue() {

NumberFormat getNumberFormatObject(String pattern) {
if (cachedNumberFormats == null) {
cachedNumberFormats = new HashMap();
cachedNumberFormats = new HashMap<String, NumberFormat>();
}

NumberFormat format = (NumberFormat) cachedNumberFormats.get(pattern);
NumberFormat format = cachedNumberFormats.get(pattern);
if (format != null) {
return format;
}

// Get format from global format cache
synchronized (JAVA_NUMBER_FORMATS) {
synchronized (GLOBAL_CACHED_NUMBER_FORMATS) {
Locale locale = getLocale();
NumberFormatKey fk = new NumberFormatKey(pattern, locale);
format = (NumberFormat) JAVA_NUMBER_FORMATS.get(fk);
format = GLOBAL_CACHED_NUMBER_FORMATS.get(fk);
if (format == null) {
// Add format to global format cache. Note this is
// globally done once per locale per pattern.
Expand All @@ -1131,7 +1130,7 @@ NumberFormat getNumberFormatObject(String pattern) {
} else {
format = new DecimalFormat(pattern, new DecimalFormatSymbols(getLocale()));
}
JAVA_NUMBER_FORMATS.put(fk, format);
GLOBAL_CACHED_NUMBER_FORMATS.put(fk, format);
}
}

Expand Down Expand Up @@ -1323,15 +1322,15 @@ private TemplateDateFormat getTemplateDateFormat(

try {
return templateDateFormatFactory.get(dateType, zonelessInput, formatDescriptor);
} catch (ParseException e) {
} catch (InvalidFormatDescriptorException e) {
throw new _TemplateModelException(e.getCause(),
(formatDescriptorCfgSettingName == null
? (Object) "Malformed date/time format descriptor: "
: new Object[] {
"The value of the \"", formatDescriptorCfgSettingName,
"\" FreeMarker configuration setting is a malformed date/time format descriptor: "
}),
new _DelayedJQuote(formatDescriptor), ". Reason given: ",
new _DelayedJQuote(e.getFormatDescriptor()), ". Reason given: ",
e.getMessage());
}
}
Expand Down
62 changes: 34 additions & 28 deletions src/main/java/freemarker/core/ISOLikeTemplateDateFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package freemarker.core;

import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

Expand All @@ -41,16 +40,16 @@ abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat {
protected final int accuracy;

/**
* @param settingValue The value of the ..._format setting, like "iso nz".
* @param formatDesc 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(
String settingValue, int parsingStart,
final String formatDesc, int parsingStart,
int dateType, boolean zonelessInput,
TimeZone timeZone,
ISOLikeLocalizedTemplateDateFormatFactory factory)
throws ParseException, UnknownDateTypeFormattingUnsupportedException {
throws InvalidFormatDescriptorException, UnknownDateTypeFormattingUnsupportedException {
this.factory = factory;
if (dateType == TemplateDateModel.UNKNOWN) {
throw new UnknownDateTypeFormattingUnsupportedException();
Expand All @@ -59,44 +58,48 @@ public ISOLikeTemplateDateFormat(
this.dateType = dateType;
this.zonelessInput = zonelessInput;

final int ln = settingValue.length();
final int ln = formatDesc.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 = settingValue.charAt(i++);
final char c = formatDesc.charAt(i++);
if (c == '_' || c == ' ') {
afterSeparator = true;
} else {
if (!afterSeparator) {
throw new java.text.ParseException(
"Missing space or \"_\" before \"" + c + "\"", i);
throw new InvalidFormatDescriptorException(
"Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ").", formatDesc);
}

switch (c) {
case 'h':
case 'm':
case 's':
if (accuracy != DateUtil.ACCURACY_MILLISECONDS) {
throw new java.text.ParseException(
"Character \"" + c + "\" is unexpected as accuracy was already specified earlier." , i);
throw new InvalidFormatDescriptorException(
"Character \"" + c + "\" is unexpected as accuracy was already specified earlier "
+ "(at char pos. " + i + ").",
formatDesc);
}
switch (c) {
case 'h':
if (isXSMode()) {
throw new java.text.ParseException(XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, i);
throw new InvalidFormatDescriptorException(
XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, formatDesc);
}
accuracy = DateUtil.ACCURACY_HOURS;
break;
case 'm':
if (i < ln && settingValue.charAt(i) == 's') {
if (i < ln && formatDesc.charAt(i) == 's') {
i++;
accuracy = DateUtil.ACCURACY_MILLISECONDS_FORCED;
} else {
if (isXSMode()) {
throw new java.text.ParseException(XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, i);
throw new InvalidFormatDescriptorException(
XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE, formatDesc);
}
accuracy = DateUtil.ACCURACY_MINUTES;
}
Expand All @@ -107,47 +110,50 @@ public ISOLikeTemplateDateFormat(
}
break;
case 'f':
if (i < ln && settingValue.charAt(i) == 'u') {
checkForceUTCNotSet(forceUTC, i);
if (i < ln && formatDesc.charAt(i) == 'u') {
checkForceUTCNotSet(forceUTC, formatDesc);
i++;
forceUTC = Boolean.TRUE;
break;
}
// Falls through
case 'n':
if (showZoneOffset != null) {
throw new java.text.ParseException(
throw new InvalidFormatDescriptorException(
"Character \"" + c + "\" is unexpected as zone offset visibility was already "
+ "specified earlier." , i);
+ "specified earlier. (at char pos. " + i + ").", formatDesc);
}
switch (c) {
case 'n':
if (i < ln && settingValue.charAt(i) == 'z') {
if (i < ln && formatDesc.charAt(i) == 'z') {
i++;
showZoneOffset = Boolean.FALSE;
} else {
throw new java.text.ParseException("\"n\" must be followed by \"z\"", i);
throw new InvalidFormatDescriptorException(
"\"n\" must be followed by \"z\" (at char pos. " + i + ").", formatDesc);
}
break;
case 'f':
if (i < ln && settingValue.charAt(i) == 'z') {
if (i < ln && formatDesc.charAt(i) == 'z') {
i++;
showZoneOffset = Boolean.TRUE;
} else {
throw new java.text.ParseException("\"f\" must be followed by \"z\"", i);
throw new InvalidFormatDescriptorException(
"\"f\" must be followed by \"z\" (at char pos. " + i + ").", formatDesc);
}
break;
}
break;
case 'u':
checkForceUTCNotSet(forceUTC, i);
checkForceUTCNotSet(forceUTC, formatDesc);
forceUTC = null; // means UTC will be used except for zonelessInput
break;
default:
throw new java.text.ParseException(
throw new InvalidFormatDescriptorException(
"Unexpected character, " + StringUtil.jQuote(String.valueOf(c))
+ ". Expected the beginning of one of: h, m, s, ms, nz, fz, u",
i);
+ ". Expected the beginning of one of: h, m, s, ms, nz, fz, u"
+ " (at char pos. " + i + ").",
formatDesc);
} // switch
afterSeparator = false;
} // else
Expand All @@ -159,10 +165,10 @@ public ISOLikeTemplateDateFormat(
this.timeZone = timeZone;
}

private void checkForceUTCNotSet(Boolean fourceUTC, int i) throws ParseException {
private void checkForceUTCNotSet(Boolean fourceUTC, String formatDesc) throws InvalidFormatDescriptorException {
if (fourceUTC != Boolean.FALSE) {
throw new java.text.ParseException(
"The UTC usage option was already set earlier." , i);
throw new InvalidFormatDescriptorException(
"The UTC usage option was already set earlier.", formatDesc);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public ISOLocalizedTemplateDateFormatFactory(Environment env, TimeZone timeZone)

@Override
public TemplateDateFormat get(int dateType, boolean zonelessInput, String formatDescriptor)
throws java.text.ParseException, UnknownDateTypeFormattingUnsupportedException {
throws InvalidFormatDescriptorException, 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,
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/freemarker/core/ISOTemplateDateFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package freemarker.core;

import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

Expand All @@ -32,7 +31,7 @@ final class ISOTemplateDateFormat extends ISOLikeTemplateDateFormat {
int dateType, boolean zonelessInput,
TimeZone timeZone,
ISOLikeLocalizedTemplateDateFormatFactory factory)
throws ParseException, UnknownDateTypeFormattingUnsupportedException {
throws InvalidFormatDescriptorException, UnknownDateTypeFormattingUnsupportedException {
super(settingValue, parsingStart, dateType, zonelessInput, timeZone, factory);
}

Expand Down
Loading

0 comments on commit 4786a52

Please sign in to comment.