blob: 8d1d141faf8cd0292c14f663d2414b9cabc4f1ae [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 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;
}
}