blob: 90ee6330c14d8b5427ecdffa080ee3b535d7b792 [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 static com.google.gwt.i18n.rebind.AnnotationUtil.getClassAnnotation;
import com.google.gwt.core.ext.TreeLogger;
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.JRawType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.i18n.client.Constants.DefaultBooleanValue;
import com.google.gwt.i18n.client.Constants.DefaultDoubleValue;
import com.google.gwt.i18n.client.Constants.DefaultFloatValue;
import com.google.gwt.i18n.client.Constants.DefaultIntValue;
import com.google.gwt.i18n.client.Constants.DefaultStringArrayValue;
import com.google.gwt.i18n.client.Constants.DefaultStringMapValue;
import com.google.gwt.i18n.client.Constants.DefaultStringValue;
import com.google.gwt.i18n.client.LocalizableResource.DefaultLocale;
import com.google.gwt.i18n.client.LocalizableResource.Description;
import com.google.gwt.i18n.client.LocalizableResource.GenerateKeys;
import com.google.gwt.i18n.client.LocalizableResource.Key;
import com.google.gwt.i18n.client.LocalizableResource.Meaning;
import com.google.gwt.i18n.client.Messages.AlternateMessage;
import com.google.gwt.i18n.client.Messages.DefaultMessage;
import com.google.gwt.i18n.client.Messages.Example;
import com.google.gwt.i18n.client.Messages.Optional;
import com.google.gwt.i18n.client.Messages.PluralCount;
import com.google.gwt.i18n.client.Messages.Select;
import com.google.gwt.i18n.client.PluralRule;
import com.google.gwt.i18n.server.KeyGenerator;
import com.google.gwt.i18n.server.Message;
import com.google.gwt.i18n.server.MessageInterface;
import com.google.gwt.i18n.server.MessageUtils;
import com.google.gwt.i18n.server.MessageUtils.KeyGeneratorException;
import com.google.gwt.i18n.shared.GwtLocale;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* AbstractResource implementation which looks up text annotations on classes.
*/
@SuppressWarnings("deprecation")
public class AnnotationsResource extends AbstractResource {
/**
* An exception indicating there was some problem with an annotation.
*
* A caller receiving this exception should log the human-readable message and
* treat it as a fatal error.
*/
public static class AnnotationsError extends Exception {
public AnnotationsError(String msg) {
super(msg);
}
}
/**
* Class for argument information, used for export.
*/
public static class ArgumentInfo {
public String example;
public boolean isPluralCount;
public String name;
public boolean optional;
public Class<? extends PluralRule> pluralRuleClass;
public boolean isSelect;
public ArgumentInfo(String name) {
this.name = name;
}
}
private static class EntryWrapper implements ResourceEntry {
private final String key;
private final MethodEntry entry;
public EntryWrapper(String key, MethodEntry entry) {
this.key = key;
this.entry = entry;
}
public String getForm(String form) {
return form == null ? entry.text : entry.altText.get(form);
}
public Collection<String> getForms() {
return entry.altText.keySet();
}
public String getKey() {
return key;
}
}
/**
* Class to keep annotation information about a particular method.
*/
private static class MethodEntry {
// Strings used in toString for formatting.
private static final String DETAILS_PREFIX = " (";
private static final String DETAILS_SEPARATOR = ", ";
public ArrayList<ArgumentInfo> arguments;
public String description;
public String meaning;
public Map<String, String> altText;
public String text;
public MethodEntry(String text, String meaning) {
this.text = text;
this.meaning = meaning;
altText = new HashMap<String, String>();
arguments = new ArrayList<ArgumentInfo>();
}
public void addAlternateText(String altForm, String altMessage) {
altText.put(altForm, altMessage);
}
public ArgumentInfo addArgument(String argName) {
ArgumentInfo argInfo = new ArgumentInfo(argName);
arguments.add(argInfo);
return argInfo;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(text);
String prefix = DETAILS_PREFIX;
if (meaning != null) {
buf.append(prefix).append("meaning=").append(meaning);
prefix = DETAILS_SEPARATOR;
}
if (description != null) {
buf.append(prefix).append("desc=").append(description);
prefix = DETAILS_SEPARATOR;
}
if (DETAILS_SEPARATOR == prefix) {
buf.append(')');
}
return buf.toString();
}
}
/**
* Returns the key for a given method.
*
* If null is returned, an error message has already been logged.
*
* @param logger
* @param keyGenerator
* @param method
* @param isConstants
* @return null if unable to get or compute the key for this method, otherwise
* the key is returned
*/
public static String getKey(TreeLogger logger, KeyGenerator keyGenerator,
JMethod method, boolean isConstants) {
Key key = method.getAnnotation(Key.class);
if (key != null) {
return key.value();
}
String text;
try {
text = getTextString(method, null, isConstants);
} catch (AnnotationsError e) {
return null;
}
if (keyGenerator == null) {
// Gracefully handle the case of an invalid KeyGenerator classname
return null;
}
MessageInterface msgIntf = new KeyGenMessageInterface(
method.getEnclosingType());
Message msg = new KeyGenMessage(method);
String keyStr = keyGenerator.generateKey(msg);
if (keyStr == null) {
if (text == null) {
logger.log(
TreeLogger.ERROR,
"Key generator "
+ keyGenerator.getClass().getName()
+ " requires the default value be specified in an annotation for method "
+ method.getName(), null);
} else {
logger.log(TreeLogger.ERROR, "Key generator "
+ keyGenerator.getClass().getName()
+ " was unable to compute a key value for method "
+ method.getName(), null);
}
}
return keyStr;
}
/**
* Returns a suitable key generator for the specified class.
*
* @param targetClass
* @return KeyGenerator instance, guaranteed to not be null
* @throws AnnotationsError if a specified KeyGenerator cannot be created
*/
public static KeyGenerator getKeyGenerator(JClassType targetClass)
throws AnnotationsError {
GenerateKeys generator = getClassAnnotation(targetClass, GenerateKeys.class);
try {
return MessageUtils.getKeyGenerator(generator);
} catch (KeyGeneratorException e) {
throw new AnnotationsError(e.getMessage());
}
}
/**
* Return the text string from annotations for a particular method.
*
* @param method the method to retrieve text
* @param map if not null, add keys for DefaultStringMapValue to this map
* @param isConstants true if the method is in a subinterface of Constants
* @throws AnnotationsError if the annotation usage is incorrect
* @return the text value to use for this method, as if read from a properties
* file, or null if there are no annotations.
*/
private static String getTextString(JMethod method,
Map<String, MethodEntry> map, boolean isConstants)
throws AnnotationsError {
JType returnType = method.getReturnType();
DefaultMessage defaultText = method.getAnnotation(DefaultMessage.class);
DefaultStringValue stringValue = method.getAnnotation(DefaultStringValue.class);
DefaultStringArrayValue stringArrayValue = method.getAnnotation(DefaultStringArrayValue.class);
DefaultStringMapValue stringMapValue = method.getAnnotation(DefaultStringMapValue.class);
DefaultIntValue intValue = method.getAnnotation(DefaultIntValue.class);
DefaultFloatValue floatValue = method.getAnnotation(DefaultFloatValue.class);
DefaultDoubleValue doubleValue = method.getAnnotation(DefaultDoubleValue.class);
DefaultBooleanValue booleanValue = method.getAnnotation(DefaultBooleanValue.class);
int constantsCount = 0;
if (stringValue != null) {
constantsCount++;
if (!returnType.getQualifiedSourceName().equals("java.lang.String")) {
throw new AnnotationsError(
"@DefaultStringValue can only be used with a method returning String");
}
}
if (stringArrayValue != null) {
constantsCount++;
JArrayType arrayType = returnType.isArray();
if (arrayType == null
|| !arrayType.getComponentType().getQualifiedSourceName().equals(
"java.lang.String")) {
throw new AnnotationsError(
"@DefaultStringArrayValue can only be used with a method returning String[]");
}
}
if (stringMapValue != null) {
constantsCount++;
JRawType rawType = returnType.getErasedType().isRawType();
boolean error = false;
if (rawType == null
|| !rawType.getQualifiedSourceName().equals("java.util.Map")) {
error = true;
} else {
JParameterizedType paramType = returnType.isParameterized();
if (paramType != null) {
JType[] args = paramType.getTypeArgs();
if (args.length != 2
|| !args[0].getQualifiedSourceName().equals("java.lang.String")
|| !args[1].getQualifiedSourceName().equals("java.lang.String")) {
error = true;
}
}
}
if (error) {
throw new AnnotationsError(
"@DefaultStringMapValue can only be used with a method "
+ "returning Map or Map<String,String>");
}
}
if (intValue != null) {
constantsCount++;
JPrimitiveType primType = returnType.isPrimitive();
if (primType != JPrimitiveType.INT) {
throw new AnnotationsError(
"@DefaultIntValue can only be used with a method returning int");
}
}
if (floatValue != null) {
constantsCount++;
JPrimitiveType primType = returnType.isPrimitive();
if (primType != JPrimitiveType.FLOAT) {
throw new AnnotationsError(
"@DefaultFloatValue can only be used with a method returning float");
}
}
if (doubleValue != null) {
constantsCount++;
JPrimitiveType primType = returnType.isPrimitive();
if (primType != JPrimitiveType.DOUBLE) {
throw new AnnotationsError(
"@DefaultDoubleValue can only be used with a method returning double");
}
}
if (booleanValue != null) {
constantsCount++;
JPrimitiveType primType = returnType.isPrimitive();
if (primType != JPrimitiveType.BOOLEAN) {
throw new AnnotationsError(
"@DefaultBooleanValue can only be used with a method returning boolean");
}
}
if (!isConstants) {
if (constantsCount > 0) {
throw new AnnotationsError(
"@Default*Value is not permitted on a Messages interface; see @DefaultMessage");
}
if (defaultText != null) {
return defaultText.value();
}
} else {
if (defaultText != null) {
throw new AnnotationsError(
"@DefaultMessage is not permitted on a Constants interface; see @Default*Value");
}
if (constantsCount > 1) {
throw new AnnotationsError(
"No more than one @Default*Value annotation may be used on a method");
}
if (stringValue != null) {
return stringValue.value();
} else if (intValue != null) {
return Integer.toString(intValue.value());
} else if (floatValue != null) {
return Float.toString(floatValue.value());
} else if (doubleValue != null) {
return Double.toString(doubleValue.value());
} else if (booleanValue != null) {
return Boolean.toString(booleanValue.value());
} else if (stringArrayValue != null) {
StringBuilder buf = new StringBuilder();
boolean firstString = true;
for (String str : stringArrayValue.value()) {
str = str.replace("\\", "\\\\");
str = str.replace(",", "\\,");
if (!firstString) {
buf.append(',');
} else {
firstString = false;
}
buf.append(str);
}
return buf.toString();
} else if (stringMapValue != null) {
StringBuilder buf = new StringBuilder();
boolean firstString = true;
String[] entries = stringMapValue.value();
if ((entries.length & 1) != 0) {
throw new AnnotationsError(
"Odd number of strings supplied to @DefaultStringMapValue");
}
for (int i = 0; i < entries.length; i += 2) {
String key = entries[i];
String value = entries[i + 1];
if (map != null) {
// add key=value part to map
MethodEntry entry = new MethodEntry(value, null);
map.put(key, entry);
}
// add the key to the master entry
key = key.replace("\\", "\\\\");
key = key.replace(",", "\\,");
if (!firstString) {
buf.append(',');
} else {
firstString = false;
}
buf.append(key);
}
return buf.toString();
}
}
return null;
}
private Map<String, MethodEntry> map;
/**
* Create a resource that supplies data from i18n-related annotations.
*
* @param logger
* @param clazz
* @param locale
* @param isConstants
* @throws AnnotationsError if there is a fatal error while processing
* annotations
*/
public AnnotationsResource(TreeLogger logger, JClassType clazz,
GwtLocale locale, boolean isConstants) throws AnnotationsError {
super(locale);
KeyGenerator keyGenerator = getKeyGenerator(clazz);
map = new HashMap<String, MethodEntry>();
setPath(clazz.getQualifiedSourceName());
String defLocaleValue = null;
// If the class has an embedded locale in it, use that for the default
String className = clazz.getSimpleSourceName();
int underscore = className.indexOf('_');
if (underscore >= 0) {
defLocaleValue = className.substring(underscore + 1);
}
// If there is an annotation declaring the default locale, use that
DefaultLocale defLocaleAnnot = getClassAnnotation(clazz,
DefaultLocale.class);
if (defLocaleAnnot != null) {
defLocaleValue = defLocaleAnnot.value();
}
GwtLocale defLocale = LocaleUtils.getLocaleFactory().fromString(
defLocaleValue);
if (!locale.isDefault() && !locale.equals(defLocale)) {
logger.log(TreeLogger.WARN, "Default locale " + defLocale + " on "
+ clazz.getQualifiedSourceName() + " doesn't match " + locale);
return;
}
matchLocale = defLocale;
for (JMethod method : clazz.getMethods()) {
String meaningString = null;
Meaning meaning = method.getAnnotation(Meaning.class);
if (meaning != null) {
meaningString = meaning.value();
}
String textString = getTextString(method, map, isConstants);
if (textString == null) {
// ignore ones without some value annotation
continue;
}
String key = null;
Key keyAnnot = method.getAnnotation(Key.class);
if (keyAnnot != null) {
key = keyAnnot.value();
} else {
Message msg = new KeyGenMessage(method);
key = keyGenerator.generateKey(msg);
if (key == null) {
throw new AnnotationsError("Could not compute key for "
+ method.getEnclosingType().getQualifiedSourceName() + "."
+ method.getName() + " using " + keyGenerator);
}
}
MethodEntry entry = new MethodEntry(textString, meaningString);
map.put(key, entry);
Description description = method.getAnnotation(Description.class);
if (description != null) {
entry.description = description.value();
}
// use full name to avoid deprecation warnings in the imports
com.google.gwt.i18n.client.Messages.PluralText pluralText = method
.getAnnotation(com.google.gwt.i18n.client.Messages.PluralText.class);
if (pluralText != null) {
String[] pluralForms = pluralText.value();
if ((pluralForms.length & 1) != 0) {
throw new AnnotationsError(
"Odd number of strings supplied to @PluralText: must be"
+ " pairs of form names and messages");
}
for (int i = 0; i + 1 < pluralForms.length; i += 2) {
entry.addAlternateText(pluralForms[i], pluralForms[i + 1]);
}
}
AlternateMessage altMsg = method.getAnnotation(AlternateMessage.class);
if (altMsg != null) {
if (pluralText != null) {
throw new AnnotationsError("May not have both @AlternateMessage"
+ " and @PluralText");
}
String[] altForms = altMsg.value();
if ((altForms.length & 1) != 0) {
throw new AnnotationsError(
"Odd number of strings supplied to @AlternateMessage: must be"
+ " pairs of values and messages");
}
for (int i = 0; i + 1 < altForms.length; i += 2) {
entry.addAlternateText(altForms[i], altForms[i + 1]);
}
}
for (JParameter param : method.getParameters()) {
ArgumentInfo argInfo = entry.addArgument(param.getName());
Optional optional = param.getAnnotation(Optional.class);
if (optional != null) {
argInfo.optional = true;
}
PluralCount pluralCount = param.getAnnotation(PluralCount.class);
if (pluralCount != null) {
argInfo.isPluralCount = true;
}
Example example = param.getAnnotation(Example.class);
if (example != null) {
argInfo.example = example.value();
}
Select select = param.getAnnotation(Select.class);
if (select != null) {
argInfo.isSelect = true;
}
}
}
}
@Override
public void addToKeySet(Set<String> s) {
s.addAll(map.keySet());
}
public Iterable<ArgumentInfo> argumentsIterator(String key) {
MethodEntry entry = map.get(key);
return entry != null ? entry.arguments : null;
}
public String getDescription(String key) {
MethodEntry entry = map.get(key);
return entry == null ? null : entry.description;
}
@Override
public ResourceEntry getEntry(String key) {
MethodEntry entry = map.get(key);
return entry == null ? null : new EntryWrapper(key, entry);
}
@Override
public Collection<String> getExtensions(String key) {
MethodEntry entry = map.get(key);
return entry == null ? new ArrayList<String>() : entry.altText.keySet();
}
public String getMeaning(String key) {
MethodEntry entry = map.get(key);
return entry == null ? null : entry.meaning;
}
@Override
public String getStringExt(String key, String extension) {
MethodEntry entry = map.get(key);
if (entry == null) {
return null;
}
if (extension != null) {
return entry.altText.get(extension);
} else {
return entry.text;
}
}
@Override
public boolean notEmpty() {
return !map.isEmpty();
}
@Override
public String toString() {
return "Annotations from class " + getPath() + " @" + getMatchLocale();
}
}