checker.src.org.checkerframework.checker.i18nformatter.I18nFormatUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of checker Show documentation
Show all versions of checker Show documentation
The Checker Framework enhances Java’s type system to
make it more powerful and useful. This lets software developers
detect and prevent errors in their Java programs.
The Checker Framework includes compiler plug-ins ("checkers")
that find bugs or verify their absence. It also permits you to
write your own compiler plug-ins.
package org.checkerframework.checker.i18nformatter;
import java.text.ChoiceFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.checkerframework.checker.i18nformatter.qual.I18nChecksFormat;
import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory;
import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat;
/**
* This class provides a collection of utilities to ease working with i18n
* format strings.
*
* @checker_framework.manual #i18n-formatter-checker Internationalization
* Format String Checker
* @author Siwakorn Srisakaokul
*/
public class I18nFormatUtil {
/**
* Throws an exception if the format is not syntactically valid.
*/
public static void tryFormatSatisfiability(String format) throws IllegalFormatException {
MessageFormat.format(format, (Object[]) null);
}
/**
* Returns a {@link I18nConversionCategory} for every conversion found in
* the format string.
*
* Throws an exception if the format is not syntactically valid.
*/
public static I18nConversionCategory[] formatParameterCategories(String format) throws IllegalFormatException {
tryFormatSatisfiability(format);
I18nConversion[] cs = MessageFormatParser.parse(format);
int max_index = -1;
Map conv = new HashMap();
for (I18nConversion c : cs) {
int index = c.index;
conv.put(index, I18nConversionCategory.intersect(c.category, conv.containsKey(index) ? conv.get(index)
: I18nConversionCategory.UNUSED));
max_index = Math.max(max_index, index);
}
I18nConversionCategory[] res = new I18nConversionCategory[max_index + 1];
for (int i = 0; i <= max_index; i++) {
res[i] = conv.containsKey(i) ? conv.get(i) : I18nConversionCategory.UNUSED;
}
return res;
}
/**
* Returns true if the format string is satisfiable, and if the format's
* parameters match the passed {@link I18nConversionCategory}s. Otherwise an
* error is thrown.
*/
// TODO introduce more such functions, see RegexUtil for examples
@I18nChecksFormat
public static boolean hasFormat(String format, I18nConversionCategory... cc) {
I18nConversionCategory[] fcc = formatParameterCategories(format);
if (fcc.length != cc.length) {
return false;
}
for (int i = 0; i < cc.length; i++) {
if (!I18nConversionCategory.isSubsetOf(cc[i], fcc[i])) {
return false;
}
}
return true;
}
@I18nValidFormat
public static boolean isFormat(String format) {
try {
formatParameterCategories(format);
} catch (Exception e) {
return false;
}
return true;
}
private static class I18nConversion {
public int index;
public I18nConversionCategory category;
public I18nConversion(int index, I18nConversionCategory category) {
this.index = index;
this.category = category;
}
@Override
public String toString() {
return category.toString() + "(index: " + index + ")";
}
}
private static class MessageFormatParser {
public static int maxOffset;
/**
* The locale to use for formatting numbers and dates.
*
*/
private static Locale locale;
/**
* An array of formatters, which are used to format the arguments.
*
*/
private static List categories;
/**
* The argument numbers corresponding to each formatter. (The formatters
* are stored in the order they occur in the pattern, not in the order
* in which the arguments are specified.)
*
*/
private static List argumentIndices;
/**
* The number of subformats
*/
private static int numFormat;
// Indices for segments
private static final int SEG_RAW = 0;
private static final int SEG_INDEX = 1;
private static final int SEG_TYPE = 2;
private static final int SEG_MODIFIER = 3; // modifier or subformat
// Indices for type keywords
private static final int TYPE_NULL = 0;
private static final int TYPE_NUMBER = 1;
private static final int TYPE_DATE = 2;
private static final int TYPE_TIME = 3;
private static final int TYPE_CHOICE = 4;
private static final String[] TYPE_KEYWORDS = { "", "number", "date", "time", "choice" };
// Indices for number modifiers
private static final int MODIFIER_DEFAULT = 0; // common in number and
// date-time
private static final int MODIFIER_CURRENCY = 1;
private static final int MODIFIER_PERCENT = 2;
private static final int MODIFIER_INTEGER = 3;
private static final String[] NUMBER_MODIFIER_KEYWORDS = { "", "currency", "percent", "integer" };
private static final String[] DATE_TIME_MODIFIER_KEYWORDS = { "", "short", "medium", "long", "full" };
public static I18nConversion[] parse(String pattern) {
MessageFormatParser.categories = new ArrayList();
MessageFormatParser.argumentIndices = new ArrayList();
MessageFormatParser.locale = Locale.getDefault(Locale.Category.FORMAT);
applyPattern(pattern);
I18nConversion[] ret = new I18nConversion[MessageFormatParser.numFormat];
for (int i = 0; i < MessageFormatParser.numFormat; i++) {
ret[i] = new I18nConversion(argumentIndices.get(i), categories.get(i));
}
return ret;
}
private static void applyPattern(String pattern) {
StringBuilder[] segments = new StringBuilder[4];
// Allocate only segments[SEG_RAW] here. The rest are
// allocated on demand.
segments[SEG_RAW] = new StringBuilder();
int part = SEG_RAW;
MessageFormatParser.numFormat = 0;
boolean inQuote = false;
int braceStack = 0;
maxOffset = -1;
for (int i = 0; i < pattern.length(); ++i) {
char ch = pattern.charAt(i);
if (part == SEG_RAW) {
if (ch == '\'') {
if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') {
segments[part].append(ch); // handle doubles
++i;
} else {
inQuote = !inQuote;
}
} else if (ch == '{' && !inQuote) {
part = SEG_INDEX;
if (segments[SEG_INDEX] == null) {
segments[SEG_INDEX] = new StringBuilder();
}
} else {
segments[part].append(ch);
}
} else {
if (inQuote) { // just copy quotes in parts
segments[part].append(ch);
if (ch == '\'') {
inQuote = false;
}
} else {
switch (ch) {
case ',':
if (part < SEG_MODIFIER) {
if (segments[++part] == null) {
segments[part] = new StringBuilder();
}
} else {
segments[part].append(ch);
}
break;
case '{':
++braceStack;
segments[part].append(ch);
break;
case '}':
if (braceStack == 0) {
part = SEG_RAW;
makeFormat(i, numFormat, segments);
numFormat++;
// throw away other segments
segments[SEG_INDEX] = null;
segments[SEG_TYPE] = null;
segments[SEG_MODIFIER] = null;
} else {
--braceStack;
segments[part].append(ch);
}
break;
case ' ':
// Skip any leading space chars for SEG_TYPE.
if (part != SEG_TYPE || segments[SEG_TYPE].length() > 0) {
segments[part].append(ch);
}
break;
case '\'':
inQuote = true;
segments[part].append(ch);
break;
default:
segments[part].append(ch);
break;
}
}
}
}
if (braceStack == 0 && part != 0) {
maxOffset = -1;
throw new IllegalArgumentException("Unmatched braces in the pattern");
}
}
private static void makeFormat(int position, int offsetNumber, StringBuilder[] textSegments) {
String[] segments = new String[textSegments.length];
for (int i = 0; i < textSegments.length; i++) {
StringBuilder oneseg = textSegments[i];
segments[i] = (oneseg != null) ? oneseg.toString() : "";
}
// get the argument number
int argumentNumber;
try {
argumentNumber = Integer.parseInt(segments[SEG_INDEX]); // always
// unlocalized!
} catch (NumberFormatException e) {
throw new IllegalArgumentException("can't parse argument number: " + segments[SEG_INDEX], e);
}
if (argumentNumber < 0) {
throw new IllegalArgumentException("negative argument number: " + argumentNumber);
}
int oldMaxOffset = maxOffset;
maxOffset = offsetNumber;
argumentIndices.add(argumentNumber);
// now get the format
I18nConversionCategory category = null;
if (segments[SEG_TYPE].length() != 0) {
int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS);
switch (type) {
case TYPE_NULL:
category = I18nConversionCategory.GENERAL;
break;
case TYPE_NUMBER:
switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) {
case MODIFIER_DEFAULT:
case MODIFIER_CURRENCY:
case MODIFIER_PERCENT:
case MODIFIER_INTEGER:
break;
default: // DecimalFormat pattern
try {
new DecimalFormat(segments[SEG_MODIFIER], DecimalFormatSymbols.getInstance(locale));
} catch (IllegalArgumentException e) {
maxOffset = oldMaxOffset;
// invalid decimal subformat pattern
throw e;
}
break;
}
category = I18nConversionCategory.NUMBER;
break;
case TYPE_DATE:
case TYPE_TIME:
int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS);
if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) {
// nothing to do
} else {
// SimpleDateFormat pattern
try {
new SimpleDateFormat(segments[SEG_MODIFIER], locale);
} catch (IllegalArgumentException e) {
maxOffset = oldMaxOffset;
// invalid date subformat pattern
throw e;
}
}
category = I18nConversionCategory.DATE;
break;
case TYPE_CHOICE:
if (segments[SEG_MODIFIER].length() == 0) {
throw new IllegalArgumentException("Choice Pattern requires Subformat Pattern: "
+ segments[SEG_MODIFIER]);
}
try {
// ChoiceFormat pattern
new ChoiceFormat(segments[SEG_MODIFIER]);
} catch (Exception e) {
maxOffset = oldMaxOffset;
// invalid choice subformat pattern
throw new IllegalArgumentException("Choice Pattern incorrect: " + segments[SEG_MODIFIER], e);
}
category = I18nConversionCategory.NUMBER;
break;
default:
maxOffset = oldMaxOffset;
throw new IllegalArgumentException("unknown format type: " + segments[SEG_TYPE]);
}
} else {
category = I18nConversionCategory.GENERAL;
}
categories.add(category);
}
private static final int findKeyword(String s, String[] list) {
for (int i = 0; i < list.length; ++i) {
if (s.equals(list[i])) {
return i;
}
}
// Try trimmed lowercase.
String ls = s.trim().toLowerCase(Locale.ROOT);
if (ls != s) {
for (int i = 0; i < list.length; ++i) {
if (ls.equals(list[i])) {
return i;
}
}
}
return -1;
}
}
}