blob: bc551f022a503aa9d1d7dbbe0c249e81edbc9ce2 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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 com.google.gwt.i18n.rebind;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.i18n.client.Messages.Offset;
import com.google.gwt.i18n.client.Messages.Optional;
import com.google.gwt.i18n.client.Messages.PluralCount;
import com.google.gwt.i18n.client.Messages.PluralText;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.i18n.client.PluralRule;
import com.google.gwt.i18n.client.PluralRule.PluralForm;
import com.google.gwt.i18n.client.impl.plurals.DefaultRule;
import com.google.gwt.i18n.rebind.AbstractResource.MissingResourceException;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceEntry;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.DefaultTemplateChunkVisitor;
import com.google.gwt.i18n.rebind.MessageFormatParser.StaticArgChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.StringChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
import com.google.gwt.user.rebind.AbstractMethodCreator;
import org.apache.tapestry.util.text.LocalizedPropertiesLoader;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Creator for methods of the Messages interface.
*/
class MessagesMethodCreator extends AbstractMethodCreator {
/**
* Implements {x,date...} references in MessageFormat.
*/
private static class DateFormatter implements ValueFormatter {
public boolean format(TreeLogger logger, GwtLocale locale,
StringGenerator out, Map<String, String> formatArgs, String subformat,
String argName, JType argType, Parameters params) {
if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
return true;
}
String tzParam = "";
String tzArg = formatArgs.get("tz");
if (tzArg != null) {
if (tzArg.startsWith("$")) {
int paramNum = params.getParameterIndex(tzArg.substring(1));
if (paramNum < 0) {
logger.log(
TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
return true;
} else if (!"com.google.gwt.i18n.client.TimeZone".equals(
params.getParameter(paramNum).getType().getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "Currency code parameter must be TimeZone");
return true;
} else {
tzParam = ", arg" + paramNum;
}
} else {
tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
+ tzArg + ")";
}
}
if (subformat == null || "medium".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getMediumDateFormat()" + ".format(" + argName
+ tzParam + ")");
} else if ("full".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getFullDateFormat().format(" + argName
+ tzParam + ")");
} else if ("long".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getLongDateFormat().format(" + argName
+ tzParam + ")");
} else if ("short".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getShortDateFormat()" + ".format(" + argName
+ tzParam + ")");
} else {
logger.log(TreeLogger.WARN, "Use localdatetime format instead");
out.appendStringValuedExpression(
dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format("
+ argName + tzParam + ")");
}
return false;
}
}
/**
* Interface used to abstract away differences between accessing an array and
* a list.
*/
private interface ListAccessor {
String getElement(String element);
String getSize();
}
/**
* Implementation of ListAccessor for an array.
*/
private static class ListAccessorArray implements ListAccessor {
private final int listArgNum;
public ListAccessorArray(int listArgNum) {
this.listArgNum = listArgNum;
}
public String getElement(String element) {
return "arg" + listArgNum + "[" + element + "]";
}
public String getSize() {
return "arg" + listArgNum + ".length";
}
}
/**
* Implementation of ListAccessor for a List.
*/
private static class ListAccessorList implements ListAccessor {
private final int listArgNum;
public ListAccessorList(int listArgNum) {
this.listArgNum = listArgNum;
}
public String getElement(String element) {
return "arg" + listArgNum + ".get(" + element + ")";
}
public String getSize() {
return "arg" + listArgNum + ".size()";
}
}
/**
* Implements {x,localdatetime,skeleton} references in MessageFormat.
*/
private static class LocalDateTimeFormatter implements ValueFormatter {
private static final String PREDEF = "predef:";
public boolean format(TreeLogger logger, GwtLocale locale,
StringGenerator out, Map<String, String> formatArgs, String subformat,
String argName, JType argType, Parameters params) {
if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
logger.log(TreeLogger.ERROR,
"Only java.util.Date acceptable for localdatetime format");
return true;
}
if (subformat == null || subformat.length() == 0) {
logger.log(TreeLogger.ERROR,
"localdatetime format requires a skeleton pattern");
return true;
}
String tzParam = "";
String tzArg = formatArgs.get("tz");
if (tzArg != null) {
if (tzArg.startsWith("$")) {
int paramNum = params.getParameterIndex(tzArg.substring(1));
if (paramNum < 0) {
logger.log(
TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
return true;
} else if (!"com.google.gwt.i18n.client.TimeZone".equals(
params.getParameter(paramNum).getType().getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "tz parameter must be of type TimeZone");
return true;
} else {
tzParam = ", arg" + paramNum;
}
} else {
tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
+ tzArg + ")";
}
}
if (subformat.startsWith(PREDEF)) {
// TODO(jat): error checking/logging
PredefinedFormat predef;
try {
predef = PredefinedFormat.valueOf(
subformat.substring(PREDEF.length()));
} catch (IllegalArgumentException e) {
logger.log(TreeLogger.ERROR,
"Unrecognized predefined format '" + subformat + "'");
return true;
}
out.appendStringValuedExpression(
dtFormatClassName + ".getFormat(" + PredefinedFormat.class.getName()
+ "." + predef.toString() + ").format(" + argName + tzParam
+ ")");
return false;
}
DateTimePatternGenerator dtpg = new DateTimePatternGenerator(locale);
try {
String pattern = dtpg.getBestPattern(subformat);
if (pattern == null) {
logger.log(TreeLogger.ERROR,
"Invalid localdatetime skeleton pattern \"" + subformat + "\"");
return true;
}
out.appendStringValuedExpression(
dtFormatClassName + ".getFormat(" + wrap(pattern) + ").format("
+ argName + tzParam + ")");
} catch (IllegalArgumentException e) {
logger.log(TreeLogger.ERROR,
"Unable to parse '" + subformat + ": " + e.getMessage());
return true;
}
return false;
}
}
/**
* Implements {x,number...} references in MessageFormat.
*/
private static class NumberFormatter implements ValueFormatter {
public boolean format(TreeLogger logger, GwtLocale locale,
StringGenerator out, Map<String, String> formatArgs, String subformat,
String argName, JType argType, Parameters params) {
JPrimitiveType argPrimType = argType.isPrimitive();
if (argPrimType != null) {
if (argPrimType == JPrimitiveType.BOOLEAN
|| argPrimType == JPrimitiveType.VOID) {
logger.log(
TreeLogger.ERROR, "Illegal argument type for number format");
return true;
}
} else {
JClassType classType = argType.isClass();
if (classType == null) {
logger.log(
TreeLogger.ERROR, "Unexpected argument type for number format");
return true;
}
TypeOracle oracle = classType.getOracle();
JClassType numberType = oracle.findType("java.lang.Number");
if (!classType.isAssignableTo(numberType)) {
logger.log(TreeLogger.ERROR,
"Only Number subclasses may be formatted as a number");
return true;
}
}
String curCodeParam = "";
String curCode = formatArgs.get("curcode");
if (curCode != null) {
if (curCode.startsWith("$")) {
int paramNum = params.getParameterIndex(curCode.substring(1));
if (paramNum < 0) {
logger.log(TreeLogger.ERROR,
"Unable to resolve curcode argument " + curCode);
return true;
} else if (!"java.lang.String".equals(
params.getParameter(paramNum).getType().getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "Currency code parameter must be String");
return true;
} else {
curCodeParam = "arg" + paramNum;
}
} else {
curCodeParam = '"' + curCode + '"';
}
}
if (subformat == null) {
out.appendStringValuedExpression(
numFormatClassName + ".getDecimalFormat().format(" + argName + ")");
} else if ("integer".equals(subformat)) {
out.appendStringValuedExpression(
numFormatClassName + ".getIntegerFormat().format(" + argName + ")");
} else if ("currency".equals(subformat)) {
out.appendStringValuedExpression(
numFormatClassName + ".getCurrencyFormat(" + curCodeParam
+ ").format(" + argName + ")");
} else if ("percent".equals(subformat)) {
out.appendStringValuedExpression(
numFormatClassName + ".getPercentFormat().format(" + argName + ")");
} else {
if (curCodeParam.length() > 0) {
curCodeParam = ", " + curCodeParam;
}
out.appendStringValuedExpression(
numFormatClassName + ".getFormat(" + wrap(subformat) + curCodeParam
+ ").format(" + argName + ")");
}
return false;
}
}
/**
* An interface for accessing parameters, giving the ability to record
* accesses.
*/
private interface Parameters {
/**
* @return the count of parameters.
*/
int getCount();
/**
* Return the given parameter.
*
* @param i index of the parameter to return, 0 .. getCount() - 1
* @return parameter or null if i is out of range
*/
JParameter getParameter(int i);
/**
* Return the given parameter.
*
* @param name the name of the parameter to return
* @return parameter or null if the named parameter doesn't exist
*/
JParameter getParameter(String name);
/**
* Find the index of a parameter by name.
*
* @param name
* @return index of requested parameter or -1 if not found
*/
int getParameterIndex(String name);
}
private static class ParametersImpl implements Parameters {
private JParameter[] params;
private boolean[] seenFlag;
public ParametersImpl(JParameter[] params, boolean[] seenFlag) {
this.params = params;
this.seenFlag = seenFlag;
}
public int getCount() {
return params.length;
}
public JParameter getParameter(int i) {
if (i < 0 || i >= params.length) {
return null;
}
seenFlag[i] = true;
return params[i];
}
public JParameter getParameter(String name) {
return getParameter(getParameterIndex(name));
}
public int getParameterIndex(String name) {
for (int i = 0; i < params.length; ++i) {
if (params[i].getName().equals(name)) {
return i;
}
}
return -1;
}
}
/**
* Helper class to produce string expressions consisting of literals and
* computed values.
*/
private static class StringGenerator {
/**
* Output string buffer.
*/
private StringBuffer buf;
/**
* True if we are in the middle of a string literal.
*/
private boolean inString;
/**
* True if the method's return type is SafeHtml (and SafeHtmlBuilder is to
* be used to generate the expression); otherwise a String expression is
* generated.
*/
private final boolean returnsSafeHtml;
/**
* Initialize the StringGenerator with an output buffer.
*
* @param buf output buffer
* @param returnsSafeHtml if true, an expression of type {@link SafeHtml} is
* being generated, otherwise a {@link String}-valued expression is
* generated
*/
public StringGenerator(StringBuffer buf, boolean returnsSafeHtml) {
this.buf = buf;
inString = false;
this.returnsSafeHtml = returnsSafeHtml;
if (returnsSafeHtml) {
buf.append("new " + SAFE_HTML_BUILDER_FQCN + "()");
} else {
buf.append("new java.lang.StringBuffer()");
}
}
/**
* Append an expression to this string expression.
*
* @param expression to add
* @param isSafeHtmlTyped true if the expression is known to be of type
* {@link SafeHtml}; only relevant if this generator has been
* initialized to generate a {@link SafeHtml}-valued expression
* @param isPrimititiveTyped true if the expression is of a primitive type;
* only relevant if this generator has been initialized to generate
* a {@link SafeHtml}-valued expression
* @param needsConversionToString true if the expression is not known to be
* of type String and needs to be converted
*/
public void appendExpression(String expression, boolean isSafeHtmlTyped,
boolean isPrimititiveTyped, boolean needsConversionToString) {
if (inString) {
buf.append("\")");
inString = false;
}
/*
* SafeHtmlBuilder has append() methods for primitive types as well as for
* SafeHtml-valued expressions. For all other expression types, use
* appendEscaped(). In addition, if the expression is not known to be of
* type String, covert to String.
*/
if (returnsSafeHtml && !isSafeHtmlTyped && !isPrimititiveTyped) {
buf.append(".appendEscaped(");
if (needsConversionToString) {
buf.append("String.valueOf(");
}
} else {
buf.append(".append(");
}
buf.append(expression);
buf.append(")");
if (returnsSafeHtml && !isSafeHtmlTyped && !isPrimititiveTyped
&& needsConversionToString) {
buf.append(")");
}
}
/**
* Append part of a string literal.
*
* @param str part of string literal
*/
public void appendStringLiteral(String str) {
if (!inString) {
if (returnsSafeHtml) {
buf.append(".appendHtmlConstant(\"");
} else {
buf.append(".append(\"");
}
inString = true;
}
buf.append(str);
}
/**
* Append an expression to this string expression.
*
* @param expression to add, which the caller asserts is String-valued
*/
public void appendStringValuedExpression(String expression) {
appendExpression(expression, false, false, false);
}
/**
* Complete the string, closing an open quote and handling empty strings.
*/
public void completeString() {
if (inString) {
buf.append("\")");
}
if (returnsSafeHtml) {
buf.append(".toSafeHtml()");
} else {
buf.append(".toString()");
}
}
}
/**
* Implements {x,time...} references in MessageFormat.
*/
private static class TimeFormatter implements ValueFormatter {
public boolean format(TreeLogger logger, GwtLocale locale,
StringGenerator out, Map<String, String> formatArgs, String subformat,
String argName, JType argType, Parameters params) {
if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "Only java.util.Date acceptable for date format");
return true;
}
String tzParam = "";
String tzArg = formatArgs.get("tz");
if (tzArg != null) {
if (tzArg.startsWith("$")) {
int paramNum = params.getParameterIndex(tzArg.substring(1));
if (paramNum < 0) {
logger.log(
TreeLogger.ERROR, "Unable to resolve tz argument " + tzArg);
return true;
} else if (!"com.google.gwt.i18n.client.TimeZone".equals(
params.getParameter(paramNum).getType().getQualifiedSourceName())) {
logger.log(
TreeLogger.ERROR, "Currency code parameter must be TimeZone");
return true;
} else {
tzParam = ", arg" + paramNum;
}
} else {
tzParam = ", com.google.gwt.i18n.client.TimeZone.createTimeZone("
+ tzArg + ")";
}
}
if (subformat == null || "medium".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getMediumTimeFormat().format(" + argName
+ tzParam + ")");
} else if ("full".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getFullTimeFormat().format(" + argName
+ tzParam + ")");
} else if ("long".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getLongTimeFormat().format(" + argName
+ tzParam + ")");
} else if ("short".equals(subformat)) {
out.appendStringValuedExpression(
dtFormatClassName + ".getShortTimeFormat().format(" + argName
+ tzParam + ")");
} else {
logger.log(TreeLogger.WARN, "Use localdatetime format instead");
out.appendStringValuedExpression(
dtFormatClassName + ".getFormat(" + wrap(subformat) + ").format("
+ argName + tzParam + ")");
}
return false;
}
}
private interface ValueFormatter {
/**
* Creates code to format a value according to a format string.
*
* @param logger
* @param locale current locale
* @param out StringBuffer to append to
* @param formatArgs format-specific arguments
* @param subformat the remainder of the format string
* @param argName the name of the argument to use in the generated code
* @param argType the type of the argument
* @param params argument list or null
* @return true if a fatal error occurred (which will already be logged)
*/
boolean format(TreeLogger logger, GwtLocale locale, StringGenerator out,
Map<String, String> formatArgs, String subformat, String argName,
JType argType, Parameters params);
}
/**
* Class names, in a refactor-friendly manner.
*/
private static final String dtFormatClassName =
DateTimeFormat.class.getCanonicalName();
/**
* Fully-qualified class name of the SafeHtml interface.
*/
public static final String SAFE_HTML_FQCN = SafeHtml.class.getCanonicalName();
/**
* Fully-qualified class name of the SafeHtmlBuilder class.
*/
public static final String SAFE_HTML_BUILDER_FQCN =
SafeHtmlBuilder.class.getCanonicalName();
/**
* Map of supported formats.
*/
private static Map<String, ValueFormatter> formatters = new HashMap<
String, ValueFormatter>();
private static final String numFormatClassName =
NumberFormat.class.getCanonicalName();
/*
* Register supported formats.
*/
static {
formatters.put("date", new DateFormatter());
formatters.put("number", new NumberFormatter());
formatters.put("time", new TimeFormatter());
formatters.put("localdatetime", new LocalDateTimeFormatter());
}
private final Map<GwtLocale, Map<String, String>> listPatternCache;
/**
* Constructor for <code>MessagesMethodCreator</code>.
*
* @param classCreator associated class creator
*/
public MessagesMethodCreator(AbstractGeneratorClassCreator classCreator) {
super(classCreator);
listPatternCache = new HashMap<GwtLocale, Map<String, String>>();
}
@Override
public void createMethodFor(TreeLogger logger, JMethod m, String key,
ResourceList resourceList, GwtLocale locale)
throws UnableToCompleteException {
ResourceEntry resourceEntry = resourceList.getEntry(key);
if (resourceEntry == null) {
throw new MissingResourceException(key, resourceList);
}
JParameter[] params = m.getParameters();
int pluralParamIndex = -1;
Class<? extends PluralRule> ruleClass = null;
int numParams = params.length;
boolean[] seenFlags = new boolean[numParams];
final Parameters paramsAccessor = new ParametersImpl(params, seenFlags);
int pluralOffset = 0;
String pluralSuffix = "";
// See if any parameter is tagged as a PluralCount parameter.
for (int i = 0; i < numParams; ++i) {
PluralCount pluralCount = params[i].getAnnotation(PluralCount.class);
if (pluralCount != null) {
if (pluralParamIndex >= 0) {
throw error(logger,
m.getName() + ": there can only be one PluralCount parameter");
}
JType paramType = params[i].getType();
boolean isArray = false;
boolean isList = false;
JPrimitiveType primType = paramType.isPrimitive();
JClassType classType = paramType.isInterface();
if (classType != null) {
classType = classType.getErasedType();
if ("java.util.List".equals(classType.getQualifiedSourceName())) {
isList = true;
} else {
classType = null;
}
}
JArrayType arrayType = paramType.isArray();
if (arrayType != null) {
isArray = true;
}
if (!isList && !isArray
&& (primType == null || (primType != JPrimitiveType.INT
&& primType != JPrimitiveType.SHORT))) {
throw error(logger, m.getName()
+ ": PluralCount parameter must be int, short, array, or List");
}
if (isList) {
pluralSuffix = ".size()";
} else if (isArray) {
pluralSuffix = ".length";
}
pluralParamIndex = i;
ruleClass = pluralCount.value();
Offset offset = params[i].getAnnotation(Offset.class);
if (offset != null) {
pluralOffset = offset.value();
}
}
}
boolean isSafeHtml = m.getReturnType().getQualifiedSourceName().equals(
SAFE_HTML_FQCN);
String template = resourceEntry.getForm(null);
if (template == null) {
logger.log(TreeLogger.ERROR,
"No default form for method " + m.getName() + "' in "
+ m.getEnclosingType() + " for locale " + locale, null);
throw new UnableToCompleteException();
}
StringBuffer generated = new StringBuffer();
ArgumentChunk listArg = null;
JType elemType = null;
ListAccessor listAccessor = null;
try {
for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
if (chunk instanceof ArgumentChunk) {
ArgumentChunk argChunk = (ArgumentChunk) chunk;
if (argChunk.isList()) {
if (listArg != null) {
logger.log(TreeLogger.ERROR,
"Only one list parameter supported in "
+ m.getEnclosingType().getSimpleSourceName() + "."
+ m.getName());
throw new UnableToCompleteException();
} else {
listArg = argChunk;
int listArgNum = argChunk.getArgumentNumber();
JType listType = params[listArgNum].getType();
JClassType classType = listType.isInterface();
if (classType != null) {
if ("java.util.List".equals(
classType.getErasedType().getQualifiedSourceName())) {
listAccessor = new ListAccessorList(listArgNum);
} else {
logger.log(TreeLogger.ERROR,
"Parameters formatted as lists must be declared as java.util.List or arrays in " + m.getEnclosingType().getSimpleSourceName() + "." + m.getName());
throw new UnableToCompleteException();
}
JParameterizedType paramType = classType.isParameterized();
if (paramType != null) {
elemType = paramType.getTypeArgs()[0];
} else {
elemType = classType.getOracle().getJavaLangObject();
}
} else {
JArrayType arrayType = listType.isArray();
if (arrayType != null) {
elemType = arrayType.getComponentType();
listAccessor = new ListAccessorArray(listArgNum);
}
}
}
}
}
}
} catch (ParseException pe) {
logger.log(TreeLogger.ERROR, "Error parsing '" + template + "'", pe);
throw new UnableToCompleteException();
}
if (listArg != null) {
generateListFormattingCode(logger, locale, generated, listArg, elemType,
isSafeHtml, listAccessor, paramsAccessor);
}
if (ruleClass == null) {
if (m.getAnnotation(PluralText.class) != null) {
logger.log(TreeLogger.WARN,
"Unused @PluralText on "
+ m.getEnclosingType().getSimpleSourceName() + "." + m.getName()
+ "; did you intend to mark a @PluralCount parameter?", null);
}
} else {
if (ruleClass == PluralRule.class) {
ruleClass = DefaultRule.class;
}
PluralRule rule = createLocalizedPluralRule(
logger, m.getEnclosingType().getOracle(), ruleClass, locale);
logger.log(TreeLogger.TRACE,
"Using plural rule " + rule.getClass() + " for locale '" + locale
+ "'", null);
boolean seenEqualsValue = false;
for (String form : resourceEntry.getForms()) {
if (form.startsWith("=")) {
int value = 0;
try {
value = Integer.parseInt(form.substring(1));
} catch (NumberFormatException e) {
logger.log(TreeLogger.WARN,
"Ignoring invalid value in plural form '" + form + "'", e);
continue;
}
if (!seenEqualsValue) {
generated.append(
"switch (arg" + pluralParamIndex + pluralSuffix + ") {\n");
seenEqualsValue = true;
}
generated.append(" case " + value + ": return ");
String pluralTemplate = resourceEntry.getForm(form);
generateString(logger, locale, pluralTemplate, paramsAccessor,
generated, isSafeHtml);
generated.append(";\n");
}
}
if (seenEqualsValue) {
generated.append("}\n");
}
boolean seenPluralForm = false;
StringBuilder pluralHeader = new StringBuilder();
pluralHeader.append(PluralRule.class.getCanonicalName());
pluralHeader.append(
" rule = new " + rule.getClass().getCanonicalName() + "();\n");
if (pluralOffset != 0) {
pluralHeader.append(
"arg" + pluralParamIndex + " -= " + pluralOffset + ";\n");
}
pluralHeader.append(
"switch (rule.select(arg" + pluralParamIndex + pluralSuffix
+ ")) {\n");
PluralForm[] pluralForms = rule.pluralForms();
resourceList.setPluralForms(key, pluralForms);
// Skip default plural form (index 0); the fall-through case will handle
// it.
for (int i = 1; i < pluralForms.length; ++i) {
String pluralTemplate = resourceEntry.getForm(pluralForms[i].getName());
if (pluralTemplate != null) {
if (!seenPluralForm) {
generated.append(pluralHeader);
seenPluralForm = true;
}
generated.append(" // " + pluralForms[i].getName() + " - "
+ pluralForms[i].getDescription() + "\n");
generated.append(" case " + i + ": return ");
generateString(logger, locale, pluralTemplate, paramsAccessor,
generated, isSafeHtml);
generated.append(";\n");
} else if (pluralForms[i].getWarnIfMissing()) {
if (!seenEqualsValue) {
// If we have seen a form "=n", assume the developer knows what
// they are doing and don't warn about plural forms that aren't
// used.
logger.log(TreeLogger.WARN,
"No plural form '" + pluralForms[i].getName()
+ "' defined for method '" + m.getName() + "' in "
+ m.getEnclosingType() + " for locale " + locale, null);
}
}
}
if (seenPluralForm) {
generated.append("}\n");
}
}
generated.append("return ");
generateString(
logger, locale, template, paramsAccessor, generated, isSafeHtml);
// Generate an error if any required parameter was not used somewhere.
for (int i = 0; i < numParams; ++i) {
if (!seenFlags[i]) {
Optional optional = params[i].getAnnotation(Optional.class);
if (optional == null) {
throw error(
logger, "Required argument " + i + " not present: " + template);
}
}
}
generated.append(';');
println(generated.toString());
}
/**
* Creates an instance of a locale-specific plural rule implementation.
*
* Note that this uses TypeOracle's ability to find all subclasses of the
* supplied parent class, then uses reflection to actually load the class.
* This works because PluralRule instances are required to be translatable,
* since part of them is executed at runtime and part at compile time.
*
* @param logger TreeLogger instance
* @param oracle TypeOracle instance to use
* @param ruleClass PluralRule implementation to localize
* @param locale current locale we are compiling for
* @return an instance of a PluralRule implementation. If an appropriate
* implementation of the requested class cannot be found, an instance
* of DefaultRule is used instead as a default of last resort.
* @throws UnableToCompleteException if findDerivedClasses fails
*
* TODO: consider impact of possibly having multiple TypeOracles
*/
private PluralRule createLocalizedPluralRule(TreeLogger logger,
TypeOracle oracle, Class<? extends PluralRule> ruleClass,
GwtLocale locale) throws UnableToCompleteException {
String baseName = ruleClass.getCanonicalName();
JClassType ruleJClassType = oracle.findType(baseName);
Map<String, JClassType> matchingClasses = LocalizableLinkageCreator.findDerivedClasses(logger, ruleJClassType);
for (GwtLocale search : locale.getCompleteSearchList()) {
JClassType localizedType = matchingClasses.get(search.toString());
if (localizedType != null) {
try {
Class<?> testClass = Class.forName(
localizedType.getQualifiedBinaryName(), false,
PluralRule.class.getClassLoader());
if (PluralRule.class.isAssignableFrom(testClass)) {
return (PluralRule) testClass.newInstance();
}
} catch (ClassCastException e) {
// ignore classes of the wrong type
} catch (ClassNotFoundException e) {
// ignore missing classes
} catch (InstantiationException e) {
// skip classes we can't instantiate
} catch (IllegalAccessException e) {
// ignore inaccessible classes
}
}
}
// default of last resort
return new DefaultRule();
}
private void formatArg(TreeLogger logger, GwtLocale locale,
StringGenerator buf, ArgumentChunk argChunk, String argExpr,
JType paramType, Parameters params) throws UnableToCompleteException {
String format = argChunk.getFormat();
if (format != null) {
String subformat = argChunk.getSubFormat();
ValueFormatter formatter = formatters.get(format);
if (formatter != null) {
if (formatter.format(logger, locale, buf, argChunk.getFormatArgs(),
subformat, argExpr, paramType, params)) {
throw new UnableToCompleteException();
}
return;
}
}
// no format specified or unknown format
// have to ensure that the result is stringified if necessary
boolean isSafeHtmlTyped = SAFE_HTML_FQCN.equals(
paramType.getQualifiedSourceName());
boolean isPrimitiveType = (paramType.isPrimitive() != null);
boolean needsConversionToString = !("java.lang.String".equals(
paramType.getQualifiedSourceName()));
buf.appendExpression(
argExpr, isSafeHtmlTyped, isPrimitiveType, needsConversionToString);
}
/**
* Generate code for one list pattern.
*
* @param logger logger to use for error/warning messages
* @param locale locale we are generating code for
* @param listArg the {n,list,...} argument in the original format pattern
* @param val0 the expression defining the {0} argument in the list pattern
* @param val1 the expression defining the {1} argument in the list pattern
* @param elemType the element type of the list/array being rendered as a list * @param isSafeHtml true if the resulting string is SafeHtml
* @param listPattern the list pattern to generate code for, ie "{0}, {1}"
* @param formatSecond true if the {1} parameter needs to be formatted
* @param params parameters passed to the Messages method call
* @return a constructed string containing the code to implement the given
* list pattern
* @throws UnableToCompleteException
*/
private CharSequence formatListPattern(final TreeLogger logger,
final GwtLocale locale, final ArgumentChunk listArg, final String val0,
final String val1, final JType elemType, final boolean isSafeHtml,
String listPattern, final boolean formatSecond, final Parameters params)
throws UnableToCompleteException {
final StringBuffer buf = new StringBuffer();
final StringGenerator gen = new StringGenerator(buf, isSafeHtml);
try {
List<TemplateChunk> chunks = MessageFormatParser.parse(listPattern);
for (TemplateChunk chunk : chunks) {
chunk.accept(new DefaultTemplateChunkVisitor() {
@Override
public void visit(ArgumentChunk argChunk)
throws UnableToCompleteException {
// The {0} argument in the list pattern always needs formatting,
// but the {1} argument is the part of the list already rendered
// (in either String of SafeHtml form) unless formatSecond is true.
if (argChunk.getArgumentNumber() == 0 || formatSecond) {
formatArg(logger, locale, gen, listArg,
argChunk.getArgumentNumber() == 0 ? val0 : val1, elemType,
params);
} else {
gen.appendExpression(val1, isSafeHtml, false, false);
}
}
@Override
public void visit(StringChunk stringChunk)
throws UnableToCompleteException {
gen.appendStringLiteral(stringChunk.getString());
}
});
}
} catch (ParseException e) {
logger.log(TreeLogger.ERROR,
"Internal error: can't parse list pattern '" + listPattern
+ "' for locale " + locale, e);
throw new UnableToCompleteException();
}
gen.completeString();
return buf;
}
/**
* Generates code to format a list in a format pattern.
*
* @param logger logger to use for error/warning messages
* @param locale locale we are generating code for
* @param generated a StringBuffer holding the generated code
* @param listArg the {n,list,...} argument in the original format pattern
* @param elemType the element type of the list/array being rendered as a list
* @param isSafeHtml true if the resulting string is SafeHtml
* @param listAccessor a way to access elements of the list type supplied by
* the user
* @param params parameters passed to the Messages method call
* @throws UnableToCompleteException
*/
private void generateListFormattingCode(TreeLogger logger, GwtLocale locale,
StringBuffer generated, ArgumentChunk listArg, JType elemType,
boolean isSafeHtml, ListAccessor listAccessor, Parameters params)
throws UnableToCompleteException {
Map<String, String> listPatternParts = getListPatternParts(logger, locale);
int listArgNum = listArg.getArgumentNumber();
generated.append(
"int arg" + listArgNum + "_size = " + listAccessor.getSize() + ";\n");
if (isSafeHtml) {
generated.append(SafeHtml.class.getCanonicalName()).append(" arg").append(
listArgNum).append("_list = new ").append(
OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.class.getCanonicalName(
)).append("(\"\");\n");
} else {
generated.append("String").append(" arg" + listArgNum
+ "_list = \"\";\n");
}
generated.append("switch (arg" + listArgNum + "_size) {\n");
// TODO(jat): add support for special-cases besides 2 if CLDR ever adds them
String pairPattern = listPatternParts.get("2");
if (pairPattern != null) {
generated.append("case 2:\n");
generated.append(" arg" + listArgNum + "_list = ");
generated.append(
formatListPattern(logger, locale, listArg,
listAccessor.getElement("0"), listAccessor.getElement("1"),
elemType, isSafeHtml, pairPattern, true, params));
generated.append(";\n");
generated.append(" break;\n");
}
generated.append("default:\n");
generated.append(" int i = arg" + listArgNum + "_size;\n");
generated.append(" if (i > 0) {\n");
generated.append(" arg" + listArgNum + "_list = ");
StringGenerator buf = new StringGenerator(generated, isSafeHtml);
formatArg(logger, locale, buf, listArg, listAccessor.getElement("--i"),
elemType, params);
buf.completeString();
generated.append(";\n");
generated.append(" }\n");
generated.append(" if (i > 0) {\n");
generated.append(" arg" + listArgNum + "_list = ");
generated.append(
formatListPattern(logger, locale, listArg,
listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
elemType, isSafeHtml, listPatternParts.get("end"), false, params));
generated.append(";\n");
generated.append(" }\n");
generated.append(" while (i > 1) {\n");
generated.append(" arg" + listArgNum + "_list = ");
generated.append(formatListPattern(logger, locale, listArg,
listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
elemType, isSafeHtml, listPatternParts.get("middle"), false, params));
generated.append(" ;\n");
generated.append(" }\n");
generated.append(" if (i > 0) {\n");
generated.append(" arg" + listArgNum + "_list = ");
generated.append(formatListPattern(logger, locale, listArg,
listAccessor.getElement("--i"), "arg" + listArgNum + "_list",
elemType, isSafeHtml, listPatternParts.get("start"), false, params));
generated.append(";\n");
generated.append(" }\n");
generated.append(" break;\n");
generated.append("}\n");
}
/**
* Generate a Java string for a given MessageFormat string.
*
* @param logger
* @param template
* @param paramsAccessor
* @param outputBuf
* @throws UnableToCompleteException
*/
@SuppressWarnings("fallthrough")
private void generateString(final TreeLogger logger, final GwtLocale locale,
final String template, final Parameters paramsAccessor,
StringBuffer outputBuf, final boolean isSafeHtml)
throws UnableToCompleteException {
final StringGenerator buf = new StringGenerator(outputBuf, isSafeHtml);
final int n = paramsAccessor.getCount();
try {
for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
chunk.accept(new DefaultTemplateChunkVisitor() {
@Override
public void visit(ArgumentChunk argChunk)
throws UnableToCompleteException {
int argNumber = argChunk.getArgumentNumber();
if (argNumber >= n) {
throw error(logger,
"Argument " + argNumber + " beyond range of arguments: "
+ template);
}
JParameter param = paramsAccessor.getParameter(argNumber);
String arg = "arg" + argNumber;
if (argChunk.isList()) {
buf.appendExpression(arg + "_list", isSafeHtml, false,
false);
} else {
JType paramType = param.getType();
formatArg(logger, locale, buf, argChunk, arg, paramType,
paramsAccessor);
}
}
@Override
public void visit(StaticArgChunk staticArgChunk)
throws UnableToCompleteException {
buf.appendStringLiteral(staticArgChunk.getReplacement());
}
@Override
public void visit(StringChunk stringChunk) {
buf.appendStringLiteral(stringChunk.getString());
}
});
}
} catch (ParseException e) {
throw error(logger, e);
}
buf.completeString();
}
private Map<String, String> getListPatternParts(
TreeLogger logger, GwtLocale locale) {
Map<String, String> map = listPatternCache.get(locale);
if (map == null) {
// TODO(jat): get these from ResourceOracle instead
String baseName =
MessagesMethodCreator.class.getPackage().getName().replace('.', '/')
+ "/cldr/ListPatterns_";
ClassLoader cl = MessagesMethodCreator.class.getClassLoader();
for (GwtLocale search : locale.getCompleteSearchList()) {
String propFile = baseName + search.getAsString() + ".properties";
InputStream stream = cl.getResourceAsStream(propFile);
if (stream != null) {
try {
LocalizedPropertiesLoader loader = new LocalizedPropertiesLoader(
stream, "UTF-8");
map = new HashMap<String, String>();
loader.load(map);
break;
} catch (IOException e) {
logger.log(
TreeLogger.WARN, "Ignoring error reading file " + propFile, e);
} finally {
try {
stream.close();
} catch (IOException e) {
}
}
}
}
listPatternCache.put(locale, map);
}
return map;
}
}