| /* |
| * 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.BadPropertyValueException; |
| import com.google.gwt.core.ext.GeneratorContext; |
| import com.google.gwt.core.ext.SelectionProperty; |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility; |
| import com.google.gwt.core.ext.typeinfo.JClassType; |
| import com.google.gwt.core.ext.typeinfo.JMethod; |
| import com.google.gwt.core.ext.typeinfo.NotFoundException; |
| import com.google.gwt.core.ext.typeinfo.TypeOracle; |
| import com.google.gwt.i18n.client.LocalizableResource.Generate; |
| import com.google.gwt.i18n.client.LocalizableResource.Key; |
| import com.google.gwt.i18n.rebind.AbstractResource.ResourceList; |
| import com.google.gwt.i18n.rebind.AnnotationsResource.AnnotationsError; |
| import com.google.gwt.i18n.rebind.format.MessageCatalogFormat; |
| import com.google.gwt.i18n.server.KeyGenerator; |
| import com.google.gwt.i18n.server.MessageCatalogFactory; |
| import com.google.gwt.i18n.server.MessageCatalogFactory.Context; |
| import com.google.gwt.i18n.server.MessageCatalogFactory.Writer; |
| import com.google.gwt.i18n.server.MessageInterface; |
| import com.google.gwt.i18n.server.MessageProcessingException; |
| import com.google.gwt.i18n.shared.GwtLocale; |
| import com.google.gwt.i18n.shared.GwtLocaleFactory; |
| import com.google.gwt.user.rebind.AbstractGeneratorClassCreator; |
| import com.google.gwt.user.rebind.AbstractMethodCreator; |
| import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; |
| import com.google.gwt.user.rebind.SourceWriter; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.util.MissingResourceException; |
| |
| /** |
| * Represents generic functionality needed for <code>Constants</code> and |
| * <code>Messages</code> classes. |
| */ |
| abstract class AbstractLocalizableImplCreator extends |
| AbstractGeneratorClassCreator { |
| |
| public static class MessageCatalogContextImpl |
| implements Context { |
| |
| private final GeneratorContext context; |
| private final TreeLogger logger; |
| |
| public MessageCatalogContextImpl(GeneratorContext context, |
| TreeLogger logger) { |
| this.context = context; |
| this.logger = logger; |
| } |
| |
| public OutputStream createBinaryFile(String catalogName) { |
| try { |
| final OutputStream ostr = context.tryCreateResource(logger, catalogName); |
| if (ostr != null) { |
| // wrap the stream so we can commit the resource on close |
| return new OutputStream() { |
| |
| @Override |
| public void close() throws IOException { |
| try { |
| context.commitResource(logger, ostr).setVisibility( |
| Visibility.Private); |
| } catch (UnableToCompleteException e) { |
| // error already logged, anything more to do? |
| } |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| ostr.write(b, off, len); |
| } |
| |
| @Override |
| public void write(int b) throws IOException { |
| ostr.write(b); |
| } |
| }; |
| } |
| } catch (UnableToCompleteException e) { |
| // error already logged, anything more to do? |
| } |
| return null; |
| } |
| |
| public PrintWriter createTextFile(String catalogName, String charSet) { |
| OutputStream outStr = createBinaryFile(catalogName); |
| if (outStr != null) { |
| try { |
| return new PrintWriter(new BufferedWriter( |
| new OutputStreamWriter(outStr, "UTF-8")), false); |
| } catch (UnsupportedEncodingException e) { |
| error("UTF-8 not supported", e); |
| } |
| } |
| return null; |
| } |
| |
| public void error(String msg) { |
| logger.log(TreeLogger.ERROR, msg); |
| } |
| |
| public void error(String msg, Throwable cause) { |
| logger.log(TreeLogger.ERROR, msg, cause); |
| } |
| |
| public GwtLocaleFactory getLocaleFactory() { |
| return LocaleUtils.getLocaleFactory(); |
| } |
| |
| public void warning(String msg) { |
| logger.log(TreeLogger.WARN, msg); |
| } |
| |
| public void warning(String msg, Throwable cause) { |
| logger.log(TreeLogger.WARN, msg, cause); |
| } |
| } |
| |
| static String generateConstantOrMessageClass(TreeLogger logger, |
| GeneratorContext context, GwtLocale locale, JClassType targetClass) |
| throws UnableToCompleteException { |
| TypeOracle oracle = context.getTypeOracle(); |
| JClassType constantsClass; |
| JClassType messagesClass; |
| JClassType constantsWithLookupClass; |
| boolean seenError = false; |
| try { |
| constantsClass = oracle.getType(LocalizableGenerator.CONSTANTS_NAME); |
| constantsWithLookupClass = oracle.getType(LocalizableGenerator.CONSTANTS_WITH_LOOKUP_NAME); |
| messagesClass = oracle.getType(LocalizableGenerator.MESSAGES_NAME); |
| } catch (NotFoundException e) { |
| // Should never happen in practice. |
| throw error(logger, e); |
| } |
| |
| String name = targetClass.getName(); |
| String packageName = targetClass.getPackage().getName(); |
| |
| // Make sure the interface being rebound extends either Constants or |
| // Messages. |
| boolean assignableToConstants = constantsClass.isAssignableFrom(targetClass); |
| boolean assignableToMessages = messagesClass.isAssignableFrom(targetClass); |
| if (!assignableToConstants && !assignableToMessages) { |
| // Let the implementation generator handle this interface. |
| return null; |
| } |
| |
| // Make sure that they don't try to extend both Messages and Constants. |
| if (assignableToConstants && assignableToMessages) { |
| throw error(logger, name + " cannot extend both Constants and Messages"); |
| } |
| |
| // Make sure that the type being rebound is in fact an interface. |
| if (targetClass.isInterface() == null) { |
| throw error(logger, name + " must be an interface"); |
| } |
| |
| ResourceList resourceList = null; |
| try { |
| resourceList = ResourceFactory.getBundle(logger, targetClass, locale, |
| assignableToConstants, context); |
| } catch (MissingResourceException e) { |
| throw error(logger, |
| "Localization failed; there must be at least one resource accessible through" |
| + " the classpath in package '" + packageName |
| + "' whose base name is '" |
| + ResourceFactory.getResourceName(targetClass) + "'"); |
| } catch (IllegalArgumentException e) { |
| // A bad key can generate an illegal argument exception. |
| throw error(logger, e.getMessage()); |
| } |
| |
| // generated implementations for interface X will be named X_, X_en, |
| // X_en_CA, etc. |
| GwtLocale generatedLocale = resourceList.findLeastDerivedLocale(logger, |
| locale); |
| String localeSuffix = String.valueOf(ResourceFactory.LOCALE_SEPARATOR); |
| localeSuffix += generatedLocale.getAsString(); |
| // Use _ rather than "." in class name, cannot use $ |
| String resourceName = targetClass.getName().replace('.', '_'); |
| String className = resourceName + localeSuffix; |
| PrintWriter pw = context.tryCreate(logger, packageName, className); |
| if (pw != null) { |
| ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory( |
| packageName, className); |
| factory.addImplementedInterface(targetClass.getQualifiedSourceName()); |
| SourceWriter writer = factory.createSourceWriter(context, pw); |
| // Now that we have all the information set up, process the class |
| if (constantsWithLookupClass.isAssignableFrom(targetClass)) { |
| ConstantsWithLookupImplCreator c = new ConstantsWithLookupImplCreator( |
| logger, writer, targetClass, resourceList, context.getTypeOracle()); |
| c.emitClass(logger, generatedLocale); |
| } else if (constantsClass.isAssignableFrom(targetClass)) { |
| ConstantsImplCreator c = new ConstantsImplCreator(logger, writer, |
| targetClass, resourceList, context.getTypeOracle()); |
| c.emitClass(logger, generatedLocale); |
| } else { |
| MessagesImplCreator messages = new MessagesImplCreator(logger, writer, targetClass, |
| resourceList, context.getTypeOracle(), context.getResourcesOracle()); |
| messages.emitClass(logger, generatedLocale); |
| } |
| context.commit(logger, pw); |
| } |
| // Generate a translatable output file if requested. |
| Generate generate = getClassAnnotation(targetClass, Generate.class); |
| if (generate != null) { |
| String path = generate.fileName(); |
| if (Generate.DEFAULT.equals(path)) { |
| path = targetClass.getPackage().getName() + "." |
| + targetClass.getName().replace('.', '_'); |
| } else if (path.endsWith(File.pathSeparator)) { |
| path = path + targetClass.getName().replace('.', '_'); |
| } |
| String[] genLocales = generate.locales(); |
| boolean found = false; |
| if (genLocales.length != 0) { |
| // verify the current locale is in the list |
| for (String genLocale : genLocales) { |
| if (GwtLocale.DEFAULT_LOCALE.equals(genLocale)) { |
| // Locale "default" gets special handling because of property |
| // fallbacks; "default" might be mapped to any real locale. |
| try { |
| SelectionProperty localeProp = context.getPropertyOracle() |
| .getSelectionProperty(logger, "locale"); |
| String defaultLocale = localeProp.getFallbackValue(); |
| if (defaultLocale.length() > 0) { |
| genLocale = defaultLocale; |
| } |
| } catch (BadPropertyValueException e) { |
| throw error(logger, "Could not get 'locale' property"); |
| } |
| } |
| if (genLocale.equals(locale.toString())) { |
| found = true; |
| break; |
| } |
| } |
| } else { |
| // Since they want all locales, this is guaranteed to be one of them. |
| found = true; |
| } |
| if (found) { |
| for (String genClassName : generate.format()) { |
| MessageCatalogFormat msgWriter = null; |
| MessageCatalogFactory msgCatFactory = null; |
| try { |
| // TODO(jat): if GWT is ever modified to take a classpath for user |
| // code as an option, we would need to use the user classloader here |
| Class<?> clazz = Class.forName(genClassName, false, |
| MessageCatalogFormat.class.getClassLoader()); |
| if (MessageCatalogFormat.class.isAssignableFrom(clazz)) { |
| Class<? extends MessageCatalogFormat> msgFormatClass |
| = clazz.asSubclass(MessageCatalogFormat.class); |
| msgWriter = msgFormatClass.newInstance(); |
| } else if (MessageCatalogFactory.class.isAssignableFrom(clazz)) { |
| Class<? extends MessageCatalogFactory> msgFactoryClass |
| = clazz.asSubclass(MessageCatalogFactory.class); |
| msgCatFactory = msgFactoryClass.newInstance(); |
| } else { |
| logger.log(TreeLogger.ERROR, "Class specified in @Generate must " |
| + "either be a subtype of MessageCatalogFormat or " |
| + "MessageCatalogFactory"); |
| seenError = true; |
| continue; |
| } |
| } catch (InstantiationException e) { |
| logger.log(TreeLogger.ERROR, "Error instantiating @Generate class " |
| + genClassName, e); |
| seenError = true; |
| continue; |
| } catch (IllegalAccessException e) { |
| logger.log(TreeLogger.ERROR, "@Generate class " + genClassName |
| + " illegal access", e); |
| seenError = true; |
| continue; |
| } catch (ClassNotFoundException e) { |
| logger.log(TreeLogger.ERROR, "@Generate class " + genClassName |
| + " not found"); |
| seenError = true; |
| continue; |
| } |
| // Make generator-specific changes to a temporary copy of the path. |
| String genPath = path; |
| if (genLocales.length != 1) { |
| // If the user explicitly specified only one locale, do not add the |
| // locale. |
| genPath += '_' + locale.toString(); |
| } |
| if (msgCatFactory != null) { |
| seenError |= generateToMsgCatFactory(logger, context, locale, |
| targetClass, seenError, resourceList, msgCatFactory, genPath); |
| } else if (msgWriter != null) { |
| seenError |= generateToLegacyMsgCatFormat(logger, context, locale, |
| targetClass, seenError, resourceList, className, msgWriter, |
| genPath); |
| } |
| } |
| } |
| } |
| if (seenError) { |
| // If one of our generators had a fatal error, don't complete normally. |
| throw new UnableToCompleteException(); |
| } |
| return packageName + "." + className; |
| } |
| |
| /** |
| * Write translation source files to the old-style |
| * {@link MessageCatalogFormat}. |
| * |
| * @return true if an error occurred (already logged) |
| */ |
| private static boolean generateToLegacyMsgCatFormat(TreeLogger logger, |
| GeneratorContext context, GwtLocale locale, JClassType targetClass, |
| boolean seenError, ResourceList resourceList, String className, |
| MessageCatalogFormat msgWriter, String genPath) |
| throws UnableToCompleteException { |
| genPath += msgWriter.getExtension(); |
| OutputStream outStr = context.tryCreateResource(logger, genPath); |
| if (outStr != null) { |
| TreeLogger branch = logger.branch(TreeLogger.TRACE, "Generating " |
| + genPath + " from " + className + " for locale " + locale, |
| null); |
| PrintWriter out = null; |
| try { |
| out = new PrintWriter(new BufferedWriter( |
| new OutputStreamWriter(outStr, "UTF-8")), false); |
| } catch (UnsupportedEncodingException e) { |
| throw error(logger, "UTF-8 not supported", e); |
| } |
| try { |
| msgWriter.write(branch, locale.toString(), resourceList, out, |
| targetClass); |
| out.flush(); |
| context.commitResource(logger, outStr).setVisibility( |
| Visibility.Private); |
| } catch (UnableToCompleteException e) { |
| // msgWriter should have already logged an error message. |
| // Keep going for now so we can find other errors. |
| seenError = true; |
| } |
| } |
| return seenError; |
| } |
| |
| /** |
| * Write translation source files to a {@link MessageCatalogFactory}. |
| * |
| * @param logger |
| * @param context |
| * @param locale |
| * @param targetClass |
| * @param seenError |
| * @param resourceList |
| * @param msgCatFactory |
| * @param genPath |
| * @return true if an error occurred (already logged) |
| */ |
| private static boolean generateToMsgCatFactory(TreeLogger logger, |
| GeneratorContext context, GwtLocale locale, JClassType targetClass, boolean seenError, |
| ResourceList resourceList, MessageCatalogFactory msgCatFactory, |
| String genPath) { |
| // TODO(jat): maintain MessageCatalogWriter instances across |
| // generator runs so they can save state. One problem is knowing |
| // when the last generator has been run. |
| Writer catWriter = null; |
| try { |
| String catalogName = genPath + msgCatFactory.getExtension(); |
| Context ctx = new MessageCatalogContextImpl( |
| context, logger); |
| MessageInterface msgIntf = new TypeOracleMessageInterface( |
| LocaleUtils.getLocaleFactory(), targetClass, resourceList); |
| catWriter = msgCatFactory.getWriter(ctx, catalogName); |
| if (catWriter == null) { |
| if (logger.isLoggable(TreeLogger.TRACE)) { |
| logger.log(TreeLogger.TRACE, "Already generated " + catalogName); |
| } |
| return false; |
| } |
| msgIntf.accept(catWriter.visitClass()); |
| } catch (MessageProcessingException e) { |
| logger.log(TreeLogger.ERROR, e.getMessage(), e); |
| seenError = true; |
| } finally { |
| if (catWriter != null) { |
| try { |
| catWriter.close(); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, |
| "IO error closing catalog writer", e); |
| seenError = true; |
| } |
| } |
| } |
| return seenError; |
| } |
| |
| /** |
| * Generator to use to create keys for messages. |
| */ |
| private KeyGenerator keyGenerator; |
| |
| /** |
| * The Dictionary/value bindings used to determine message contents. |
| */ |
| private ResourceList resourceList; |
| |
| /** |
| * True if the class being generated uses Constants-style annotations/quoting. |
| */ |
| private boolean isConstants; |
| |
| /** |
| * Constructor for <code>AbstractLocalizableImplCreator</code>. |
| * |
| * @param writer writer |
| * @param targetClass current target |
| * @param resourceList backing resource |
| */ |
| public AbstractLocalizableImplCreator(TreeLogger logger, SourceWriter writer, |
| JClassType targetClass, ResourceList resourceList, boolean isConstants) { |
| super(writer, targetClass); |
| this.resourceList = resourceList; |
| this.isConstants = isConstants; |
| try { |
| keyGenerator = AnnotationsResource.getKeyGenerator(targetClass); |
| } catch (AnnotationsError e) { |
| logger.log(TreeLogger.WARN, "Error getting key generator for " |
| + targetClass.getQualifiedSourceName(), e); |
| } |
| } |
| |
| /** |
| * Gets the resource associated with this class. |
| * |
| * @return the resource |
| */ |
| public ResourceList getResourceBundle() { |
| return resourceList; |
| } |
| |
| @Override |
| protected String branchMessage() { |
| return "Processing " + this.getTarget(); |
| } |
| |
| /** |
| * Find the creator associated with the given method, and delegate the |
| * creation of the method body to it. |
| * |
| * @param logger TreeLogger instance for logging |
| * @param method method to be generated |
| * @param locale locale to generate |
| * @throws UnableToCompleteException |
| */ |
| protected void delegateToCreator(TreeLogger logger, JMethod method, |
| GwtLocale locale) throws UnableToCompleteException { |
| AbstractMethodCreator methodCreator = getMethodCreator(logger, method); |
| String key = getKey(logger, method); |
| if (key == null) { |
| logger.log(TreeLogger.ERROR, "Unable to get or compute key for method " |
| + method.getName(), null); |
| throw new UnableToCompleteException(); |
| } |
| methodCreator.createMethodFor(logger, method, key, resourceList, locale); |
| } |
| |
| /** |
| * Returns a resource key given a method name. |
| * |
| * @param logger TreeLogger instance for logging |
| * @param method method to get key for |
| * @return the key to use for resource lookups or null if unable to get or |
| * compute the key |
| */ |
| protected String getKey(TreeLogger logger, JMethod method) { |
| Key key = method.getAnnotation(Key.class); |
| if (key != null) { |
| return key.value(); |
| } |
| return AnnotationsResource.getKey(logger, keyGenerator, method, isConstants); |
| } |
| } |