blob: a6725e2df6a95e371a76f99bddad09e77f5f1d48 [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.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
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.NumberFormat;
import com.google.gwt.i18n.client.PluralRule;
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.PluralRule.PluralForm;
import com.google.gwt.i18n.client.impl.plurals.DefaultRule;
import com.google.gwt.i18n.rebind.AbstractResource.ResourceList;
import com.google.gwt.i18n.rebind.MessageFormatParser.ArgumentChunk;
import com.google.gwt.i18n.rebind.MessageFormatParser.TemplateChunk;
import com.google.gwt.i18n.shared.GwtLocale;
import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
import com.google.gwt.user.rebind.AbstractMethodCreator;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
/**
* Creator for methods of the form String getX(arg0,...,argN).
*/
class MessagesMethodCreator extends AbstractMethodCreator {
/**
* Implements {x,date...} references in MessageFormat.
*/
private static class DateFormatter implements ValueFormatter {
public String format(StringGenerator out, String subformat, String argName,
JType argType) {
if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
return "Only java.util.Date acceptable for date format";
}
if (subformat == null || "medium".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getMediumDateFormat()"
+ ".format(" + argName + ")", true);
} else if ("full".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getFullDateFormat().format("
+ argName + ")", true);
} else if ("long".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getLongDateFormat().format("
+ argName + ")", true);
} else if ("short".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getShortDateFormat()"
+ ".format(" + argName + ")", true);
} else {
out.appendExpression(dtFormatClassName + ".getFormat("
+ wrap(subformat) + ").format(" + argName + ")", true);
}
return null;
}
}
/**
* Implements {x,number...} references in MessageFormat.
*/
private static class NumberFormatter implements ValueFormatter {
public String format(StringGenerator out, String subformat, String argName,
JType argType) {
JPrimitiveType argPrimType = argType.isPrimitive();
if (argPrimType == null || argPrimType == JPrimitiveType.BOOLEAN
|| argPrimType == JPrimitiveType.VOID) {
return "Illegal argument type for number format";
}
if (subformat == null) {
out.appendExpression(numFormatClassName + ".getDecimalFormat().format("
+ argName + ")", true);
} else if ("integer".equals(subformat)) {
out.appendExpression(numFormatClassName + ".getIntegerFormat().format("
+ argName + ")", true);
} else if ("currency".equals(subformat)) {
out.appendExpression(numFormatClassName
+ ".getCurrencyFormat().format(" + argName + ")", true);
} else if ("percent".equals(subformat)) {
out.appendExpression(numFormatClassName + ".getPercentFormat().format("
+ argName + ")", true);
} else {
out.appendExpression(numFormatClassName + ".getFormat("
+ wrap(subformat) + ").format(" + argName + ")", true);
}
return null;
}
}
/**
* 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 we have produced any output.
*/
private boolean producedOutput;
/**
* Initialize the StringGenerator with an output buffer.
*
* @param buf output buffer
*/
public StringGenerator(StringBuffer buf) {
this.buf = buf;
producedOutput = false;
inString = false;
}
/**
* Append an expression to this string expression.
*
* @param expression to add
*/
public void appendExpression(String expression) {
appendExpression(expression, false);
}
/**
* Append an expression to this string expression.
*
* @param expression to add
* @param knownToBeString true if the expression is known to be of string
* type; otherwise ""+ will be prepended to ensure it.
*/
public void appendExpression(String expression, boolean knownToBeString) {
if (inString) {
buf.append('"');
inString = false;
producedOutput = true;
}
if (producedOutput) {
buf.append(" + ");
} else if (!knownToBeString) {
buf.append("\"\" + ");
}
buf.append(expression);
producedOutput = true;
}
/**
* Append part of a string literal.
*
* @param ch part of string literal
*/
public void appendStringLiteral(char ch) {
if (!inString) {
if (producedOutput) {
buf.append(" + ");
} else {
producedOutput = true;
}
buf.append('"');
inString = true;
}
buf.append(ch);
}
/**
* Append part of a string literal.
*
* @param str part of string literal
*/
public void appendStringLiteral(String str) {
if (!inString) {
if (producedOutput) {
buf.append(" + ");
} else {
producedOutput = true;
}
buf.append('"');
inString = true;
}
buf.append(str);
}
/**
* Complete the string, closing an open quote and handling empty strings.
*/
public void completeString() {
if (inString) {
buf.append('\"');
} else if (!producedOutput) {
buf.append("\"\"");
}
}
}
/**
* Implements {x,time...} references in MessageFormat.
*/
private static class TimeFormatter implements ValueFormatter {
public String format(StringGenerator out, String subformat, String argName,
JType argType) {
if (!"java.util.Date".equals(argType.getQualifiedSourceName())) {
return "Only java.util.Date acceptable for date format";
}
if (subformat == null || "medium".equals(subformat)) {
out.appendExpression(dtFormatClassName
+ ".getMediumTimeFormat().format(" + argName + ")", true);
} else if ("full".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getFullTimeFormat().format("
+ argName + ")", true);
} else if ("long".equals(subformat)) {
out.appendExpression(dtFormatClassName + ".getLongTimeFormat().format("
+ argName + ")", true);
} else if ("short".equals(subformat)) {
out.appendExpression(dtFormatClassName
+ ".getShortTimeFormat().format(" + argName + ")", true);
} else {
out.appendExpression(dtFormatClassName + ".getFormat("
+ wrap(subformat) + ").format(" + argName + ")", true);
}
return null;
}
}
private interface ValueFormatter {
/**
* Creates code to format a value according to a format string.
*
* @param out StringBuffer to append to
* @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
* @return null if no error or an appropriate error message
*/
String format(StringGenerator out, String subformat, String argName,
JType argType);
}
/**
* Class names, in a refactor-friendly manner.
*/
private static final String dtFormatClassName = DateTimeFormat.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());
// TODO: implement ChoiceFormat and PluralFormat
}
/**
* Constructor for <code>MessagesMethodCreator</code>.
*
* @param classCreator associated class creator
*/
public MessagesMethodCreator(AbstractGeneratorClassCreator classCreator) {
super(classCreator);
}
@Override
public void createMethodFor(TreeLogger logger, JMethod m, String key,
ResourceList resourceList, GwtLocale locale)
throws UnableToCompleteException {
JParameter[] params = m.getParameters();
int pluralParamIndex = -1;
Class<? extends PluralRule> ruleClass = null;
int numParams = params.length;
boolean[] seenFlags = new boolean[numParams];
// 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");
}
JPrimitiveType primType = params[i].getType().isPrimitive();
if (primType != JPrimitiveType.INT && primType != JPrimitiveType.SHORT) {
throw error(logger, m.getName()
+ ": PluralCount parameter must be int or short");
}
pluralParamIndex = i;
ruleClass = pluralCount.value();
}
}
StringBuffer generated = new StringBuffer();
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);
generated.append(PluralRule.class.getCanonicalName());
generated.append(" rule = new " + rule.getClass().getCanonicalName()
+ "();\n");
generated.append("switch (rule.select("
+ params[pluralParamIndex].getName() + ")) {\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 template = resourceList.getStringExt(key,
pluralForms[i].getName());
if (template != null) {
generated.append(" // " + pluralForms[i].getName() + " - "
+ pluralForms[i].getDescription() + "\n");
generated.append(" case " + i + ": return ");
generateString(logger, template, params, seenFlags, generated);
generated.append(";\n");
} else if (pluralForms[i].getWarnIfMissing()) {
logger.log(TreeLogger.WARN, "No plural form '"
+ pluralForms[i].getName() + "' defined for method '"
+ m.getName() + "' in " + m.getEnclosingType() + " for locale "
+ locale, null);
}
}
generated.append("}\n");
}
generated.append("return ");
String template = resourceList.getRequiredStringExt(key, null);
generateString(logger, template, params, seenFlags, generated);
// 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 " + params[i].getName()
+ " 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();
}
/**
* Generate a Java string for a given MessageFormat string.
*
* @param logger
* @param template
* @param params
* @param seenFlag
* @param outputBuf
* @throws UnableToCompleteException
*/
@SuppressWarnings("fallthrough")
private void generateString(TreeLogger logger, String template,
JParameter[] params, boolean[] seenFlag, StringBuffer outputBuf)
throws UnableToCompleteException {
StringGenerator buf = new StringGenerator(outputBuf);
try {
for (TemplateChunk chunk : MessageFormatParser.parse(template)) {
if (chunk.isLiteral()) {
buf.appendStringLiteral(chunk.getString());
} else if (chunk instanceof ArgumentChunk) {
ArgumentChunk argChunk = (ArgumentChunk) chunk;
int argNumber = argChunk.getArgumentNumber();
if (argNumber >= params.length) {
throw error(logger, "Argument " + argNumber
+ " beyond range of arguments: " + template);
}
seenFlag[argNumber] = true;
String arg = params[argNumber].getName();
String format = argChunk.getFormat();
if (format != null) {
String subformat = argChunk.getSubFormat();
ValueFormatter formatter = formatters.get(format);
if (formatter != null) {
String err = formatter.format(buf, subformat, arg,
params[argNumber].getType());
if (err != null) {
throw error(logger, err);
}
continue;
}
}
// no format specified or unknown format
// have to ensure that the result is stringified if necessary
buf.appendExpression(
arg,
"java.lang.String".equals(params[argNumber].getType().getQualifiedSourceName()));
} else {
throw error(logger, "Unexpected result from parsing template "
+ template);
}
}
} catch (ParseException e) {
throw error(logger, e);
}
buf.completeString();
}
}