boot-script/src/main/java/net/java/html/boot/script/ScriptPresenter.java
author Jaroslav Tulach <jtulach@netbeans.org>
Wed, 13 Jan 2016 05:53:12 +0100
changeset 1040 6429e051b1de
parent 915 15af7ebf1d0e
child 1083 b49546d64269
permissions -rw-r--r--
Better formatting of the function envelope
     1 /**
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
    31  *
    32  * If you wish your version of this file to be governed by only the CDDL
    33  * or only the GPL Version 2, indicate your decision by adding
    34  * "[Contributor] elects to include this software in this distribution
    35  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    36  * single choice of license, a recipient has the option to distribute
    37  * your version of this file under either the CDDL, the GPL Version 2 or
    38  * to extend the choice of license to its licensees as provided above.
    39  * However, if you add GPL Version 2 code and therefore, elected the GPL
    40  * Version 2 license, then the option applies only if the new code is
    41  * made subject to such option by the copyright holder.
    42  */
    43 package net.java.html.boot.script;
    44 
    45 import java.io.Closeable;
    46 import java.io.IOException;
    47 import java.io.Reader;
    48 import java.lang.ref.WeakReference;
    49 import java.net.URL;
    50 import java.util.ArrayList;
    51 import java.util.List;
    52 import java.util.concurrent.Executor;
    53 import java.util.logging.Level;
    54 import java.util.logging.Logger;
    55 import javax.script.Invocable;
    56 import javax.script.ScriptEngine;
    57 import javax.script.ScriptEngineManager;
    58 import javax.script.ScriptException;
    59 import org.netbeans.html.boot.spi.Fn;
    60 import org.netbeans.html.boot.spi.Fn.Presenter;
    61 
    62 /** Implementation of {@link Presenter} that delegates
    63  * to Java {@link ScriptEngine scripting} API. The presenter runs headless
    64  * without appropriate simulation of browser APIs. Its primary usefulness
    65  * is inside testing environments. 
    66  * <p>
    67  * One can load in browser simulation for example from 
    68  * <a href="http://www.envjs.com/">env.js</a>. The best way to achieve so,
    69  * is to wait until JDK-8046013 gets fixed....
    70  * 
    71  *
    72  * @author Jaroslav Tulach
    73  */
    74 final class ScriptPresenter implements Fn.KeepAlive,
    75 Presenter, Fn.FromJavaScript, Fn.ToJavaScript, Executor {
    76     private static final Logger LOG = Logger.getLogger(ScriptPresenter.class.getName());
    77     private final ScriptEngine eng;
    78     private final Executor exc;
    79 
    80     public ScriptPresenter(Executor exc) {
    81         this.exc = exc;
    82         try {
    83             eng = new ScriptEngineManager().getEngineByName("javascript");
    84             eng.eval("function alert(msg) { Packages.java.lang.System.out.println(msg); };");
    85             eng.eval("function confirm(msg) { Packages.java.lang.System.out.println(msg); return true; };");
    86             eng.eval("function prompt(msg, txt) { Packages.java.lang.System.out.println(msg + ':' + txt); return txt; };");
    87         } catch (ScriptException ex) {
    88             throw new IllegalStateException(ex);
    89         }
    90     }
    91 
    92     @Override
    93     public Fn defineFn(String code, String... names) {
    94         return defineImpl(code, names, null);
    95     }
    96 
    97     @Override
    98     public Fn defineFn(String code, String[] names, boolean[] keepAlive) {
    99         return defineImpl(code, names, keepAlive);
   100     }    
   101     private FnImpl defineImpl(String code, String[] names, boolean[] keepAlive) {
   102         StringBuilder sb = new StringBuilder();
   103         sb.append("(function() {\n");
   104         sb.append("  return function(");
   105         String sep = "";
   106         if (names != null) for (String n : names) {
   107             sb.append(sep).append(n);
   108             sep = ",";
   109         }
   110         sb.append(") {\n");
   111         sb.append(code);
   112         sb.append("\n  };\n");
   113         sb.append("})()\n");
   114 
   115         final Object fn;
   116         try {
   117             fn = eng.eval(sb.toString());
   118         } catch (ScriptException ex) {
   119             throw new IllegalStateException(ex);
   120         }
   121         return new FnImpl(this, fn, keepAlive);
   122     }
   123 
   124     @Override
   125     public void displayPage(URL page, Runnable onPageLoad) {
   126         try {
   127             eng.eval("if (typeof window !== 'undefined') window.location = '" + page + "'");
   128         } catch (ScriptException ex) {
   129             LOG.log(Level.SEVERE, "Cannot load " + page, ex);
   130         }
   131         if (onPageLoad != null) {
   132             onPageLoad.run();
   133         }
   134     }
   135 
   136     @Override
   137     public void loadScript(Reader code) throws Exception {
   138         eng.eval(code);
   139     }
   140     
   141     //
   142     // array conversions
   143     //
   144     
   145     final Object convertArrays(Object[] arr) throws Exception {
   146         for (int i = 0; i < arr.length; i++) {
   147             if (arr[i] instanceof Object[]) {
   148                 arr[i] = convertArrays((Object[]) arr[i]);
   149             }
   150         }
   151         final Object wrapArr = wrapArrFn().invokeImpl(null, false, arr); // NOI18N
   152         return wrapArr;
   153     }
   154 
   155     private FnImpl wrapArrImpl;
   156     private FnImpl wrapArrFn() {
   157         if (wrapArrImpl == null) {
   158             try {
   159                 wrapArrImpl = defineImpl("return Array.prototype.slice.call(arguments);", null, null);
   160             } catch (Exception ex) {
   161                 throw new IllegalStateException(ex);
   162             }
   163         }
   164         return wrapArrImpl;
   165     }
   166 
   167     final Object checkArray(Object val) throws Exception {
   168         final FnImpl fn = arraySizeFn();
   169         final Object fnRes = fn.invokeImpl(null, false, val, null);
   170         int length = ((Number) fnRes).intValue();
   171         if (length == -1) {
   172             return val;
   173         }
   174         Object[] arr = new Object[length];
   175         fn.invokeImpl(null, false, val, arr);
   176         return arr;
   177     }
   178     private FnImpl arraySize;
   179     private FnImpl arraySizeFn() {
   180         if (arraySize == null) {
   181             try {
   182                 arraySize = defineImpl("\n"
   183                     + "if (to === null) {\n"
   184                     + "  if (Object.prototype.toString.call(arr) === '[object Array]') return arr.length;\n"
   185                     + "  else return -1;\n"
   186                     + "} else {\n"
   187                     + "  var l = arr.length;\n"
   188                     + "  for (var i = 0; i < l; i++) to[i] = arr[i];\n"
   189                     + "  return l;\n"
   190                     + "}", new String[] { "arr", "to" }, null
   191                 );
   192             } catch (Exception ex) {
   193                 throw new IllegalStateException(ex);
   194             }
   195         }
   196         return arraySize;
   197     }
   198 
   199     @Override
   200     public Object toJava(Object jsArray) {
   201         if (jsArray instanceof Weak) {
   202             jsArray = ((Weak)jsArray).get();
   203         }
   204         try {
   205             return checkArray(jsArray);
   206         } catch (Exception ex) {
   207             throw new IllegalStateException(ex);
   208         }
   209     }
   210     
   211     @Override
   212     public Object toJavaScript(Object toReturn) {
   213         if (toReturn instanceof Object[]) {
   214             try {
   215                 return convertArrays((Object[])toReturn);
   216             } catch (Exception ex) {
   217                 throw new IllegalStateException(ex);
   218             }
   219         } else {
   220             return toReturn;
   221         }
   222     }
   223 
   224     @Override
   225     public void execute(final Runnable command) {
   226         if (Fn.activePresenter() == this) {
   227             command.run();
   228             return;
   229         }
   230         
   231         class Wrap implements Runnable {
   232             public void run() {
   233                 try (Closeable c = Fn.activate(ScriptPresenter.this)) {
   234                     command.run();
   235                 } catch (IOException ex) {
   236                     throw new IllegalStateException(ex);
   237                 }
   238             }
   239         }
   240         final Runnable wrap = new Wrap();
   241         if (exc == null) {
   242             wrap.run();
   243         } else {
   244             exc.execute(wrap);
   245         }
   246     }
   247 
   248     private class FnImpl extends Fn {
   249 
   250         private final Object fn;
   251         private final boolean[] keepAlive;
   252 
   253         public FnImpl(Presenter presenter, Object fn, boolean[] keepAlive) {
   254             super(presenter);
   255             this.fn = fn;
   256             this.keepAlive = keepAlive;
   257         }
   258 
   259         @Override
   260         public Object invoke(Object thiz, Object... args) throws Exception {
   261             return invokeImpl(thiz, true, args);
   262         }
   263 
   264             final Object invokeImpl(Object thiz, boolean arrayChecks, Object... args) throws Exception {
   265                 List<Object> all = new ArrayList<>(args.length + 1);
   266                 all.add(thiz == null ? fn : thiz);
   267                 for (int i = 0; i < args.length; i++) {
   268                     Object conv = args[i];
   269                     if (arrayChecks) {
   270                         if (args[i] instanceof Object[]) {
   271                             Object[] arr = (Object[]) args[i];
   272                             conv = ((ScriptPresenter) presenter()).convertArrays(arr);
   273                         }
   274                         if (conv != null && keepAlive != null
   275                             && !keepAlive[i] && !isJSReady(conv)
   276                             && !conv.getClass().getSimpleName().equals("$JsCallbacks$") // NOI18N
   277                             ) {
   278                             conv = new Weak(conv);
   279                         }
   280                         if (conv instanceof Character) {
   281                             conv = (int)(Character)conv;
   282                         }
   283                     }
   284                     all.add(conv);
   285                 }
   286                 Object ret = ((Invocable)eng).invokeMethod(fn, "call", all.toArray()); // NOI18N
   287                 if (ret instanceof Weak) {
   288                     ret = ((Weak)ret).get();
   289                 }
   290                 if (ret == fn) {
   291                     return null;
   292                 }
   293                 if (!arrayChecks) {
   294                     return ret;
   295                 }
   296                 return ((ScriptPresenter)presenter()).checkArray(ret);
   297             }
   298     }
   299     
   300     private static boolean isJSReady(Object obj) {
   301         if (obj == null) {
   302             return true;
   303         }
   304         if (obj instanceof String) {
   305             return true;
   306         }
   307         if (obj instanceof Number) {
   308             return true;
   309         }
   310         final String cn = obj.getClass().getName();
   311         if (cn.startsWith("jdk.nashorn") || ( // NOI18N
   312             cn.contains(".mozilla.") && cn.contains(".Native") // NOI18N
   313         )) {
   314             return true;
   315         }
   316         if (obj instanceof Character) {
   317             return true;
   318         }
   319         return false;
   320     }    
   321     
   322     private static final class Weak extends WeakReference<Object> {
   323         public Weak(Object referent) {
   324             super(referent);
   325         }
   326     }
   327 }