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