| /* |
| * 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(); |
| } |
| } |