boot/src/main/java/org/apidesign/html/boot/impl/FnUtils.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Thu, 12 Dec 2013 23:54:32 +0100
changeset 349 53634fd10e30
parent 323 86aabecda7a3
child 350 25abd193f145
child 358 80702021b851
permissions -rw-r--r--
Load global scripts when first method in the class is called
     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         private String resource;
   160 
   161         public FindInClass(ClassLoader l, ClassVisitor cv) {
   162             super(Opcodes.ASM4, cv);
   163             this.loader = l;
   164         }
   165 
   166         @Override
   167         public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
   168             this.name = name;
   169             super.visit(version, access, name, signature, superName, interfaces);
   170         }
   171 
   172         @Override
   173         public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
   174             if ("Lnet/java/html/js/JavaScriptResource;".equals(desc)) {
   175                 return new LoadResource();
   176             }
   177             return super.visitAnnotation(desc, visible);
   178         }
   179 
   180         @Override
   181         public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
   182             return new FindInMethod(access, name, desc,
   183                     super.visitMethod(access & (~Opcodes.ACC_NATIVE), name, desc, signature, exceptions)
   184             );
   185         }
   186 
   187         private final class FindInMethod extends MethodVisitor {
   188 
   189             private final String name;
   190             private final String desc;
   191             private final int access;
   192             private List<String> args;
   193             private String body;
   194             private boolean bodyGenerated;
   195 
   196             public FindInMethod(int access, String name, String desc, MethodVisitor mv) {
   197                 super(Opcodes.ASM4, mv);
   198                 this.access = access;
   199                 this.name = name;
   200                 this.desc = desc;
   201             }
   202 
   203             @Override
   204             public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
   205                 if ("Lnet/java/html/js/JavaScriptBody;".equals(desc) // NOI18N
   206                         || "Lorg/apidesign/bck2brwsr/core/JavaScriptBody;".equals(desc) // NOI18N
   207                         ) {
   208                     found++;
   209                     return new FindInAnno();
   210                 }
   211                 return super.visitAnnotation(desc, visible);
   212             }
   213 
   214             private void generateJSBody(List<String> args, String body) {
   215                 this.args = args;
   216                 this.body = body;
   217             }
   218 
   219             @Override
   220             public void visitCode() {
   221                 if (body == null) {
   222                     return;
   223                 }
   224                 generateBody();
   225             }
   226 
   227             private boolean generateBody() {
   228                 if (bodyGenerated) {
   229                     return false;
   230                 }
   231                 bodyGenerated = true;
   232 
   233                 super.visitFieldInsn(
   234                         Opcodes.GETSTATIC, FindInClass.this.name,
   235                         "$$fn$$" + name + "_" + found,
   236                         "Lorg/apidesign/html/boot/spi/Fn;"
   237                 );
   238                 super.visitInsn(Opcodes.DUP);
   239                 super.visitMethodInsn(
   240                         Opcodes.INVOKESTATIC,
   241                         "org/apidesign/html/boot/spi/Fn", "isValid",
   242                         "(Lorg/apidesign/html/boot/spi/Fn;)Z"
   243                 );
   244                 Label ifNotNull = new Label();
   245                 super.visitJumpInsn(Opcodes.IFNE, ifNotNull);
   246 
   247                 // init Fn
   248                 super.visitInsn(Opcodes.POP);
   249                 super.visitLdcInsn(Type.getObjectType(FindInClass.this.name));
   250                 super.visitLdcInsn(body);
   251                 super.visitIntInsn(Opcodes.SIPUSH, args.size());
   252                 super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String");
   253                 boolean needsVM = false;
   254                 for (int i = 0; i < args.size(); i++) {
   255                     assert !needsVM;
   256                     String argName = args.get(i);
   257                     needsVM = "vm".equals(argName);
   258                     super.visitInsn(Opcodes.DUP);
   259                     super.visitIntInsn(Opcodes.BIPUSH, i);
   260                     super.visitLdcInsn(argName);
   261                     super.visitInsn(Opcodes.AASTORE);
   262                 }
   263                 super.visitMethodInsn(Opcodes.INVOKESTATIC,
   264                         "org/apidesign/html/boot/spi/Fn", "define",
   265                         "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/String;)Lorg/apidesign/html/boot/spi/Fn;"
   266                 );
   267                 if (resource != null) {
   268                     super.visitLdcInsn(Type.getObjectType(FindInClass.this.name));
   269                     super.visitLdcInsn(resource);
   270                     super.visitMethodInsn(Opcodes.INVOKESTATIC,
   271                             "org/apidesign/html/boot/spi/Fn", "preload",
   272                             "(Lorg/apidesign/html/boot/spi/Fn;Ljava/lang/Class;Ljava/lang/String;)Lorg/apidesign/html/boot/spi/Fn;"
   273                     );
   274                 }
   275                 super.visitInsn(Opcodes.DUP);
   276                 super.visitFieldInsn(
   277                         Opcodes.PUTSTATIC, FindInClass.this.name,
   278                         "$$fn$$" + name + "_" + found,
   279                         "Lorg/apidesign/html/boot/spi/Fn;"
   280                 );
   281                 // end of Fn init
   282 
   283                 super.visitLabel(ifNotNull);
   284 
   285                 final int offset;
   286                 if ((access & Opcodes.ACC_STATIC) == 0) {
   287                     offset = 1;
   288                     super.visitIntInsn(Opcodes.ALOAD, 0);
   289                 } else {
   290                     offset = 0;
   291                     super.visitInsn(Opcodes.ACONST_NULL);
   292                 }
   293 
   294                 super.visitIntInsn(Opcodes.SIPUSH, args.size());
   295                 super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
   296 
   297                 class SV extends SignatureVisitor {
   298 
   299                     private boolean nowReturn;
   300                     private Type returnType;
   301                     private int index;
   302                     private int loadIndex = offset;
   303 
   304                     public SV() {
   305                         super(Opcodes.ASM4);
   306                     }
   307 
   308                     @Override
   309                     public void visitBaseType(char descriptor) {
   310                         final Type t = Type.getType("" + descriptor);
   311                         if (nowReturn) {
   312                             returnType = t;
   313                             return;
   314                         }
   315                         FindInMethod.super.visitInsn(Opcodes.DUP);
   316                         FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
   317                         FindInMethod.super.visitVarInsn(t.getOpcode(Opcodes.ILOAD), loadIndex++);
   318                         String factory;
   319                         switch (descriptor) {
   320                             case 'I':
   321                                 factory = "java/lang/Integer";
   322                                 break;
   323                             case 'J':
   324                                 factory = "java/lang/Long";
   325                                 loadIndex++;
   326                                 break;
   327                             case 'S':
   328                                 factory = "java/lang/Short";
   329                                 break;
   330                             case 'F':
   331                                 factory = "java/lang/Float";
   332                                 break;
   333                             case 'D':
   334                                 factory = "java/lang/Double";
   335                                 loadIndex++;
   336                                 break;
   337                             case 'Z':
   338                                 factory = "java/lang/Boolean";
   339                                 break;
   340                             case 'C':
   341                                 factory = "java/lang/Character";
   342                                 break;
   343                             case 'B':
   344                                 factory = "java/lang/Byte";
   345                                 break;
   346                             default:
   347                                 throw new IllegalStateException(t.toString());
   348                         }
   349                         FindInMethod.super.visitMethodInsn(Opcodes.INVOKESTATIC,
   350                                 factory, "valueOf", "(" + descriptor + ")L" + factory + ";"
   351                         );
   352                         FindInMethod.super.visitInsn(Opcodes.AASTORE);
   353                     }
   354 
   355                     @Override
   356                     public SignatureVisitor visitArrayType() {
   357                         if (nowReturn) {
   358                             throw new IllegalStateException("Not supported yet");
   359                         }
   360                         loadObject();
   361                         return new SignatureWriter();
   362                     }
   363 
   364                     @Override
   365                     public void visitClassType(String name) {
   366                         if (nowReturn) {
   367                             returnType = Type.getObjectType(name);
   368                             return;
   369                         }
   370                         loadObject();
   371                     }
   372 
   373                     @Override
   374                     public SignatureVisitor visitReturnType() {
   375                         nowReturn = true;
   376                         return this;
   377                     }
   378 
   379                     private void loadObject() {
   380                         FindInMethod.super.visitInsn(Opcodes.DUP);
   381                         FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, index++);
   382                         FindInMethod.super.visitVarInsn(Opcodes.ALOAD, loadIndex++);
   383                         FindInMethod.super.visitInsn(Opcodes.AASTORE);
   384                     }
   385 
   386                 }
   387                 SV sv = new SV();
   388                 SignatureReader sr = new SignatureReader(desc);
   389                 sr.accept(sv);
   390 
   391                 if (needsVM) {
   392                     FindInMethod.super.visitInsn(Opcodes.DUP);
   393                     FindInMethod.super.visitIntInsn(Opcodes.SIPUSH, sv.index);
   394                     int lastSlash = FindInClass.this.name.lastIndexOf('/');
   395                     String jsCallbacks = FindInClass.this.name.substring(0, lastSlash + 1) + "$JsCallbacks$";
   396                     FindInMethod.super.visitFieldInsn(Opcodes.GETSTATIC, jsCallbacks, "VM", "L" + jsCallbacks + ";");
   397                     FindInMethod.super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, jsCallbacks, "current", "()L" + jsCallbacks + ";");
   398                     FindInMethod.super.visitInsn(Opcodes.AASTORE);
   399                 }
   400 
   401                 super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   402                         "org/apidesign/html/boot/spi/Fn", "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
   403                 );
   404                 switch (sv.returnType.getSort()) {
   405                     case Type.VOID:
   406                         super.visitInsn(Opcodes.RETURN);
   407                         break;
   408                     case Type.ARRAY:
   409                     case Type.OBJECT:
   410                         super.visitTypeInsn(Opcodes.CHECKCAST, sv.returnType.getInternalName());
   411                         super.visitInsn(Opcodes.ARETURN);
   412                         break;
   413                     case Type.BOOLEAN:
   414                         super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Boolean");
   415                         super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   416                                 "java/lang/Boolean", "booleanValue", "()Z"
   417                         );
   418                         super.visitInsn(Opcodes.IRETURN);
   419                         break;
   420                     default:
   421                         super.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Number");
   422                         super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
   423                                 "java/lang/Number", sv.returnType.getClassName() + "Value", "()" + sv.returnType.getDescriptor()
   424                         );
   425                         super.visitInsn(sv.returnType.getOpcode(Opcodes.IRETURN));
   426                 }
   427                 return true;
   428             }
   429 
   430             @Override
   431             public void visitEnd() {
   432                 super.visitEnd();
   433                 if (body != null) {
   434                     if (generateBody()) {
   435                         // native method
   436                         super.visitMaxs(1, 0);
   437                     }
   438                     FindInClass.this.visitField(
   439                             Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
   440                             "$$fn$$" + name + "_" + found,
   441                             "Lorg/apidesign/html/boot/spi/Fn;",
   442                             null, null
   443                     );
   444                 }
   445             }
   446 
   447             private final class FindInAnno extends AnnotationVisitor {
   448 
   449                 private List<String> args = new ArrayList<String>();
   450                 private String body;
   451                 private boolean javacall = false;
   452 
   453                 public FindInAnno() {
   454                     super(Opcodes.ASM4);
   455                 }
   456 
   457                 @Override
   458                 public void visit(String name, Object value) {
   459                     if (name == null) {
   460                         args.add((String) value);
   461                         return;
   462                     }
   463                     if (name.equals("javacall")) { // NOI18N
   464                         javacall = (Boolean) value;
   465                         return;
   466                     }
   467                     assert name.equals("body");
   468                     body = (String) value;
   469                 }
   470 
   471                 @Override
   472                 public AnnotationVisitor visitArray(String name) {
   473                     return this;
   474                 }
   475 
   476                 @Override
   477                 public void visitEnd() {
   478                     if (body != null) {
   479                         if (javacall) {
   480                             body = callback(body);
   481                             args.add("vm");
   482                         }
   483                         generateJSBody(args, body);
   484                     }
   485                 }
   486             }
   487         }
   488 
   489         private final class LoadResource extends AnnotationVisitor {
   490 
   491             public LoadResource() {
   492                 super(Opcodes.ASM4);
   493             }
   494 
   495             @Override
   496             public void visit(String attrName, Object value) {
   497                 String relPath = (String) value;
   498                 if (relPath.startsWith("/")) {
   499                     resource = relPath;
   500                 } else {
   501                     int last = name.lastIndexOf('/');
   502                     String fullPath = name.substring(0, last + 1) + relPath;
   503                     resource = fullPath;
   504                 }
   505             }
   506         }
   507     }
   508 
   509     private static class ClassWriterEx extends ClassWriter {
   510 
   511         private ClassLoader loader;
   512 
   513         public ClassWriterEx(ClassLoader l, ClassReader classReader, int flags) {
   514             super(classReader, flags);
   515             this.loader = l;
   516         }
   517 
   518         @Override
   519         protected String getCommonSuperClass(final String type1, final String type2) {
   520             Class<?> c, d;
   521             try {
   522                 c = Class.forName(type1.replace('/', '.'), false, loader);
   523                 d = Class.forName(type2.replace('/', '.'), false, loader);
   524             } catch (Exception e) {
   525                 throw new RuntimeException(e.toString());
   526             }
   527             if (c.isAssignableFrom(d)) {
   528                 return type1;
   529             }
   530             if (d.isAssignableFrom(c)) {
   531                 return type2;
   532             }
   533             if (c.isInterface() || d.isInterface()) {
   534                 return "java/lang/Object";
   535             } else {
   536                 do {
   537                     c = c.getSuperclass();
   538                 } while (!c.isAssignableFrom(d));
   539                 return c.getName().replace('.', '/');
   540             }
   541         }
   542     }
   543 
   544     static byte[] transform(ClassLoader loader, byte[] arr) {
   545         ClassReader cr = new ClassReader(arr) {
   546             // to allow us to compile with -profile compact1 on 
   547             // JDK8 while processing the class as JDK7, the highest
   548             // class format asm 4.1 understands to
   549             @Override
   550             public short readShort(int index) {
   551                 short s = super.readShort(index);
   552                 if (index == 6 && s > Opcodes.V1_7) {
   553                     return Opcodes.V1_7;
   554                 }
   555                 return s;
   556             }
   557         };
   558         FindInClass tst = new FindInClass(loader, null);
   559         cr.accept(tst, 0);
   560         if (tst.found > 0) {
   561             ClassWriter w = new ClassWriterEx(loader, cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
   562             FindInClass fic = new FindInClass(loader, w);
   563             cr.accept(fic, 0);
   564             arr = w.toByteArray();
   565         }
   566         return arr;
   567     }
   568 
   569     private static final class TrueFn extends Fn {
   570         @Override
   571         public Object invoke(Object thiz, Object... args) throws Exception {
   572             return Boolean.TRUE;
   573         }
   574     } // end of TrueFn
   575 }