boot/src/main/java/org/apidesign/html/boot/impl/FnUtils.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Wed, 06 Nov 2013 15:15:54 +0100
changeset 323 86aabecda7a3
parent 309 7025177bd67e
child 331 72dda6af599b
child 349 53634fd10e30
permissions -rw-r--r--
Introducing Agent-Class to allow java.lang.instrument-like transforms of the @JavaScriptBody annotation
     1 /**
     2  * HTML via Java(tm) Language Bindings
     3  * Copyright (C) 2013 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 2 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  * GNU General Public License for more details. apidesign.org
    13  * designates this particular file as subject to the
    14  * "Classpath" exception as provided by apidesign.org
    15  * in the License file that accompanied this code.
    16  *
    17  * You should have received a copy of the GNU General Public License
    18  * along with this program. Look for COPYING file in the top folder.
    19  * If not, see http://wiki.apidesign.org/wiki/GPLwithClassPathException
    20  */
    21 package org.apidesign.html.boot.impl;
    22 
    23 import java.io.Closeable;
    24 import java.io.InputStream;
    25 import java.io.InputStreamReader;
    26 import java.io.Reader;
    27 import java.net.URL;
    28 import java.util.ArrayList;
    29 import java.util.Collections;
    30 import java.util.Enumeration;
    31 import java.util.List;
    32 import java.util.concurrent.Callable;
    33 import org.apidesign.html.boot.spi.Fn;
    34 import org.objectweb.asm.AnnotationVisitor;
    35 import org.objectweb.asm.ClassReader;
    36 import org.objectweb.asm.ClassVisitor;
    37 import org.objectweb.asm.ClassWriter;
    38 import org.objectweb.asm.Label;
    39 import org.objectweb.asm.MethodVisitor;
    40 import org.objectweb.asm.Opcodes;
    41 import org.objectweb.asm.Type;
    42 import org.objectweb.asm.signature.SignatureReader;
    43 import org.objectweb.asm.signature.SignatureVisitor;
    44 import org.objectweb.asm.signature.SignatureWriter;
    45 
    46 /**
    47  *
    48  * @author Jaroslav Tulach <jtulach@netbeans.org>
    49  */
    50 public final class FnUtils implements Fn.Presenter {
    51     
    52     private FnUtils() {
    53     }
    54     
    55     public static boolean isJavaScriptCapable(ClassLoader l) {
    56         if (l instanceof JsClassLoader) {
    57             return true;
    58         }
    59         Class<?> clazz;
    60         try (Closeable c = Fn.activate(new FnUtils())) {
    61             clazz = Class.forName(Test.class.getName(), true, l);
    62             final Object is = ((Callable<?>)clazz.newInstance()).call();
    63             return Boolean.TRUE.equals(is);
    64         } catch (Exception ex) {
    65             return false;
    66         }
    67     }
    68     
    69     public static boolean isValid(Fn fn) {
    70         return fn != null && fn.isValid();
    71     }
    72 
    73     public static ClassLoader newLoader(final FindResources f, final Fn.Presenter d, ClassLoader parent) {
    74         return new JsClassLoader(parent) {
    75             @Override
    76             protected URL findResource(String name) {
    77                 List<URL> l = res(name, true);
    78                 return l.isEmpty() ? null : l.get(0);
    79             }
    80             
    81             @Override
    82             protected Enumeration<URL> findResources(String name) {
    83                 return Collections.enumeration(res(name, false));
    84             }
    85             
    86             private List<URL> res(String name, boolean oneIsEnough) {
    87                 List<URL> l = new ArrayList<URL>();
    88                 f.findResources(name, l, oneIsEnough);
    89                 return l;
    90             }
    91             
    92             @Override
    93             protected Fn defineFn(String code, String... names) {
    94                 return d.defineFn(code, names);
    95             }
    96 
    97             @Override
    98             protected void loadScript(Reader code) throws Exception {
    99                 d.loadScript(code);
   100             }
   101         };
   102     }
   103 
   104     static String callback(final String body) {
   105         return new JsCallback() {
   106             @Override
   107             protected CharSequence callMethod(
   108                 String ident, String fqn, String method, String params
   109             ) {
   110                 StringBuilder sb = new StringBuilder();
   111                 sb.append("vm.").append(mangle(fqn, method, params));
   112                 sb.append("(");
   113                 if (ident != null) {
   114                     sb.append(ident);
   115                 }
   116                 return sb;
   117             }
   118 
   119         }.parse(body);
   120     }
   121 
   122     static void loadScript(ClassLoader jcl, String resource) {
   123         final InputStream script = jcl.getResourceAsStream(resource);
   124         if (script == null) {
   125             throw new NullPointerException("Can't find " + resource);
   126         }
   127         try {
   128             Reader isr = null;
   129             try {
   130                 isr = new InputStreamReader(script, "UTF-8");
   131                 FnContext.currentPresenter().loadScript(isr);
   132             } finally {
   133                 if (isr != null) {
   134                     isr.close();
   135                 }
   136             }
   137         } catch (Exception ex) {
   138             throw new IllegalStateException("Can't execute " + resource, ex);
   139         } 
   140     }
   141 
   142     @Override
   143     public Fn defineFn(String code, String... names) {
   144         return new TrueFn();
   145     }
   146 
   147     @Override
   148     public void displayPage(URL page, Runnable onPageLoad) {
   149     }
   150 
   151     @Override
   152     public void loadScript(Reader code) throws Exception {
   153     }
   154     
   155     private static final class FindInClass extends ClassVisitor {
   156         private String name;
   157         private int found;
   158         private ClassLoader loader;
   159 
   160         public FindInClass(ClassLoader l, ClassVisitor cv) {
   161             super(Opcodes.ASM4, cv);
   162             this.loader = l;
   163         }
   164 
   165         @Override
   166         public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
   167             this.name = name;
   168             super.visit(version, access, name, signature, superName, interfaces);
   169         }
   170 
   171         @Override
   172         public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
   173             if ("Lnet/java/html/js/JavaScriptResource;".equals(desc)) {
   174                 return new LoadResource();
   175             }
   176             return super.visitAnnotation(desc, visible);
   177         }
   178 
   179         @Override
   180         public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
   181             return new FindInMethod(access, name, desc,
   182                     super.visitMethod(access & (~Opcodes.ACC_NATIVE), name, desc, signature, exceptions)
   183             );
   184         }
   185 
   186         private final class FindInMethod extends MethodVisitor {
   187 
   188             private final String name;
   189             private final String desc;
   190             private final int access;
   191             private List<String> args;
   192             private String body;
   193             private boolean bodyGenerated;
   194 
   195             public FindInMethod(int access, String name, String desc, MethodVisitor mv) {
   196                 super(Opcodes.ASM4, mv);
   197                 this.access = access;
   198                 this.name = name;
   199                 this.desc = desc;
   200             }
   201 
   202             @Override
   203             public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
   204                 if ("Lnet/java/html/js/JavaScriptBody;".equals(desc) // NOI18N
   205                         || "Lorg/apidesign/bck2brwsr/core/JavaScriptBody;".equals(desc) // NOI18N
   206                         ) {
   207                     found++;
   208                     return new FindInAnno();
   209                 }
   210                 return super.visitAnnotation(desc, visible);
   211             }
   212 
   213             private void generateJSBody(List<String> args, String body) {
   214                 this.args = args;
   215                 this.body = body;
   216             }
   217 
   218             @Override
   219             public void visitCode() {
   220                 if (body == null) {
   221                     return;
   222                 }
   223                 generateBody();
   224             }
   225 
   226             private boolean generateBody() {
   227                 if (bodyGenerated) {
   228                     return false;
   229                 }
   230                 bodyGenerated = true;
   231 
   232                 super.visitFieldInsn(
   233                         Opcodes.GETSTATIC, FindInClass.this.name,
   234                         "$$fn$$" + name + "_" + found,
   235                         "Lorg/apidesign/html/boot/spi/Fn;"
   236                 );
   237                 super.visitInsn(Opcodes.DUP);
   238                 super.visitMethodInsn(
   239                         Opcodes.INVOKESTATIC,
   240                         "org/apidesign/html/boot/spi/Fn", "isValid",
   241                         "(Lorg/apidesign/html/boot/spi/Fn;)Z"
   242                 );
   243                 Label ifNotNull = new Label();
   244                 super.visitJumpInsn(Opcodes.IFNE, ifNotNull);
   245 
   246                 // init Fn
   247                 super.visitInsn(Opcodes.POP);
   248                 super.visitLdcInsn(Type.getObjectType(FindInClass.this.name));
   249                 super.visitLdcInsn(body);
   250                 super.visitIntInsn(Opcodes.SIPUSH, args.size());
   251                 super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String");
   252                 boolean needsVM = false;
   253                 for (int i = 0; i < args.size(); i++) {
   254                     assert !needsVM;
   255                     String argName = args.get(i);
   256                     needsVM = "vm".equals(argName);
   257                     super.visitInsn(Opcodes.DUP);
   258                     super.visitIntInsn(Opcodes.BIPUSH, i);
   259                     super.visitLdcInsn(argName);
   260                     super.visitInsn(Opcodes.AASTORE);
   261                 }
   262                 super.visitMethodInsn(Opcodes.INVOKESTATIC,
   263                         "org/apidesign/html/boot/spi/Fn", "define",
   264                         "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/String;)Lorg/apidesign/html/boot/spi/Fn;"
   265                 );
   266                 super.visitInsn(Opcodes.DUP);
   267                 super.visitFieldInsn(
   268                         Opcodes.PUTSTATIC, FindInClass.this.name,
   269                         "$$fn$$" + name + "_" + found,
   270                         "Lorg/apidesign/html/boot/spi/Fn;"
   271                 );
   272                 // end of Fn init
   273 
   274                 super.visitLabel(ifNotNull);
   275 
   276                 final int offset;
   277                 if ((access & Opcodes.ACC_STATIC) == 0) {
   278                     offset = 1;
   279                     super.visitIntInsn(Opcodes.ALOAD, 0);
   280                 } else {
   281                     offset = 0;
   282                     super.visitInsn(Opcodes.ACONST_NULL);
   283                 }
   284 
   285                 super.visitIntInsn(Opcodes.SIPUSH, args.size());
   286                 super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
   287 
   288                 class SV extends SignatureVisitor {
   289 
   290                     private boolean nowReturn;
   291                     private Type returnType;
   292                     private int index;
   293                     private int loadIndex = offset;
   294 
   295                     public SV() {
   296                         super(Opcodes.ASM4);
   297                     }
   298 
   299                     @Override
   300                     public void visitBaseType(char descriptor) {
   301                         final Type t = Type.getType("" + descriptor);
   302                         if (nowReturn) {
   303                             returnType = t;
   304                             return;
   305                         }
   306                         FindInMethod.super.visitInsn(Opcodes.DUP);
   307                         FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
   308                         FindInMethod.super.visitVarInsn(t.getOpcode(Opcodes.ILOAD), loadIndex++);
   309                         String factory;
   310                         switch (descriptor) {
   311                             case 'I':
   312                                 factory = "java/lang/Integer";
   313                                 break;
   314                             case 'J':
   315                                 factory = "java/lang/Long";
   316                                 loadIndex++;
   317                                 break;
   318                             case 'S':
   319                                 factory = "java/lang/Short";
   320                                 break;
   321                             case 'F':
   322                                 factory = "java/lang/Float";
   323                                 break;
   324                             case 'D':
   325                                 factory = "java/lang/Double";
   326                                 loadIndex++;
   327                                 break;
   328                             case 'Z':
   329                                 factory = "java/lang/Boolean";
   330                                 break;
   331                             case 'C':
   332                                 factory = "java/lang/Character";
   333                                 break;
   334                             case 'B':
   335                                 factory = "java/lang/Byte";
   336                                 break;
   337                             default:
   338                                 throw new IllegalStateException(t.toString());
   339                         }
   340                         FindInMethod.super.visitMethodInsn(Opcodes.INVOKESTATIC,
   341                                 factory, "valueOf", "(" + descriptor + ")L" + factory + ";"
   342                         );
   343                         FindInMethod.super.visitInsn(Opcodes.AASTORE);
   344                     }
   345 
   346                     @Override
   347                     public SignatureVisitor visitArrayType() {
   348                         if (nowReturn) {
   349                             throw new IllegalStateException("Not supported yet");
   350                         }
   351                         loadObject();
   352                         return new SignatureWriter();
   353                     }
   354 
   355                     @Override
   356                     public void visitClassType(String name) {
   357                         if (nowReturn) {
   358                             returnType = Type.getObjectType(name);
   359                             return;
   360                         }
   361                         loadObject();
   362                     }
   363 
   364                     @Override
   365                     public SignatureVisitor visitReturnType() {
   366                         nowReturn = true;
   367                         return this;
   368                     }
   369 
   370                     private void loadObject() {
   371                         FindInMethod.super.visitInsn(Opcodes.DUP);
   372                         FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
   373                         FindInMethod.super.visitVarInsn(Opcodes.ALOAD, loadIndex++);
   374                         FindInMethod.super.visitInsn(Opcodes.AASTORE);
   375                     }
   376 
   377                 }
   378                 SV sv = new SV();
   379                 SignatureReader sr = new SignatureReader(desc);
   380                 sr.accept(sv);
   381 
   382                 if (needsVM) {
   383                     FindInMethod.super.visitInsn(Opcodes.DUP);
   384                     FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, sv.index);
   385                     int lastSlash = FindInClass.this.name.lastIndexOf('/');
   386                     String jsCallbacks = FindInClass.this.name.substring(0, lastSlash + 1) + "$JsCallbacks$";
   387                     FindInMethod.super.visitFieldInsn(Opcodes.GETSTATIC, jsCallbacks, "VM", "L" + jsCallbacks + ";");
   388                     FindInMethod.super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, jsCallbacks, "current", "()L" + jsCallbacks + ";");
   389                     FindInMethod.super.visitInsn(Opcodes.AASTORE);
   390                 }
   391 
   392                 super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   393                         "org/apidesign/html/boot/spi/Fn", "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
   394                 );
   395                 switch (sv.returnType.getSort()) {
   396                     case Type.VOID:
   397                         super.visitInsn(Opcodes.RETURN);
   398                         break;
   399                     case Type.ARRAY:
   400                     case Type.OBJECT:
   401                         super.visitTypeInsn(Opcodes.CHECKCAST, sv.returnType.getInternalName());
   402                         super.visitInsn(Opcodes.ARETURN);
   403                         break;
   404                     case Type.BOOLEAN:
   405                         super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Boolean");
   406                         super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   407                                 "java/lang/Boolean", "booleanValue", "()Z"
   408                         );
   409                         super.visitInsn(Opcodes.IRETURN);
   410                         break;
   411                     default:
   412                         super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Number");
   413                         super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   414                                 "java/lang/Number", sv.returnType.getClassName() + "Value", "()" + sv.returnType.getDescriptor()
   415                         );
   416                         super.visitInsn(sv.returnType.getOpcode(Opcodes.IRETURN));
   417                 }
   418                 return true;
   419             }
   420 
   421             @Override
   422             public void visitEnd() {
   423                 super.visitEnd();
   424                 if (body != null) {
   425                     if (generateBody()) {
   426                         // native method
   427                         super.visitMaxs(1, 0);
   428                     }
   429                     FindInClass.this.visitField(
   430                             Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
   431                             "$$fn$$" + name + "_" + found,
   432                             "Lorg/apidesign/html/boot/spi/Fn;",
   433                             null, null
   434                     );
   435                 }
   436             }
   437 
   438             private final class FindInAnno extends AnnotationVisitor {
   439 
   440                 private List<String> args = new ArrayList<String>();
   441                 private String body;
   442                 private boolean javacall = false;
   443 
   444                 public FindInAnno() {
   445                     super(Opcodes.ASM4);
   446                 }
   447 
   448                 @Override
   449                 public void visit(String name, Object value) {
   450                     if (name == null) {
   451                         args.add((String) value);
   452                         return;
   453                     }
   454                     if (name.equals("javacall")) { // NOI18N
   455                         javacall = (Boolean) value;
   456                         return;
   457                     }
   458                     assert name.equals("body");
   459                     body = (String) value;
   460                 }
   461 
   462                 @Override
   463                 public AnnotationVisitor visitArray(String name) {
   464                     return this;
   465                 }
   466 
   467                 @Override
   468                 public void visitEnd() {
   469                     if (body != null) {
   470                         if (javacall) {
   471                             body = callback(body);
   472                             args.add("vm");
   473                         }
   474                         generateJSBody(args, body);
   475                     }
   476                 }
   477             }
   478         }
   479 
   480         private final class LoadResource extends AnnotationVisitor {
   481 
   482             public LoadResource() {
   483                 super(Opcodes.ASM4);
   484             }
   485 
   486             @Override
   487             public void visit(String attrName, Object value) {
   488                 String relPath = (String) value;
   489                 if (relPath.startsWith("/")) {
   490                     loadScript(loader, relPath);
   491                 } else {
   492                     int last = name.lastIndexOf('/');
   493                     String fullPath = name.substring(0, last + 1) + relPath;
   494                     loadScript(loader, fullPath);
   495                 }
   496             }
   497         }
   498     }
   499 
   500     private static class ClassWriterEx extends ClassWriter {
   501 
   502         private ClassLoader loader;
   503 
   504         public ClassWriterEx(ClassLoader l, ClassReader classReader, int flags) {
   505             super(classReader, flags);
   506             this.loader = l;
   507         }
   508 
   509         @Override
   510         protected String getCommonSuperClass(final String type1, final String type2) {
   511             Class<?> c, d;
   512             try {
   513                 c = Class.forName(type1.replace('/', '.'), false, loader);
   514                 d = Class.forName(type2.replace('/', '.'), false, loader);
   515             } catch (Exception e) {
   516                 throw new RuntimeException(e.toString());
   517             }
   518             if (c.isAssignableFrom(d)) {
   519                 return type1;
   520             }
   521             if (d.isAssignableFrom(c)) {
   522                 return type2;
   523             }
   524             if (c.isInterface() || d.isInterface()) {
   525                 return "java/lang/Object";
   526             } else {
   527                 do {
   528                     c = c.getSuperclass();
   529                 } while (!c.isAssignableFrom(d));
   530                 return c.getName().replace('.', '/');
   531             }
   532         }
   533     }
   534 
   535     static byte[] transform(ClassLoader loader, byte[] arr) {
   536         ClassReader cr = new ClassReader(arr) {
   537             // to allow us to compile with -profile compact1 on 
   538             // JDK8 while processing the class as JDK7, the highest
   539             // class format asm 4.1 understands to
   540             @Override
   541             public short readShort(int index) {
   542                 short s = super.readShort(index);
   543                 if (index == 6 && s > Opcodes.V1_7) {
   544                     return Opcodes.V1_7;
   545                 }
   546                 return s;
   547             }
   548         };
   549         FindInClass tst = new FindInClass(loader, null);
   550         cr.accept(tst, 0);
   551         if (tst.found > 0) {
   552             ClassWriter w = new ClassWriterEx(loader, cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
   553             FindInClass fic = new FindInClass(loader, w);
   554             cr.accept(fic, 0);
   555             arr = w.toByteArray();
   556         }
   557         return arr;
   558     }
   559 
   560     private static final class TrueFn extends Fn {
   561         @Override
   562         public Object invoke(Object thiz, Object... args) throws Exception {
   563             return Boolean.TRUE;
   564         }
   565     } // end of TrueFn
   566 }