/*
* 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.codegen.rebind.GwtCodeGenContext;
import com.google.gwt.codegen.server.CodeGenContext;
import com.google.gwt.codegen.server.CodeGenUtils;
import com.google.gwt.codegen.server.JavaSourceWriterBuilder;
import com.google.gwt.codegen.server.SourceWriter;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.Generator.RunsLocal;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
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.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.i18n.client.Constants;
import com.google.gwt.i18n.client.ConstantsWithLookup;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.i18n.client.Messages;
import com.google.gwt.i18n.shared.GwtLocale;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Generator used to bind classes extending the <code>Localizable</code> and
* <code>Constants</code> interfaces.
*/
@RunsLocal(requiresProperties = {"locale.queryparam", "locale", "runtime.locales", "locale.cookie"})
public class LocalizableGenerator extends Generator {
/**
* Comparator for methods - sorts first by visibility, then name, then number
* of arguments, then argument names.
*/
public class JMethodComparator implements Comparator<JMethod> {
public int compare(JMethod a, JMethod b) {
if (a.isPublic() != b.isPublic()) {
return a.isPublic() ? -1 : 1;
}
if (a.isDefaultAccess() != b.isDefaultAccess()) {
return a.isDefaultAccess() ? -1 : 1;
}
if (a.isProtected() != b.isProtected()) {
return a.isProtected() ? -1 : 1;
}
int c = a.getName().compareTo(b.getName());
if (c != 0) {
return c;
}
JParameter[] aParams = a.getParameters();
JParameter[] bParams = b.getParameters();
c = aParams.length - bParams.length;
if (c != 0) {
return c;
}
for (int i = 0; i < aParams.length; ++i) {
c = aParams[i].getName().compareTo(bParams[i].getName());
if (c != 0) {
return c;
}
}
return 0;
}
}
public static final String CONSTANTS_NAME = Constants.class.getName();
public static final String CONSTANTS_WITH_LOOKUP_NAME = ConstantsWithLookup.class.getName();
/**
* Obsolete comment for GWT metadata - needs to be removed once all
* references have been removed.
*/
public static final String GWT_KEY = "gwt.key";
public static final String MESSAGES_NAME = Messages.class.getName();
private LocalizableLinkageCreator linkageCreator = new LocalizableLinkageCreator();
/**
* Generate an implementation for the given type.
*
* @param logger error logger
* @param context generator context
* @param typeName target type name
* @return a fully-qualified classname of the generated implementation, or
* null to use the base class
* @throws UnableToCompleteException
*/
@Override
public final String generate(TreeLogger logger, GeneratorContext context,
String typeName) throws UnableToCompleteException {
// Get the current locale
PropertyOracle propertyOracle = context.getPropertyOracle();
LocaleUtils localeUtils = LocaleUtils.getInstance(logger, propertyOracle,
context);
GwtLocale locale = localeUtils.getCompileLocale();
return generate(logger, context, typeName, localeUtils, locale);
}
/**
* Generate an implementation for a given type.
*
* @param logger
* @param context
* @param typeName
* @param localeUtils
* @param locale
* @return a fully-qualified classname of the generated implementation, or
* null to use the base class
* @throws UnableToCompleteException
*/
public final String generate(TreeLogger logger, GeneratorContext context,
String typeName, LocaleUtils localeUtils, GwtLocale locale) throws UnableToCompleteException {
TypeOracle typeOracle = context.getTypeOracle();
JClassType targetClass;
try {
targetClass = typeOracle.getType(typeName);
} catch (NotFoundException e) {
logger.log(TreeLogger.ERROR, "No such type", e);
throw new UnableToCompleteException();
}
// Link current locale and interface type to correct implementation class.
String generatedClass = AbstractLocalizableImplCreator.generateConstantOrMessageClass(
logger, context, locale, targetClass);
if (generatedClass != null) {
return generatedClass;
}
// Now that we know it is a regular Localizable class, handle runtime
// locales
Set<GwtLocale> runtimeLocales = localeUtils.getRuntimeLocales();
String returnedClass = linkageCreator.linkWithImplClass(logger, targetClass, locale);
if (!runtimeLocales.isEmpty()) {
Map<String, Set<GwtLocale>> localeMap = new TreeMap<String, Set<GwtLocale>>();
Set<GwtLocale> localeSet = new TreeSet<GwtLocale>();
localeSet.add(locale);
localeMap.put(returnedClass, localeSet);
for (GwtLocale rtLocale : runtimeLocales) {
String rtClass = linkageCreator.linkWithImplClass(logger, targetClass, rtLocale);
localeSet = localeMap.get(rtClass);
if (localeSet == null) {
localeSet = new TreeSet<GwtLocale>();
localeMap.put(rtClass, localeSet);
}
localeSet.add(rtLocale);
}
if (localeMap.size() > 1) {
CodeGenContext genCtx = new GwtCodeGenContext(logger, context);
returnedClass = generateRuntimeSelection(genCtx, targetClass, returnedClass, locale,
localeMap);
}
}
return returnedClass;
}
/**
* Generate a runtime-selection implementation of the target class if needed,
* delegating all overridable methods to an instance chosen at runtime based
* on the map of locales to implementing classes.
*
* @param ctx code generator context
* @param targetClass class being GWT.create'd
* @param defaultClass the default implementation to use
* @param locale compile-time locale for this runtime selection
* @param localeMap map of target class names to the set of locales that are
* mapped to that implementation (for deterministic code generation, both
* the map and set should be ordered)
* @return fully qualified classname of the runtime selection implementation
*/
// @VisibleForTesting
String generateRuntimeSelection(CodeGenContext ctx, JClassType targetClass, String defaultClass,
GwtLocale locale, Map<String, Set<GwtLocale>> localeMap) {
String className = targetClass.getName().replace('.', '_') + '_' + locale.getAsString()
+ "_runtimeSelection";
String pkgName = targetClass.getPackage().getName();
JavaSourceWriterBuilder builder = ctx.addClass(pkgName, className);
if (builder != null) {
writeRuntimeSelection(builder, targetClass, defaultClass, locale, localeMap);
}
return pkgName + '.' + className;
}
/**
* Generate a runtime-selection implementation of the target class, delegating
* all overridable methods to an instance chosen at runtime based on the map
* of locales to implementing classes.
*
* @param builder source writer builder
* @param targetClass class being GWT.create'd
* @param defaultClass the default implementation to use
* @param locale compile-time locale for this runtime selection
* @param localeMap map of target class names to the set of locales that are
* mapped to that implementation (for deterministic code generation, both
* the map and set should be ordered)
*/
// @VisibleForTesting
void writeRuntimeSelection(JavaSourceWriterBuilder builder, JClassType targetClass,
String defaultClass, GwtLocale locale, Map<String, Set<GwtLocale>> localeMap) {
boolean isInterface = targetClass.isInterface() != null;
if (isInterface) {
builder.addImplementedInterface(targetClass.getQualifiedSourceName());
} else {
builder.setSuperclass(targetClass.getQualifiedSourceName());
}
SourceWriter writer = builder.createSourceWriter();
writer.println();
writer.println("private " + targetClass.getQualifiedSourceName() + " instance;");
for (JMethod method : collectOverridableMethods(targetClass)) {
writer.println();
if (!isInterface) {
writer.println("@Override");
}
writer.println(method.getReadableDeclaration(false, true, true, true, true) + " {");
writer.indent();
writer.println("ensureInstance();");
if (method.getReturnType() != JPrimitiveType.VOID) {
writer.print("return ");
}
writer.print("instance." + method.getName() + '(');
boolean first = true;
for (JParameter param : method.getParameters()) {
if (first) {
first = false;
} else {
writer.print(", ");
}
writer.print(param.getName());
}
writer.println(");");
writer.outdent();
writer.println("}");
}
writer.println();
writer.println("private void ensureInstance() {");
writer.indent();
writer.println("if (instance != null) {");
writer.indentln("return;");
writer.println("}");
writer.println("String locale = " + LocaleInfo.class.getCanonicalName()
+ ".getCurrentLocale().getLocaleName();");
String targetClassName = targetClass.getQualifiedSourceName() + '_' + locale.getAsString();
for (Map.Entry<String, Set<GwtLocale>> entry : localeMap.entrySet()) {
String implClassName = entry.getKey();
if (defaultClass.equals(implClassName) || targetClassName.equals(implClassName)) {
continue;
}
String prefix = "if (";
for (GwtLocale match : entry.getValue()) {
writer.print(prefix + CodeGenUtils.asStringLiteral(match.toString()) + ".equals(locale)");
prefix = "\n || ";
}
writer.println(") {");
writer.indent();
writer.println("instance = new " + implClassName + "();");
writer.println("return;");
writer.outdent();
writer.println("}");
}
writer.println("instance = new " + defaultClass + "();");
writer.outdent();
writer.println("}");
writer.close();
}
/**
* @param targetClass
* @return a set of overrideable methods, in the order they should appear in
* generated source
*/
private TreeSet<JMethod> collectOverridableMethods(JClassType targetClass) {
TreeSet<JMethod> overrides = new TreeSet<JMethod>(new JMethodComparator());
Set<String> seenSignatures = new HashSet<String>();
// collect methods from superclass until we get to object
for (JClassType clazz = targetClass; clazz != null; clazz = clazz.getSuperclass()) {
if ("java.lang.Object".equals(clazz.getQualifiedSourceName())) {
break;
}
for (JMethod method : clazz.getMethods()) {
String signature = getSignature(method);
if (!seenSignatures.contains(signature)) {
seenSignatures.add(signature);
if (!method.isPrivate() && !method.isFinal() && !method.isStatic()) {
overrides.add(method);
}
}
}
}
// collect methods from superinterfaces until hitting Localizable
ArrayList<JClassType> todo = new ArrayList<JClassType>();
todo.addAll(Arrays.asList(targetClass.getImplementedInterfaces()));
while (!todo.isEmpty()) {
JClassType clazz = todo.remove(0);
for (JMethod method : clazz.getMethods()) {
String signature = getSignature(method);
if (!seenSignatures.contains(signature)) {
seenSignatures.add(signature);
if (!method.isPrivate() && !method.isFinal() && !method.isStatic()) {
overrides.add(method);
}
}
}
if (!"Localizable".equals(clazz.getSimpleSourceName())) {
todo.addAll(Arrays.asList(clazz.getImplementedInterfaces()));
}
}
return overrides;
}
/**
* @param method
* @return JNI signature of the method
*/
private String getSignature(JMethod method) {
StringBuilder buf = new StringBuilder();
buf.append(method.getName()).append('(');
for (JParameter param : method.getParameters()) {
JType type = param.getType();
buf.append(type.getJNISignature());
}
return buf.append(')').toString();
}
}