package net.jhorstmann.i18n.tools;
import antlr.RecognitionException;
import antlr.TokenStreamException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
import net.jhorstmann.i18n.tools.expr.Expression;
import org.fedorahosted.tennera.jgettext.Message;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.util.CheckClassAdapter;
public class ResourceBundleCompiler {
private static final String CONTEXT_GLUE = "\u0004";
private static final String DEFAULT_PARENT_CLASS = ResourceBundle.class.getName();
static class MyClassLoader extends ClassLoader {
MyClassLoader(ClassLoader parent) {
super(parent);
}
final Class<?> defineClass(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length);
}
}
public static byte[] compile(MessageBundle bundle, String className) {
return compile(bundle, DEFAULT_PARENT_CLASS, className);
}
public static void compileFile(MessageBundle bundle, String className, File dir) throws IOException {
compileFile(bundle, DEFAULT_PARENT_CLASS, className, dir);
}
public static void compileFile(MessageBundle bundle, String parentClassName, String className, File dir) throws IOException {
File file = new File(dir, className.replace('.', '/') + ".class");
File parent = file.getParentFile();
if (!parent.exists() && !parent.mkdirs()) {
throw new IOException("Could not create directory " + parent);
}
byte[] bytes = compile(bundle, parentClassName, className);
FileOutputStream fos = new FileOutputStream(file);
try {
fos.write(bytes);
} finally {
fos.close();
}
}
static Class<ResourceBundle> compileAndLoad(MessageBundle bundle, String className) throws InstantiationException, IllegalAccessException {
return compileAndLoad(bundle, DEFAULT_PARENT_CLASS, className, null);
}
static Class<ResourceBundle> compileAndLoad(MessageBundle bundle, String parentClassName, String className, ClassLoader parent) throws InstantiationException, IllegalAccessException {
byte[] bytes = compile(bundle, parentClassName, className);
return (Class<ResourceBundle>) new MyClassLoader(parent).defineClass(className, bytes);
}
public static byte[] compile(MessageBundle bundle, String parentClassName, String className) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
CheckClassAdapter ca = new CheckClassAdapter(cw, true);
compile(ca, bundle, parentClassName, className);
return cw.toByteArray();
}
static void compile(ClassVisitor cv, MessageBundle bundle, String parentClassName, String className) {
String parent = parentClassName.replace('.', '/');
String owner = className.replace('.', '/');
cv.visit(V1_2, ACC_PUBLIC, owner, null, parent, null);
cv.visitField(ACC_PRIVATE | ACC_STATIC, "messages", "Ljava/util/HashMap;", null, null);
compileStaticInit(cv, owner, bundle);
compileConstructor(cv, parent);
compileGetParent(cv, parent);
compileLookup(cv, owner);
compileGetKeys(cv, owner);
compileHandleGetObject(cv, owner);
String pluralForms = bundle.getPluralForms();
if (pluralForms != null) {
compilePluralEval(cv, pluralForms);
compilePluralIndex(cv, owner);
} else {
compilePluralIndexDummy(cv, owner);
}
cv.visitEnd();
}
private static void compileHandleGetObject(ClassVisitor cv, String owner) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "handleGetObject", "(Ljava/lang/String;)Ljava/lang/Object;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, owner, "lookup", "(Ljava/lang/String;)Ljava/lang/Object;");
mv.visitInsn(ARETURN);
mv.visitMaxs(2, 2);
/*
Label isString = new Label();
mv.visitVarInsn(ASTORE, 2);
mv.visitVarInsn(ALOAD, 2);
mv.visitTypeInsn(INSTANCEOF, "[Ljava/lang/String;");
mv.visitJumpInsn(IFEQ, isString);
mv.visitVarInsn(ALOAD, 2);
mv.visitTypeInsn(CHECKCAST, "[Ljava/lang/String;");
mv.visitInsn(ICONST_0);
mv.visitInsn(AALOAD);
mv.visitInsn(ARETURN);
mv.visitLabel(isString);
mv.visitVarInsn(ALOAD, 2);
mv.visitInsn(ARETURN);
mv.visitMaxs(2, 3);
*/
mv.visitEnd();
}
private static void compileGetKeys(ClassVisitor cv, String owner) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getKeys", "()Ljava/util/Enumeration;", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "messages", "Ljava/util/HashMap;");
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "keySet", "()Ljava/util/Set;");
mv.visitMethodInsn(INVOKESTATIC, "java/util/Collections", "enumeration", "(Ljava/util/Collection;)Ljava/util/Enumeration;");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
private static void compileLookup(ClassVisitor cv, String owner) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "lookup", "(Ljava/lang/String;)Ljava/lang/Object;", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "messages", "Ljava/util/HashMap;");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
mv.visitInsn(ARETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
private static void compileGetParent(ClassVisitor cv, String parent) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getParent", "()Ljava/util/ResourceBundle;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, parent, "parent", "Ljava/util/ResourceBundle;");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
private static void compileConstructor(ClassVisitor cv, String parent) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, parent, "<init>", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
private static void compileStaticInit(ClassVisitor cv, String owner, MessageBundle bundle) {
Label localMapBegin = new Label();
Label localMapEnd = new Label();
Label localArrayBegin = new Label();
Label localArrayEnd = new Label();
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitLabel(localMapBegin);
mv.visitTypeInsn(NEW, "java/util/HashMap");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "<init>", "()V");
mv.visitVarInsn(ASTORE, 0);
mv.visitLabel(localArrayBegin);
// TODO: Depending on the amount of messages this might get too big for one method and should be split up
for (Message message : bundle) {
String msgctx = message.getMsgctxt();
String msgid = (msgctx != null ? msgctx + CONTEXT_GLUE : "") + message.getMsgid();
mv.visitVarInsn(ALOAD, 0);
if (message.isPlural()) {
List<String> plurals = bundle.isTemplate() ? Arrays.asList(msgid, message.getMsgidPlural()) : message.getMsgstrPlural();
if (plurals.isEmpty()) {
mv.visitInsn(ACONST_NULL);
mv.visitLdcInsn(msgid);
} else {
mv.visitLdcInsn(Integer.valueOf(plurals.size()));
mv.visitTypeInsn(ANEWARRAY, "java/lang/String");
mv.visitVarInsn(ASTORE, 1);
int i = 0;
for (String plural : plurals) {
mv.visitVarInsn(ALOAD, 1);
mv.visitLdcInsn(Integer.valueOf(i));
mv.visitLdcInsn(plural);
mv.visitInsn(AASTORE);
i++;
}
mv.visitLdcInsn(msgid);
mv.visitVarInsn(ALOAD, 1);
}
} else {
String msgstr = bundle.isTemplate() ? message.getMsgid() : message.getMsgstr();
if (msgstr == null || msgstr.length() == 0) {
mv.visitLdcInsn(msgid);
mv.visitInsn(ACONST_NULL);
} else {
mv.visitLdcInsn(msgid);
mv.visitLdcInsn(msgstr);
}
}
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
mv.visitInsn(POP);
}
mv.visitLabel(localArrayEnd);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(PUTSTATIC, owner, "messages", "Ljava/util/HashMap;");
mv.visitLabel(localMapEnd);
mv.visitInsn(RETURN);
mv.visitLocalVariable("array", "[Ljava/lang/Object;", null, localArrayBegin, localArrayEnd, 1);
mv.visitLocalVariable("map", "Ljava/util/Map;", null, localMapBegin, localMapEnd, 0);
mv.visitMaxs(4, 2);
mv.visitEnd();
}
private static void compilePluralEval(ClassVisitor cv, String pluralForms) {
try {
PluralForms pf = PluralsParser.parsePluralForms(pluralForms);
Expression expr = pf.getExpression();
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "pluralEval", "(J)J", null, null);
//mv.visitCode();
//GeneratorAdapter ga = new GeneratorAdapter(ACC_PUBLIC | ACC_STATIC, Method.getMethod("long pluralEval(long)"), null, null, cv);
GeneratorAdapter ga = new GeneratorAdapter(mv, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "pluralEval", "(J)J");
ga.visitCode();
expr.compile(ga, 0);
ga.returnValue();
int stack = expr.computeStackSize();
ga.visitMaxs(stack, 2);
ga.visitEnd();
} catch (RecognitionException ex) {
throw new IllegalStateException(ex);
} catch (TokenStreamException ex) {
throw new IllegalStateException(ex);
}
}
private static void compilePluralIndex(ClassVisitor cv, String owner) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "pluralIndex", "(J)I", null, null);
mv.visitCode();
mv.visitVarInsn(LLOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, owner, "pluralEval", "(J)J");
mv.visitInsn(L2I);
mv.visitInsn(IRETURN);
mv.visitMaxs(3, 3);
mv.visitEnd();
}
private static void compilePluralIndexDummy(ClassVisitor cv, String owner) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "pluralIndex", "(J)I", null, null);
mv.visitCode();
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 3);
mv.visitEnd();
}
}