| /* |
| * 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.dev.util.Util; |
| import com.google.gwt.dev.util.log.PrintWriterTreeLogger; |
| import com.google.gwt.i18n.client.Localizable; |
| import com.google.gwt.user.rebind.ClassSourceFileComposerFactory; |
| import com.google.gwt.user.rebind.SourceWriter; |
| |
| import org.apache.tapestry.util.text.LocalizedProperties; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Abstract base functionality for <code>MessagesInterfaceCreator</code> and |
| * <code>ConstantsInterfaceCreator</code>. |
| */ |
| public abstract class AbstractLocalizableInterfaceCreator { |
| private static class RenameDuplicates extends ResourceKeyFormatter { |
| private Set<String> methodNames = new HashSet<String>(); |
| |
| @Override |
| public String format(String key) { |
| while (methodNames.contains(key)) { |
| key += "_dup"; |
| } |
| methodNames.add(key); |
| return key; |
| } |
| } |
| |
| private static class ReplaceBadChars extends ResourceKeyFormatter { |
| @Override |
| public String format(String key) { |
| StringBuilder buf = new StringBuilder(); |
| int keyLen = key == null ? 0 : key.length(); |
| for (int i = 0; i < keyLen; i = key.offsetByCodePoints(i, 1)) { |
| int codePoint = key.codePointAt(i); |
| if (i == 0 ? Character.isJavaIdentifierStart(codePoint) |
| : Character.isJavaIdentifierPart(codePoint)) { |
| buf.appendCodePoint(codePoint); |
| } else { |
| buf.append('_'); |
| } |
| } |
| if (buf.length() == 0) { |
| buf.append('_'); |
| } |
| return buf.toString(); |
| } |
| } |
| |
| private abstract static class ResourceKeyFormatter { |
| public abstract String format(String key); |
| } |
| |
| /** |
| * Index into this array using a nibble, 4 bits, to get the corresponding |
| * hexadecimal character representation. |
| */ |
| private static final char NIBBLE_TO_HEX_CHAR[] = { |
| '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', |
| 'E', 'F'}; |
| |
| private static boolean needsUnicodeEscape(char ch) { |
| if (ch == ' ') { |
| return false; |
| } |
| switch (Character.getType(ch)) { |
| case Character.COMBINING_SPACING_MARK: |
| case Character.ENCLOSING_MARK: |
| case Character.NON_SPACING_MARK: |
| case Character.UNASSIGNED: |
| case Character.PRIVATE_USE: |
| case Character.SPACE_SEPARATOR: |
| case Character.CONTROL: |
| case Character.LINE_SEPARATOR: |
| case Character.FORMAT: |
| case Character.PARAGRAPH_SEPARATOR: |
| case Character.SURROGATE: |
| return true; |
| |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| private static void unicodeEscape(char ch, StringBuilder buf) { |
| buf.append('\\'); |
| buf.append('u'); |
| buf.append(NIBBLE_TO_HEX_CHAR[(ch >> 12) & 0x0F]); |
| buf.append(NIBBLE_TO_HEX_CHAR[(ch >> 8) & 0x0F]); |
| buf.append(NIBBLE_TO_HEX_CHAR[(ch >> 4) & 0x0F]); |
| buf.append(NIBBLE_TO_HEX_CHAR[ch & 0x0F]); |
| } |
| |
| /** |
| * Composer for the current Constant. |
| */ |
| protected SourceWriter composer; |
| |
| private List<ResourceKeyFormatter> formatters = new ArrayList<ResourceKeyFormatter>(); |
| |
| private File resourceFile; |
| |
| private File sourceFile; |
| |
| private PrintWriter writer; |
| |
| /** |
| * Creates a new constants creator. |
| * |
| * @param className constant class to create |
| * @param packageName package to create it in |
| * @param resourceBundle resource bundle with value |
| * @param targetLocation |
| * @throws IOException |
| */ |
| public AbstractLocalizableInterfaceCreator(String className, |
| String packageName, File resourceBundle, File targetLocation, |
| Class<? extends Localizable> interfaceClass) throws IOException { |
| setup(packageName, className, resourceBundle, targetLocation, |
| interfaceClass); |
| } |
| |
| /** |
| * Generate class. |
| * |
| * @throws FileNotFoundException |
| * @throws IOException |
| */ |
| public void generate() throws FileNotFoundException, IOException { |
| try { |
| try { |
| // right now, only property files are legal |
| generateFromPropertiesFile(); |
| } finally { |
| writer.close(); |
| } |
| } catch (IOException e) { |
| sourceFile.delete(); |
| throw e; |
| } catch (RuntimeException e) { |
| sourceFile.delete(); |
| throw e; |
| } |
| } |
| |
| /** |
| * Create a String method declaration from a Dictionary/value pair. |
| * |
| * @param key Dictionary |
| * @param defaultValue default value |
| */ |
| public void genSimpleMethodDecl(String key, String defaultValue) { |
| genMethodDecl("String", defaultValue, key); |
| } |
| |
| /** |
| * Create method args based upon the default value. |
| * |
| * @param defaultValue |
| */ |
| protected abstract void genMethodArgs(String defaultValue); |
| |
| /** |
| * Create an annotation to hold the default value. |
| */ |
| protected abstract void genValueAnnotation(String defaultValue); |
| |
| /** |
| * Returns the javaDocComment for the class. |
| * |
| * @param path path of class |
| * @return java doc comment |
| */ |
| protected abstract String javaDocComment(String path); |
| |
| protected String makeJavaString(String value) { |
| StringBuilder buf = new StringBuilder(); |
| buf.append('\"'); |
| for (int i = 0; i < value.length(); ++i) { |
| char c = value.charAt(i); |
| switch (c) { |
| case '\r': |
| buf.append("\\r"); |
| break; |
| case '\n': |
| buf.append("\\n"); |
| break; |
| case '\"': |
| buf.append("\\\""); |
| break; |
| default: |
| if (needsUnicodeEscape(c)) { |
| unicodeEscape(c, buf); |
| } else { |
| buf.append(c); |
| } |
| break; |
| } |
| } |
| buf.append('\"'); |
| return buf.toString(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| // use of raw type from LocalizedProperties |
| void generateFromPropertiesFile() throws IOException { |
| InputStream propStream = new FileInputStream(resourceFile); |
| LocalizedProperties p = new LocalizedProperties(); |
| p.load(propStream, Util.DEFAULT_ENCODING); |
| addFormatters(); |
| // TODO: Look for a generic version of Tapestry's LocalizedProperties class |
| Set<String> keySet = p.getPropertyMap().keySet(); |
| // sort keys for deterministic results |
| String[] keys = keySet.toArray(new String[keySet.size()]); |
| Arrays.sort(keys); |
| if (keys.length == 0) { |
| throw new IllegalStateException( |
| "File '" |
| + resourceFile |
| + "' cannot be used to generate message classes, as it has no key/value pairs defined."); |
| } |
| for (String key : keys) { |
| String value = p.getProperty(key); |
| genSimpleMethodDecl(key, value); |
| } |
| composer.commit(new PrintWriterTreeLogger()); |
| } |
| |
| private void addFormatters() { |
| // For now, we completely control property key formatters. |
| formatters.add(new ReplaceBadChars()); |
| |
| // Rename Duplicates must always come last. |
| formatters.add(new RenameDuplicates()); |
| } |
| |
| private String formatKey(String key) { |
| for (ResourceKeyFormatter formatter : formatters) { |
| key = formatter.format(key); |
| } |
| return key; |
| } |
| |
| private void genMethodDecl(String type, String defaultValue, String key) { |
| composer.beginJavaDocComment(); |
| String escaped = makeJavaString(defaultValue); |
| composer.println("Translated " + escaped + ".\n"); |
| composer.print("@return translated " + escaped); |
| composer.endJavaDocComment(); |
| genValueAnnotation(defaultValue); |
| composer.println("@Key(" + makeJavaString(key) + ")"); |
| String methodName = formatKey(key); |
| composer.print(type + " " + methodName); |
| composer.print("("); |
| genMethodArgs(defaultValue); |
| composer.print(");\n"); |
| } |
| |
| private void setup(String packageName, String className, File resourceBundle, |
| File targetLocation, Class<? extends Localizable> interfaceClass) |
| throws IOException { |
| ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory( |
| packageName, className); |
| factory.makeInterface(); |
| // TODO(jat): need a way to add an @GeneratedFrom annotation. |
| factory.setJavaDocCommentForClass(javaDocComment(resourceBundle.getCanonicalPath().replace( |
| File.separatorChar, '/'))); |
| factory.addImplementedInterface(interfaceClass.getName()); |
| FileOutputStream file = new FileOutputStream(targetLocation); |
| Writer underlying = new OutputStreamWriter(file, Util.DEFAULT_ENCODING); |
| writer = new PrintWriter(underlying); |
| composer = factory.createSourceWriter(writer); |
| resourceFile = resourceBundle; |
| sourceFile = targetLocation; |
| } |
| } |