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