boot/src/main/java/net/java/html/boot/BrowserBuilder.java
author Jaroslav Tulach <jtulach@netbeans.org>
Sat, 29 Nov 2014 22:25:40 +0100
changeset 884 af690d50d7d6
parent 882 0faee341d5b5
child 886 88d62267a0b5
permissions -rw-r--r--
Use optional dependency on asm-5.0.jar, so we don't have to bundle it when the classes are post processed.
     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;
    44 
    45 import java.io.File;
    46 import java.io.IOException;
    47 import java.io.InputStream;
    48 import java.lang.reflect.Method;
    49 import java.net.HttpURLConnection;
    50 import java.net.JarURLConnection;
    51 import java.net.MalformedURLException;
    52 import java.net.URL;
    53 import java.net.URLConnection;
    54 import java.security.ProtectionDomain;
    55 import java.util.Arrays;
    56 import java.util.Collection;
    57 import java.util.Enumeration;
    58 import java.util.Locale;
    59 import java.util.ServiceLoader;
    60 import java.util.concurrent.Executor;
    61 import java.util.logging.Level;
    62 import java.util.logging.Logger;
    63 import net.java.html.BrwsrCtx;
    64 import net.java.html.js.JavaScriptBody;
    65 import org.netbeans.html.boot.spi.Fn;
    66 import org.netbeans.html.boot.spi.Fn.Presenter;
    67 import org.netbeans.html.context.spi.Contexts;
    68 import org.netbeans.html.boot.impl.FindResources;
    69 import org.netbeans.html.boot.impl.FnContext;
    70 import org.netbeans.html.boot.impl.FnUtils;
    71 
    72 /** Use this builder to launch your Java/HTML based application. Typical
    73  * usage in a main method of your application looks like this: 
    74  * <pre>
    75  * 
    76  * <b>public static void</b> <em>main</em>(String... args) {
    77  *     BrowserBuilder.{@link #newBrowser newBrowser()}.
    78  *          {@link #loadClass(java.lang.Class) loadClass(YourMain.class)}.
    79  *          {@link #loadPage(java.lang.String) loadPage("index.html")}.
    80  *          {@link #locale(java.util.Locale) locale}({@link Locale#getDefault()}).
    81  *          {@link #invoke(java.lang.String, java.lang.String[]) invoke("initialized", args)}.
    82  *          {@link #showAndWait()};
    83  *     System.exit(0);
    84  * }
    85  * </pre>
    86  * The above will load <code>YourMain</code> class via
    87  * a special classloader, it will locate an <code>index.html</code> (relative
    88  * to <code>YourMain</code> class) and show it in a browser window. When the
    89  * initialization is over, a <b>public static</b> method <em>initialized</em>
    90  * in <code>YourMain</code> will be called with provided string parameters.
    91  * <p>
    92  * This module provides only API for building browsers. To use it properly one
    93  * also needs an implementation on the classpath of one's application. For example
    94  * use: <pre>
    95  * &lt;dependency&gt;
    96  *   &lt;groupId&gt;org.netbeans.html&lt;/groupId&gt;
    97  *   &lt;artifactId&gt;net.java.html.boot.fx&lt;/artifactId&gt;
    98  *   &lt;scope&gt;runtime&lt;/scope&gt;
    99  * &lt;/dependency&gt;
   100  * </pre>
   101  *
   102  * @author Jaroslav Tulach
   103  */
   104 public final class BrowserBuilder {
   105     private static final Logger LOG = Logger.getLogger(BrowserBuilder.class.getName());
   106     
   107     private String resource;
   108     private Class<?> clazz;
   109     private Class[] browserClass;
   110     private Runnable onLoad;
   111     private String methodName;
   112     private String[] methodArgs;
   113     private final Object[] context;
   114     private ClassLoader loader;
   115     private Locale locale;
   116     
   117     private BrowserBuilder(Object[] context) {
   118         this.context = context;
   119     }
   120 
   121     /** Entry method to obtain a new browser builder. Follow by calling 
   122      * its instance methods like {@link #loadClass(java.lang.Class)} and
   123      * {@link #loadPage(java.lang.String)}.
   124      * 
   125      * @param context any instances that should be available to the builder -
   126      *   implementation dependant
   127      * @return new browser builder
   128      */
   129     public static BrowserBuilder newBrowser(Object... context) {
   130         return new BrowserBuilder(context);
   131     }
   132     
   133     /** The class to load when the browser is initialized. This class
   134      * is loaded by a special classloader (that supports {@link JavaScriptBody}
   135      * and co.). 
   136      * 
   137      * @param mainClass the class to load and resolve when the browser is ready
   138      * @return this builder
   139      */
   140     public BrowserBuilder loadClass(Class<?> mainClass) {
   141         this.clazz = mainClass;
   142         return this;
   143     }
   144     
   145     /** Allows one to specify a runnable that should be invoked when a load
   146      * of a page is finished. This method may be used in addition or instead
   147      * of {@link #loadClass(java.lang.Class)} and 
   148      * {@link #invoke(java.lang.String, java.lang.String...)} methods.
   149      * 
   150      * @param r the code to run when the page is loaded
   151      * @return this builder
   152      * @since 0.8.1
   153      */
   154     public BrowserBuilder loadFinished(Runnable r) {
   155         this.onLoad = r;
   156         return this;
   157     }
   158 
   159     /** Page to load into the browser. If the <code>page</code> represents
   160      * a {@link URL} known to the Java system, the URL is passed to the browser. 
   161      * If system property <code>browser.rootdir</code> is specified, then a
   162      * file <code>page</code> relative to this directory is used as the URL.
   163      * If no such file exists, the system seeks for the 
   164      * resource via {@link Class#getResource(java.lang.String)}
   165      * method (relative to the {@link #loadClass(java.lang.Class) specified class}). 
   166      * If such resource is not found, a file relative to the location JAR
   167      * that contains the {@link #loadClass(java.lang.Class) main class} is 
   168      * searched for.
   169      * <p>
   170      * The search honors provided {@link #locale}, if specified.
   171      * E.g. it will prefer <code>index_cs.html</code> over <code>index.html</code>
   172      * if the locale is set to <code>cs_CZ</code>.
   173      * 
   174      * @param page the location (relative, absolute, or URL) of a page to load
   175      * @return this builder
   176      */
   177     public BrowserBuilder loadPage(String page) {
   178         this.resource = page;
   179         return this;
   180     }
   181     
   182     /** Locale to use when searching for an initial {@link #loadPage(java.lang.String) page to load}.
   183      * Localization is best done by providing different versions of the 
   184      * initial page with appropriate suffixes (like <code>index_cs.html</code>).
   185      * Then one can call this method with value of {@link Locale#getDefault()}
   186      * to instruct the builder to use the user's current locale.
   187      * 
   188      * @param locale the locale to use or <code>null</code> if no suffix search should be performed
   189      * @return this builder
   190      * @since 1.0
   191      */
   192     public BrowserBuilder locale(Locale locale) {
   193         this.locale = locale;
   194         return this;
   195     }
   196     
   197     /** Specifies callback method to notify the application that the browser is ready.
   198      * There should be a <b>public static</b> method in the class specified
   199      * by {@link #loadClass(java.lang.Class)} which takes an array of {@link String}
   200      * argument. The method is called on the browser dispatch thread one
   201      * the browser finishes loading of the {@link #loadPage(java.lang.String) HTML page}.
   202      * 
   203      * @param methodName name of a method to seek for
   204      * @param args parameters to pass to the method
   205      * @return this builder
   206      */
   207     public BrowserBuilder invoke(String methodName, String... args) {
   208         this.methodName = methodName;
   209         this.methodArgs = args;
   210         return this;
   211     }
   212 
   213     /** Loader to use when searching for classes to initialize. 
   214      * If specified, this loader is going to be used to load {@link Presenter}
   215      * and {@link Contexts#fillInByProviders(java.lang.Class, org.netbeans.html.context.spi.Contexts.Builder) fill} {@link BrwsrCtx} in.
   216      * Specifying special classloader may be useful in modular systems, 
   217      * like OSGi, where one needs to load classes from many otherwise independent
   218      * modules.
   219      * 
   220      * @param l the loader to use (or <code>null</code>)
   221      * @return this builder
   222      * @since 0.9
   223      */
   224     public BrowserBuilder classloader(ClassLoader l) {
   225         this.loader = l;
   226         return this;
   227     }
   228 
   229     /** Shows the browser, loads specified page in and executes the 
   230      * {@link #invoke(java.lang.String, java.lang.String[]) initialization method}.
   231      * The method returns when the browser is closed.
   232      * 
   233      * @throws NullPointerException if some of essential parameters (like {@link #loadPage(java.lang.String) page} or
   234      *    {@link #loadClass(java.lang.Class) class} have not been specified
   235      */
   236     public void showAndWait() {
   237         if (resource == null) {
   238             throw new NullPointerException("Need to specify resource via loadPage method");
   239         }
   240         
   241         final Class<?> myCls;
   242         if (clazz != null) {
   243             myCls = clazz;
   244         } else if (onLoad != null) {
   245             myCls = onLoad.getClass();
   246         } else {
   247             throw new NullPointerException("loadClass, neither loadFinished was called!");
   248         }
   249         IOException mal[] = { null };
   250         URL url = findLocalizedResourceURL(resource, locale, mal, myCls);
   251         
   252         Fn.Presenter dfnr = null;
   253         for (Object o : context) {
   254             if (o instanceof Fn.Presenter) {
   255                 dfnr = (Fn.Presenter)o;
   256                 break;
   257             }
   258         }
   259 
   260         if (dfnr == null && loader != null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class, loader)) {
   261             dfnr = o;
   262             break;
   263         }
   264         
   265         if (dfnr == null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class)) {
   266             dfnr = o;
   267             break;
   268         }
   269         
   270         if (dfnr == null) {
   271             throw new IllegalStateException("Can't find any Fn.Presenter");
   272         }
   273         
   274         final ClassLoader activeLoader;
   275         if (loader != null) {
   276             if (!FnContext.isJavaScriptCapable(loader)) {
   277                 throw new IllegalStateException("Loader cannot resolve @JavaScriptBody: " + loader);
   278             }
   279             activeLoader = loader;
   280         } else if (FnContext.isJavaScriptCapable(myCls.getClassLoader())) {
   281             activeLoader = myCls.getClassLoader();
   282         } else {
   283             if (!FnContext.isAsmPresent()) {
   284                 throw new IllegalStateException("Cannot find asm-5.0.jar classes!");
   285             }
   286             FImpl impl = new FImpl(myCls.getClassLoader());
   287             activeLoader = FnUtils.newLoader(impl, dfnr, myCls.getClassLoader().getParent());
   288         }
   289         
   290         final Fn.Presenter dP = dfnr;
   291 
   292         class OnPageLoad implements Runnable {
   293             @Override
   294             public void run() {
   295                 try {
   296                     final Fn.Presenter aP = Fn.activePresenter();
   297                     final Fn.Presenter currentP = aP != null ? aP : dP;
   298                     
   299                     Thread.currentThread().setContextClassLoader(activeLoader);
   300                     final Class<?> newClazz = Class.forName(myCls.getName(), true, activeLoader);
   301                     if (browserClass != null) {
   302                         browserClass[0] = newClazz;
   303                     }
   304                     Contexts.Builder cb = Contexts.newBuilder();
   305                     if (!Contexts.fillInByProviders(newClazz, cb)) {
   306                         LOG.log(Level.WARNING, "Using empty technology for {0}", newClazz);
   307                     }
   308                     if (currentP instanceof Executor) {
   309                         cb.register(Executor.class, (Executor)currentP, 1000);
   310                     }
   311                     cb.register(Fn.Presenter.class, currentP, 1000);
   312                     BrwsrCtx c = cb.build();
   313 
   314                     class CallInitMethod implements Runnable {
   315                         @Override
   316                         public void run() {
   317                             Throwable firstError = null;
   318                             if (onLoad != null) {
   319                                 try {
   320                                     FnContext.currentPresenter(currentP);
   321                                     onLoad.run();
   322                                 } catch (Throwable ex) {
   323                                     firstError = ex;
   324                                 } finally {
   325                                     FnContext.currentPresenter(null);
   326                                 }
   327                             }
   328                             INIT: if (methodName != null) {
   329                                 if (methodArgs.length == 0) {
   330                                     try {
   331                                         Method m = newClazz.getMethod(methodName);
   332                                         FnContext.currentPresenter(currentP);
   333                                         m.invoke(null);
   334                                         firstError = null;
   335                                         break INIT;
   336                                     } catch (Throwable ex) {
   337                                         firstError = ex;
   338                                     } finally {
   339                                         FnContext.currentPresenter(null);
   340                                     }
   341                                 }
   342                                 try {
   343                                     Method m = newClazz.getMethod(methodName, String[].class);
   344                                     FnContext.currentPresenter(currentP);
   345                                     m.invoke(m, (Object) methodArgs);
   346                                     firstError = null;
   347                                 } catch (Throwable ex) {
   348                                     LOG.log(Level.SEVERE, "Can't call " + methodName + " with args " + Arrays.toString(methodArgs), ex);
   349                                 } finally {
   350                                     FnContext.currentPresenter(null);
   351                                 }
   352                             }
   353                             if (firstError != null) {
   354                                 LOG.log(Level.SEVERE, "Can't initialize the view", firstError);
   355                             }
   356                         }
   357                     }
   358                     
   359                     c.execute(new CallInitMethod());
   360                 } catch (ClassNotFoundException ex) {
   361                     LOG.log(Level.SEVERE, "Can't load " + myCls.getName(), ex);
   362                 }
   363             }
   364         }
   365         dfnr.displayPage(url, new OnPageLoad());
   366     }
   367 
   368     private static URL findResourceURL(String resource, String suffix, IOException[] mal, Class<?> relativeTo) {
   369         if (suffix != null) {
   370             int lastDot = resource.lastIndexOf('.');
   371             if (lastDot != -1) {
   372                 resource = resource.substring(0, lastDot) + suffix + resource.substring(lastDot);
   373             } else {
   374                 resource = resource + suffix;
   375             }
   376         }
   377         
   378         URL url = null;
   379         try {
   380             String baseURL = System.getProperty("browser.rootdir"); // NOI18N
   381             if (baseURL != null) {
   382                 URL u = new File(baseURL, resource).toURI().toURL();
   383                 if (isReal(u)) {
   384                     url = u;
   385                 }
   386             } 
   387             
   388             {
   389                 URL u = new URL(resource);
   390                 if (suffix == null || isReal(u)) {
   391                     url = u;
   392                 }
   393                 return url;
   394             }
   395         } catch (MalformedURLException ex) {
   396             mal[0] = ex;
   397         }
   398         
   399         if (url == null) {
   400             url = relativeTo.getResource(resource);
   401         }
   402         if (url == null) {
   403             final ProtectionDomain pd = relativeTo.getProtectionDomain();
   404             if (pd != null && pd.getCodeSource() != null) {
   405                 URL jar = pd.getCodeSource().getLocation();
   406                 try {
   407                     URL u = new URL(jar, resource);
   408                     if (isReal(u)) {
   409                         url = u;
   410                     }
   411                 } catch (MalformedURLException ex) {
   412                     ex.initCause(mal[0]);
   413                     mal[0] = ex;
   414                 }
   415             }
   416         }
   417         if (url == null) {
   418             URL res = BrowserBuilder.class.getResource("html4j.txt");
   419             LOG.log(Level.FINE, "Found html4j {0}", res);
   420             if (res != null) {
   421                 try {
   422                     URLConnection c = res.openConnection();
   423                     LOG.log(Level.FINE, "testing : {0}", c);
   424                     if (c instanceof JarURLConnection) {
   425                         JarURLConnection jc = (JarURLConnection) c;
   426                         URL base = jc.getJarFileURL();
   427                         for (int i = 0; i < 50; i++) {
   428                             URL u = new URL(base, resource);
   429                             if (isReal(u)) {
   430                                 url = u;
   431                                 break;
   432                             }
   433                             base = new URL(base, "..");
   434                         }
   435                     }
   436                 } catch (IOException ex) {
   437                     mal[0] = ex;
   438                 }
   439             }
   440         }
   441         return url;
   442     }
   443 
   444     static URL findLocalizedResourceURL(String resource, Locale l, IOException[] mal, Class<?> relativeTo) {
   445         URL url = null;
   446         if (l != null) {
   447             url = findResourceURL(resource, "_" + l.getLanguage() + "_" + l.getCountry(), mal, relativeTo);
   448             if (url != null) {
   449                 return url;
   450             }
   451             url = findResourceURL(resource, "_" + l.getLanguage(), mal, relativeTo);
   452         }
   453         if (url != null) {
   454             return url;
   455         }
   456         return findResourceURL(resource, null, mal, relativeTo);
   457     }
   458     
   459     private static boolean isReal(URL u) {
   460         try {
   461             URLConnection conn = u.openConnection();
   462             if (conn instanceof HttpURLConnection) {
   463                 HttpURLConnection hc = (HttpURLConnection) conn;
   464                 hc.setReadTimeout(5000);
   465                 if (hc.getResponseCode() >= 300) {
   466                     throw new IOException("Wrong code: " + hc.getResponseCode());
   467                 }
   468             }
   469             InputStream is = conn.getInputStream();
   470             is.close();
   471             LOG.log(Level.FINE, "found real url: {0}", u);
   472             return true;
   473         } catch (IOException ignore) {
   474             LOG.log(Level.FINE, "Cannot open " + u, ignore);
   475             return false;
   476         }
   477     }
   478 
   479     private static final class FImpl implements FindResources {
   480         final ClassLoader l;
   481 
   482         public FImpl(ClassLoader l) {
   483             this.l = l;
   484         }
   485 
   486         @Override
   487         public void findResources(String path, Collection<? super URL> results, boolean oneIsEnough) {
   488             if (oneIsEnough) {
   489                 URL u = l.getResource(path);
   490                 if (u != null) {
   491                     results.add(u);
   492                 }
   493             } else {
   494                 try {
   495                     Enumeration<URL> en = l.getResources(path);
   496                     while (en.hasMoreElements()) {
   497                         results.add(en.nextElement());
   498                     }
   499                 } catch (IOException ex) {
   500                     // no results
   501                 }
   502             }
   503         }
   504         
   505     }
   506 }