blob: 367a6416179ec00b546bf5c7edc62d33a3dada27 [file] [log] [blame]
/**
* HTML via Java(tm) Language Bindings
* Copyright (C) 2013 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. apidesign.org
* designates this particular file as subject to the
* "Classpath" exception as provided by apidesign.org
* in the License file that accompanied this code.
*
* You should have received a copy of the GNU General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://wiki.apidesign.org/wiki/GPLwithClassPathException
*/
package org.apidesign.html.boot.impl;
import java.io.Closeable;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Callable;
import org.apidesign.html.boot.spi.Fn;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
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 org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;
import org.objectweb.asm.signature.SignatureWriter;
/**
*
* @author Jaroslav Tulach <jtulach@netbeans.org>
*/
public final class FnUtils implements Fn.Presenter {
private FnUtils() {
}
public static boolean isJavaScriptCapable(ClassLoader l) {
if (l instanceof JsClassLoader) {
return true;
}
Class<?> clazz;
try (Closeable c = Fn.activate(new FnUtils())) {
clazz = Class.forName(Test.class.getName(), true, l);
final Object is = ((Callable<?>)clazz.newInstance()).call();
return Boolean.TRUE.equals(is);
} catch (Exception ex) {
return false;
}
}
public static boolean isValid(Fn fn) {
return fn != null && fn.isValid();
}
public static ClassLoader newLoader(final FindResources f, final Fn.Presenter d, ClassLoader parent) {
return new JsClassLoader(parent) {
@Override
protected URL findResource(String name) {
List<URL> l = res(name, true);
return l.isEmpty() ? null : l.get(0);
}
@Override
protected Enumeration<URL> findResources(String name) {
return Collections.enumeration(res(name, false));
}
private List<URL> res(String name, boolean oneIsEnough) {
List<URL> l = new ArrayList<URL>();
f.findResources(name, l, oneIsEnough);
return l;
}
@Override
protected Fn defineFn(String code, String... names) {
return d.defineFn(code, names);
}
@Override
protected void loadScript(Reader code) throws Exception {
d.loadScript(code);
}
};
}
static String callback(final String body) {
return new JsCallback() {
@Override
protected CharSequence callMethod(
String ident, String fqn, String method, String params
) {
StringBuilder sb = new StringBuilder();
sb.append("vm.").append(mangle(fqn, method, params));
sb.append("(");
if (ident != null) {
sb.append(ident);
}
return sb;
}
}.parse(body);
}
static void loadScript(ClassLoader jcl, String resource) {
final InputStream script = jcl.getResourceAsStream(resource);
if (script == null) {
throw new NullPointerException("Can't find " + resource);
}
try {
Reader isr = null;
try {
isr = new InputStreamReader(script, "UTF-8");
FnContext.currentPresenter().loadScript(isr);
} finally {
if (isr != null) {
isr.close();
}
}
} catch (Exception ex) {
throw new IllegalStateException("Can't execute " + resource, ex);
}
}
@Override
public Fn defineFn(String code, String... names) {
return new TrueFn();
}
@Override
public void displayPage(URL page, Runnable onPageLoad) {
}
@Override
public void loadScript(Reader code) throws Exception {
}
private static final class FindInClass extends ClassVisitor {
private String name;
private int found;
private ClassLoader loader;
private String resource;
public FindInClass(ClassLoader l, ClassVisitor cv) {
super(Opcodes.ASM4, cv);
this.loader = l;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.name = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if ("Lnet/java/html/js/JavaScriptResource;".equals(desc)) {
return new LoadResource();
}
return super.visitAnnotation(desc, visible);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new FindInMethod(access, name, desc,
super.visitMethod(access & (~Opcodes.ACC_NATIVE), name, desc, signature, exceptions)
);
}
private final class FindInMethod extends MethodVisitor {
private final String name;
private final String desc;
private final int access;
private List<String> args;
private String body;
private boolean bodyGenerated;
public FindInMethod(int access, String name, String desc, MethodVisitor mv) {
super(Opcodes.ASM4, mv);
this.access = access;
this.name = name;
this.desc = desc;
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if ("Lnet/java/html/js/JavaScriptBody;".equals(desc) // NOI18N
|| "Lorg/apidesign/bck2brwsr/core/JavaScriptBody;".equals(desc) // NOI18N
) {
found++;
return new FindInAnno();
}
return super.visitAnnotation(desc, visible);
}
private void generateJSBody(List<String> args, String body) {
this.args = args;
this.body = body;
}
@Override
public void visitCode() {
if (body == null) {
return;
}
generateBody();
}
private boolean generateBody() {
if (bodyGenerated) {
return false;
}
bodyGenerated = true;
super.visitFieldInsn(
Opcodes.GETSTATIC, FindInClass.this.name,
"$$fn$$" + name + "_" + found,
"Lorg/apidesign/html/boot/spi/Fn;"
);
super.visitInsn(Opcodes.DUP);
super.visitMethodInsn(
Opcodes.INVOKESTATIC,
"org/apidesign/html/boot/spi/Fn", "isValid",
"(Lorg/apidesign/html/boot/spi/Fn;)Z"
);
Label ifNotNull = new Label();
super.visitJumpInsn(Opcodes.IFNE, ifNotNull);
// init Fn
super.visitInsn(Opcodes.POP);
super.visitLdcInsn(Type.getObjectType(FindInClass.this.name));
super.visitLdcInsn(body);
super.visitIntInsn(Opcodes.SIPUSH, args.size());
super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String");
boolean needsVM = false;
for (int i = 0; i < args.size(); i++) {
assert !needsVM;
String argName = args.get(i);
needsVM = "vm".equals(argName);
super.visitInsn(Opcodes.DUP);
super.visitIntInsn(Opcodes.BIPUSH, i);
super.visitLdcInsn(argName);
super.visitInsn(Opcodes.AASTORE);
}
super.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/apidesign/html/boot/spi/Fn", "define",
"(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/String;)Lorg/apidesign/html/boot/spi/Fn;"
);
if (resource != null) {
super.visitLdcInsn(Type.getObjectType(FindInClass.this.name));
super.visitLdcInsn(resource);
super.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/apidesign/html/boot/spi/Fn", "preload",
"(Lorg/apidesign/html/boot/spi/Fn;Ljava/lang/Class;Ljava/lang/String;)Lorg/apidesign/html/boot/spi/Fn;"
);
}
super.visitInsn(Opcodes.DUP);
super.visitFieldInsn(
Opcodes.PUTSTATIC, FindInClass.this.name,
"$$fn$$" + name + "_" + found,
"Lorg/apidesign/html/boot/spi/Fn;"
);
// end of Fn init
super.visitLabel(ifNotNull);
final int offset;
if ((access & Opcodes.ACC_STATIC) == 0) {
offset = 1;
super.visitIntInsn(Opcodes.ALOAD, 0);
} else {
offset = 0;
super.visitInsn(Opcodes.ACONST_NULL);
}
super.visitIntInsn(Opcodes.SIPUSH, args.size());
super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
class SV extends SignatureVisitor {
private boolean nowReturn;
private Type returnType;
private int index;
private int loadIndex = offset;
public SV() {
super(Opcodes.ASM4);
}
@Override
public void visitBaseType(char descriptor) {
final Type t = Type.getType("" + descriptor);
if (nowReturn) {
returnType = t;
return;
}
FindInMethod.super.visitInsn(Opcodes.DUP);
FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
FindInMethod.super.visitVarInsn(t.getOpcode(Opcodes.ILOAD), loadIndex++);
String factory;
switch (descriptor) {
case 'I':
factory = "java/lang/Integer";
break;
case 'J':
factory = "java/lang/Long";
loadIndex++;
break;
case 'S':
factory = "java/lang/Short";
break;
case 'F':
factory = "java/lang/Float";
break;
case 'D':
factory = "java/lang/Double";
loadIndex++;
break;
case 'Z':
factory = "java/lang/Boolean";
break;
case 'C':
factory = "java/lang/Character";
break;
case 'B':
factory = "java/lang/Byte";
break;
default:
throw new IllegalStateException(t.toString());
}
FindInMethod.super.visitMethodInsn(Opcodes.INVOKESTATIC,
factory, "valueOf", "(" + descriptor + ")L" + factory + ";"
);
FindInMethod.super.visitInsn(Opcodes.AASTORE);
}
@Override
public SignatureVisitor visitArrayType() {
if (nowReturn) {
throw new IllegalStateException("Not supported yet");
}
loadObject();
return new SignatureWriter();
}
@Override
public void visitClassType(String name) {
if (nowReturn) {
returnType = Type.getObjectType(name);
return;
}
loadObject();
}
@Override
public SignatureVisitor visitReturnType() {
nowReturn = true;
return this;
}
private void loadObject() {
FindInMethod.super.visitInsn(Opcodes.DUP);
FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
FindInMethod.super.visitVarInsn(Opcodes.ALOAD, loadIndex++);
FindInMethod.super.visitInsn(Opcodes.AASTORE);
}
}
SV sv = new SV();
SignatureReader sr = new SignatureReader(desc);
sr.accept(sv);
if (needsVM) {
FindInMethod.super.visitInsn(Opcodes.DUP);
FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, sv.index);
int lastSlash = FindInClass.this.name.lastIndexOf('/');
String jsCallbacks = FindInClass.this.name.substring(0, lastSlash + 1) + "$JsCallbacks$";
FindInMethod.super.visitFieldInsn(Opcodes.GETSTATIC, jsCallbacks, "VM", "L" + jsCallbacks + ";");
FindInMethod.super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, jsCallbacks, "current", "()L" + jsCallbacks + ";");
FindInMethod.super.visitInsn(Opcodes.AASTORE);
}
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/apidesign/html/boot/spi/Fn", "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
);
switch (sv.returnType.getSort()) {
case Type.VOID:
super.visitInsn(Opcodes.RETURN);
break;
case Type.ARRAY:
case Type.OBJECT:
super.visitTypeInsn(Opcodes.CHECKCAST, sv.returnType.getInternalName());
super.visitInsn(Opcodes.ARETURN);
break;
case Type.BOOLEAN:
super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Boolean");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Boolean", "booleanValue", "()Z"
);
super.visitInsn(Opcodes.IRETURN);
break;
default:
super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Number");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Number", sv.returnType.getClassName() + "Value", "()" + sv.returnType.getDescriptor()
);
super.visitInsn(sv.returnType.getOpcode(Opcodes.IRETURN));
}
return true;
}
@Override
public void visitEnd() {
super.visitEnd();
if (body != null) {
if (generateBody()) {
// native method
super.visitMaxs(1, 0);
}
FindInClass.this.visitField(
Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
"$$fn$$" + name + "_" + found,
"Lorg/apidesign/html/boot/spi/Fn;",
null, null
);
}
}
private final class FindInAnno extends AnnotationVisitor {
private List<String> args = new ArrayList<String>();
private String body;
private boolean javacall = false;
public FindInAnno() {
super(Opcodes.ASM4);
}
@Override
public void visit(String name, Object value) {
if (name == null) {
args.add((String) value);
return;
}
if (name.equals("javacall")) { // NOI18N
javacall = (Boolean) value;
return;
}
assert name.equals("body");
body = (String) value;
}
@Override
public AnnotationVisitor visitArray(String name) {
return this;
}
@Override
public void visitEnd() {
if (body != null) {
if (javacall) {
body = callback(body);
args.add("vm");
}
generateJSBody(args, body);
}
}
}
}
private final class LoadResource extends AnnotationVisitor {
public LoadResource() {
super(Opcodes.ASM4);
}
@Override
public void visit(String attrName, Object value) {
String relPath = (String) value;
if (relPath.startsWith("/")) {
resource = relPath;
} else {
int last = name.lastIndexOf('/');
String fullPath = name.substring(0, last + 1) + relPath;
resource = fullPath;
}
}
}
}
private static class ClassWriterEx extends ClassWriter {
private ClassLoader loader;
public ClassWriterEx(ClassLoader l, ClassReader classReader, int flags) {
super(classReader, flags);
this.loader = l;
}
@Override
protected String getCommonSuperClass(final String type1, final String type2) {
Class<?> c, d;
try {
c = Class.forName(type1.replace('/', '.'), false, loader);
d = Class.forName(type2.replace('/', '.'), false, loader);
} catch (Exception e) {
throw new RuntimeException(e.toString());
}
if (c.isAssignableFrom(d)) {
return type1;
}
if (d.isAssignableFrom(c)) {
return type2;
}
if (c.isInterface() || d.isInterface()) {
return "java/lang/Object";
} else {
do {
c = c.getSuperclass();
} while (!c.isAssignableFrom(d));
return c.getName().replace('.', '/');
}
}
}
static byte[] transform(ClassLoader loader, byte[] arr) {
ClassReader cr = new ClassReader(arr) {
// to allow us to compile with -profile compact1 on
// JDK8 while processing the class as JDK7, the highest
// class format asm 4.1 understands to
@Override
public short readShort(int index) {
short s = super.readShort(index);
if (index == 6 && s > Opcodes.V1_7) {
return Opcodes.V1_7;
}
return s;
}
};
FindInClass tst = new FindInClass(loader, null);
cr.accept(tst, 0);
if (tst.found > 0) {
ClassWriter w = new ClassWriterEx(loader, cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
FindInClass fic = new FindInClass(loader, w);
cr.accept(fic, 0);
arr = w.toByteArray();
}
return arr;
}
private static final class TrueFn extends Fn {
@Override
public Object invoke(Object thiz, Object... args) throws Exception {
return Boolean.TRUE;
}
} // end of TrueFn
}