blob: 8f2da0db390eb3a86666c832113449de5d62f7b1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package freemarker.core;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Currency;
import java.util.HashMap;
import java.util.Locale;
import java.util.Set;
import freemarker.template.utility.StringUtil;
class ExtendedDecimalFormatParser {
private static final String PARAM_ROUNDING_MODE = "roundingMode";
private static final String PARAM_MULTIPIER = "multipier";
private static final String PARAM_MULTIPLIER = "multiplier";
private static final String PARAM_DECIMAL_SEPARATOR = "decimalSeparator";
private static final String PARAM_MONETARY_DECIMAL_SEPARATOR = "monetaryDecimalSeparator";
private static final String PARAM_GROUP_SEPARATOR = "groupingSeparator";
private static final String PARAM_EXPONENT_SEPARATOR = "exponentSeparator";
private static final String PARAM_MINUS_SIGN = "minusSign";
private static final String PARAM_INFINITY = "infinity";
private static final String PARAM_NAN = "nan";
private static final String PARAM_PERCENT = "percent";
private static final String PARAM_PER_MILL = "perMill";
private static final String PARAM_ZERO_DIGIT = "zeroDigit";
private static final String PARAM_CURRENCY_CODE = "currencyCode";
private static final String PARAM_CURRENCY_SYMBOL = "currencySymbol";
private static final String PARAM_VALUE_RND_UP = "up";
private static final String PARAM_VALUE_RND_DOWN = "down";
private static final String PARAM_VALUE_RND_CEILING = "ceiling";
private static final String PARAM_VALUE_RND_FLOOR = "floor";
private static final String PARAM_VALUE_RND_HALF_DOWN = "halfDown";
private static final String PARAM_VALUE_RND_HALF_EVEN = "halfEven";
private static final String PARAM_VALUE_RND_HALF_UP = "halfUp";
private static final String PARAM_VALUE_RND_UNNECESSARY = "unnecessary";
private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS;
static {
HashMap<String, ParameterHandler> m = new HashMap<>();
m.put(PARAM_ROUNDING_MODE, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
RoundingMode parsedValue;
if (value.equals(PARAM_VALUE_RND_UP)) {
parsedValue = RoundingMode.UP;
} else if (value.equals(PARAM_VALUE_RND_DOWN)) {
parsedValue = RoundingMode.DOWN;
} else if (value.equals(PARAM_VALUE_RND_CEILING)) {
parsedValue = RoundingMode.CEILING;
} else if (value.equals(PARAM_VALUE_RND_FLOOR)) {
parsedValue = RoundingMode.FLOOR;
} else if (value.equals(PARAM_VALUE_RND_HALF_DOWN)) {
parsedValue = RoundingMode.HALF_DOWN;
} else if (value.equals(PARAM_VALUE_RND_HALF_EVEN)) {
parsedValue = RoundingMode.HALF_EVEN;
} else if (value.equals(PARAM_VALUE_RND_HALF_UP)) {
parsedValue = RoundingMode.HALF_UP;
} else if (value.equals(PARAM_VALUE_RND_UNNECESSARY)) {
parsedValue = RoundingMode.UNNECESSARY;
} else {
throw new InvalidParameterValueException("Should be one of: "
+ PARAM_VALUE_RND_UP + ", " + PARAM_VALUE_RND_DOWN + ", " + PARAM_VALUE_RND_CEILING + ", "
+ PARAM_VALUE_RND_FLOOR + ", " + PARAM_VALUE_RND_HALF_DOWN + ", "
+ PARAM_VALUE_RND_HALF_EVEN + ", " + PARAM_VALUE_RND_UNNECESSARY);
}
parser.roundingMode = parsedValue;
}
});
ParameterHandler multiplierParamHandler = new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
try {
parser.multiplier = Integer.valueOf(value);
} catch (NumberFormatException e) {
throw new InvalidParameterValueException("Malformed integer.");
}
}
};
m.put(PARAM_MULTIPLIER, multiplierParamHandler);
m.put(PARAM_MULTIPIER, multiplierParamHandler);
m.put(PARAM_DECIMAL_SEPARATOR, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setDecimalSeparator(value.charAt(0));
}
});
m.put(PARAM_MONETARY_DECIMAL_SEPARATOR, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setMonetaryDecimalSeparator(value.charAt(0));
}
});
m.put(PARAM_GROUP_SEPARATOR, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setGroupingSeparator(value.charAt(0));
}
});
m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
parser.symbols.setExponentSeparator(value);
}
});
m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setMinusSign(value.charAt(0));
}
});
m.put(PARAM_INFINITY, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
parser.symbols.setInfinity(value);
}
});
m.put(PARAM_NAN, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
parser.symbols.setNaN(value);
}
});
m.put(PARAM_PERCENT, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setPercent(value.charAt(0));
}
});
m.put(PARAM_PER_MILL, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setPerMill(value.charAt(0));
}
});
m.put(PARAM_ZERO_DIGIT, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
if (value.length() != 1) {
throw new InvalidParameterValueException("Must contain exactly 1 character.");
}
parser.symbols.setZeroDigit(value.charAt(0));
}
});
m.put(PARAM_CURRENCY_CODE, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
Currency currency;
try {
currency = Currency.getInstance(value);
} catch (IllegalArgumentException e) {
throw new InvalidParameterValueException("Not a known ISO 4217 code.");
}
parser.symbols.setCurrency(currency);
}
});
PARAM_HANDLERS = m;
}
private static final String SNIP_MARK = "[...]";
private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length!
private final String src;
private int pos = 0;
private final DecimalFormatSymbols symbols;
private RoundingMode roundingMode;
private Integer multiplier;
static DecimalFormat parse(String formatString, Locale locale) throws ParseException {
return new ExtendedDecimalFormatParser(formatString, locale).parse();
}
private DecimalFormat parse() throws ParseException {
String stdPattern = fetchStandardPattern();
skipWS();
parseFormatStringExtension();
DecimalFormat decimalFormat;
try {
decimalFormat = new DecimalFormat(stdPattern, symbols);
} catch (IllegalArgumentException e) {
ParseException pe = new ParseException(e.getMessage(), 0);
if (e.getCause() != null) {
try {
e.initCause(e.getCause());
} catch (Exception e2) {
// Supress
}
}
throw pe;
}
if (roundingMode != null) {
decimalFormat.setRoundingMode(roundingMode);
}
if (multiplier != null) {
decimalFormat.setMultiplier(multiplier.intValue());
}
return decimalFormat;
}
private void parseFormatStringExtension() throws ParseException {
int ln = src.length();
if (pos == ln) {
return;
}
String currencySymbol = null; // Exceptional, as must be applied after "currency code"
fetchParamters: do {
int namePos = pos;
String name = fetchName();
if (name == null) {
throw newExpectedSgParseException("name");
}
skipWS();
if (!fetchChar('=')) {
throw newExpectedSgParseException("\"=\"");
}
skipWS();
int valuePos = pos;
String value = fetchValue();
if (value == null) {
throw newExpectedSgParseException("value");
}
int paramEndPos = pos;
ParameterHandler handler = PARAM_HANDLERS.get(name);
if (handler == null) {
if (name.equals(PARAM_CURRENCY_SYMBOL)) {
currencySymbol = value;
} else {
throw newUnknownParameterException(name, namePos);
}
} else {
try {
handler.handle(this, value);
} catch (InvalidParameterValueException e) {
throw newInvalidParameterValueException(name, value, valuePos, e);
}
}
skipWS();
// Optional comma
if (fetchChar(',')) {
skipWS();
} else {
if (pos == ln) {
break fetchParamters;
}
if (pos == paramEndPos) {
throw newExpectedSgParseException("parameter separator whitespace or comma");
}
}
} while (true);
// This is brought out to here to ensure that it's applied after "currency code":
if (currencySymbol != null) {
symbols.setCurrencySymbol(currencySymbol);
}
}
private ParseException newInvalidParameterValueException(String name, String value, int valuePos,
InvalidParameterValueException e) {
return new java.text.ParseException(
StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: "
+ e.message,
valuePos);
}
private ParseException newUnknownParameterException(String name, int namePos) throws ParseException {
StringBuilder sb = new StringBuilder(128);
sb.append("Unsupported parameter name, ").append(StringUtil.jQuote(name));
sb.append(". The supported names are: ");
Set<String> legalNames = PARAM_HANDLERS.keySet();
String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]);
Arrays.sort(legalNameArr);
for (int i = 0; i < legalNameArr.length; i++) {
if (i != 0) {
sb.append(", ");
}
sb.append(legalNameArr[i]);
}
return new java.text.ParseException(sb.toString(), namePos);
}
private void skipWS() {
int ln = src.length();
while (pos < ln && isWS(src.charAt(pos))) {
pos++;
}
}
private boolean fetchChar(char fetchedChar) {
if (pos < src.length() && src.charAt(pos) == fetchedChar) {
pos++;
return true;
} else {
return false;
}
}
private boolean isWS(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0';
}
private String fetchName() throws ParseException {
int ln = src.length();
int startPos = pos;
boolean firstChar = true;
scanUntilEnd: while (pos < ln) {
char c = src.charAt(pos);
if (firstChar) {
if (!Character.isJavaIdentifierStart(c)) {
break scanUntilEnd;
}
firstChar = false;
} else if (!Character.isJavaIdentifierPart(c)) {
break scanUntilEnd;
}
pos++;
}
return !firstChar ? src.substring(startPos, pos) : null;
}
private String fetchValue() throws ParseException {
int ln = src.length();
int startPos = pos;
char openedQuot = 0;
boolean needsUnescaping = false;
scanUntilEnd: while (pos < ln) {
char c = src.charAt(pos);
if (c == '\'' || c == '"') {
if (openedQuot == 0) {
if (startPos != pos) {
throw new java.text.ParseException(
"The " + c + " character can only be used for quoting values, "
+ "but it was in the middle of an non-quoted value.",
pos);
}
openedQuot = c;
} else if (c == openedQuot) {
if (pos + 1 < ln && src.charAt(pos + 1) == openedQuot) {
pos++; // skip doubled quote (escaping)
needsUnescaping = true;
} else {
String str = src.substring(startPos + 1, pos);
pos++;
return needsUnescaping ? unescape(str, openedQuot) : str;
}
}
} else {
if (openedQuot == 0 && !Character.isJavaIdentifierPart(c)) {
break scanUntilEnd;
}
}
pos++;
} // while
if (openedQuot != 0) {
throw new java.text.ParseException(
"The " + openedQuot
+ " quotation wasn't closed when the end of the source was reached.",
pos);
}
return startPos == pos ? null : src.substring(startPos, pos);
}
private String unescape(String s, char openedQuot) {
return openedQuot == '\'' ? StringUtil.replace(s, "\'\'", "\'") : StringUtil.replace(s, "\"\"", "\"");
}
private String fetchStandardPattern() {
int pos = this.pos;
int ln = src.length();
int semicolonCnt = 0;
boolean quotedMode = false;
findStdPartEnd: while (pos < ln) {
char c = src.charAt(pos);
if (c == ';' && !quotedMode) {
semicolonCnt++;
if (semicolonCnt == 2) {
break findStdPartEnd;
}
} else if (c == '\'') {
if (quotedMode) {
if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
// Skips "''" used for escaping "'"
pos++;
} else {
quotedMode = false;
}
} else {
quotedMode = true;
}
}
pos++;
}
String stdFormatStr;
if (semicolonCnt < 2) { // We have a standard DecimalFormat string
// Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there
stdFormatStr = src;
} else { // `pos` points to the 2nd ';'
int stdEndPos = pos;
if (src.charAt(pos - 1) == ';') { // we have a ";;"
// Note that ";;" is illegal in DecimalFormat, so this is backward compatible.
stdEndPos--;
}
stdFormatStr = src.substring(0, stdEndPos);
}
if (pos < ln) {
pos++; // Skips closing ';'
}
this.pos = pos;
return stdFormatStr;
}
private ExtendedDecimalFormatParser(String formatString, Locale locale) {
src = formatString;
this.symbols = DecimalFormatSymbols.getInstance(locale);
}
private ParseException newExpectedSgParseException(String expectedThing) {
String quotation;
// Ignore trailing WS when calculating the length:
int i = src.length() - 1;
while (i >= 0 && Character.isWhitespace(src.charAt(i))) {
i--;
}
int ln = i + 1;
if (pos < ln) {
int qEndPos = pos + MAX_QUOTATION_LENGTH;
if (qEndPos >= ln) {
quotation = src.substring(pos, ln);
} else {
quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK;
}
} else {
quotation = null;
}
return new ParseException(
"Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but "
+ (quotation == null ? "reached the end of the input." : "found: " + quotation),
pos);
}
private interface ParameterHandler {
void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException;
}
private static class InvalidParameterValueException extends Exception {
private final String message;
public InvalidParameterValueException(String message) {
this.message = message;
}
}
}