boot/src/main/java/net/java/html/boot/BrowserBuilder.java
author Jaroslav Tulach <jtulach@netbeans.org>
Thu, 05 Jun 2014 21:01:56 +0200
branchenvjs
changeset 700 055a0f3766f5
parent 662 1c26fa21a8cd
child 736 8e6e4f811215
permissions -rw-r--r--
A bit of documentation
     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.FileNotFoundException;
    47 import java.io.IOException;
    48 import java.io.InputStream;
    49 import java.lang.reflect.Method;
    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.ServiceLoader;
    59 import java.util.concurrent.Executor;
    60 import java.util.logging.Level;
    61 import java.util.logging.Logger;
    62 import net.java.html.BrwsrCtx;
    63 import net.java.html.js.JavaScriptBody;
    64 import org.apidesign.html.boot.spi.Fn;
    65 import org.apidesign.html.context.spi.Contexts;
    66 import org.netbeans.html.boot.impl.FindResources;
    67 import org.netbeans.html.boot.impl.FnContext;
    68 import org.netbeans.html.boot.impl.FnUtils;
    69 
    70 /** Use this builder to launch your Java/HTML based application. Typical
    71  * usage in a main method of your application looks like this: 
    72  * <pre>
    73  * 
    74  * <b>public static void</b> <em>main</em>(String... args) {
    75  *     BrowserBuilder.{@link #newBrowser newBrowser()}.
    76  *          {@link #loadClass(java.lang.Class) loadClass(YourMain.class)}.
    77  *          {@link #loadPage(java.lang.String) loadPage("index.html")}.
    78  *          {@link #invoke(java.lang.String, java.lang.String[]) invoke("initialized", args)}.
    79  *          {@link #showAndWait()};
    80  *     System.exit(0);
    81  * }
    82  * </pre>
    83  * The above will load <code>YourMain</code> class via
    84  * a special classloader, it will locate an <code>index.html</code> (relative
    85  * to <code>YourMain</code> class) and show it in a browser window. When the
    86  * initialization is over, a <b>public static</b> method <em>initialized</em>
    87  * in <code>YourMain</code> will be called with provided string parameters.
    88  * <p>
    89  * This module provides only API for building browsers. To use it properly one
    90  * also needs an implementation on the classpath of one's application. For example
    91  * use: <pre>
    92  * &lt;dependency&gt;
    93  *   &lt;groupId&gt;org.netbeans.html&lt;/groupId&gt;
    94  *   &lt;artifactId&gt;net.java.html.boot.fx&lt;/artifactId&gt;
    95  *   &lt;scope&gt;runtime&lt;/scope&gt;
    96  * &lt;/dependency&gt;
    97  * </pre>
    98  *
    99  * @author Jaroslav Tulach <jtulach@netbeans.org>
   100  */
   101 public final class BrowserBuilder {
   102     private static final Logger LOG = Logger.getLogger(BrowserBuilder.class.getName());
   103     
   104     private String resource;
   105     private Class<?> clazz;
   106     private Class[] browserClass;
   107     private Runnable onLoad;
   108     private String methodName;
   109     private String[] methodArgs;
   110     private final Object[] context;
   111     
   112     private BrowserBuilder(Object[] context) {
   113         this.context = context;
   114     }
   115 
   116     /** Entry method to obtain a new browser builder. Follow by calling 
   117      * its instance methods like {@link #loadClass(java.lang.Class)} and
   118      * {@link #loadPage(java.lang.String)}.
   119      * 
   120      * @param context any instances that should be available to the builder -
   121      *   implementation dependant
   122      * @return new browser builder
   123      */
   124     public static BrowserBuilder newBrowser(Object... context) {
   125         return new BrowserBuilder(context);
   126     }
   127     
   128     /** The class to load when the browser is initialized. This class
   129      * is loaded by a special classloader (that supports {@link JavaScriptBody}
   130      * and co.). 
   131      * 
   132      * @param mainClass the class to load and resolve when the browser is ready
   133      * @return this builder
   134      */
   135     public BrowserBuilder loadClass(Class<?> mainClass) {
   136         this.clazz = mainClass;
   137         return this;
   138     }
   139     
   140     /** Allows one to specify a runnable that should be invoked when a load
   141      * of a page is finished. This method may be used in addition or instead
   142      * of {@link #loadClass(java.lang.Class)} and 
   143      * {@link #invoke(java.lang.String, java.lang.String...)} methods.
   144      * 
   145      * @param r the code to run when the page is loaded
   146      * @return this builder
   147      * @since 0.8.1
   148      */
   149     public BrowserBuilder loadFinished(Runnable r) {
   150         this.onLoad = r;
   151         return this;
   152     }
   153 
   154     /** Page to load into the browser. If the <code>page</code> represents
   155      * a {@link URL} known to the Java system, the URL is passed to the browser. 
   156      * If system property <code>browser.rootdir</code> is specified, then a
   157      * file <code>page</code> relative to this directory is used as the URL.
   158      * If no such file exists, the system seeks for the 
   159      * resource via {@link Class#getResource(java.lang.String)}
   160      * method (relative to the {@link #loadClass(java.lang.Class) specified class}). 
   161      * If such resource is not found, a file relative to the location JAR
   162      * that contains the {@link #loadClass(java.lang.Class) main class} is 
   163      * searched for.
   164      * 
   165      * @param page the location (relative, absolute, or URL) of a page to load
   166      * @return this browser
   167      */
   168     public BrowserBuilder loadPage(String page) {
   169         this.resource = page;
   170         return this;
   171     }
   172     
   173     /** Specifies callback method to notify the application that the browser is ready.
   174      * There should be a <b>public static</b> method in the class specified
   175      * by {@link #loadClass(java.lang.Class)} which takes an array of {@link String}
   176      * argument. The method is called on the browser dispatch thread one
   177      * the browser finishes loading of the {@link #loadPage(java.lang.String) HTML page}.
   178      * 
   179      * @param methodName name of a method to seek for
   180      * @param args parameters to pass to the method
   181      * @return this builder
   182      */
   183     public BrowserBuilder invoke(String methodName, String... args) {
   184         this.methodName = methodName;
   185         this.methodArgs = args;
   186         return this;
   187     }
   188 
   189     /** Shows the browser, loads specified page in and executes the 
   190      * {@link #invoke(java.lang.String, java.lang.String[]) initialization method}.
   191      * The method returns when the browser is closed.
   192      * 
   193      * @throws NullPointerException if some of essential parameters (like {@link #loadPage(java.lang.String) page} or
   194      *    {@link #loadClass(java.lang.Class) class} have not been specified
   195      */
   196     public void showAndWait() {
   197         if (resource == null) {
   198             throw new NullPointerException("Need to specify resource via loadPage method");
   199         }
   200         
   201         URL url = null;
   202         IOException mal = null;
   203         try {
   204             String baseURL = System.getProperty("browser.rootdir");
   205             if (baseURL != null) {
   206                 url = new File(baseURL, resource).toURI().toURL();
   207             } else {
   208                 url = new URL(resource);
   209             }
   210         } catch (MalformedURLException ex) {
   211             mal = ex;
   212         }
   213         final Class<?> myCls;
   214         if (clazz != null) {
   215             myCls = clazz;
   216         } else if (onLoad != null) {
   217             myCls = onLoad.getClass();
   218         } else {
   219             throw new NullPointerException("loadClass, neither loadFinished was called!");
   220         }
   221         
   222         if (url == null) {
   223             url = myCls.getResource(resource);
   224         }
   225         if (url == null) {
   226             final ProtectionDomain pd = myCls.getProtectionDomain();
   227             if (pd != null && pd.getCodeSource() != null) {
   228                 URL jar = pd.getCodeSource().getLocation();
   229                 try {
   230                     url = new URL(jar, resource);
   231                 } catch (MalformedURLException ex) {
   232                     ex.initCause(mal);
   233                     mal = ex;
   234                 }
   235             }
   236         }
   237         if (url == null) {
   238             URL res = BrowserBuilder.class.getResource("html4j.txt");
   239             LOG.log(Level.FINE, "Found html4j {0}", res);
   240             if (res != null) try {
   241                 URLConnection c = res.openConnection();
   242                 LOG.log(Level.FINE, "testing : {0}", c);
   243                 if (c instanceof JarURLConnection) {
   244                     JarURLConnection jc = (JarURLConnection)c;
   245                     URL base = jc.getJarFileURL();
   246                     for (int i = 0; i < 50; i++) {
   247                         URL u = new URL(base, resource);
   248                         try {
   249                             InputStream is = u.openStream();
   250                             is.close();
   251                             url = u;
   252                             LOG.log(Level.FINE, "found real url: {0}", url);
   253                             break;
   254                         } catch (FileNotFoundException ignore) {
   255                             LOG.log(Level.FINE, "Cannot open " + u, ignore);
   256                         }
   257                         base = new URL(base, "..");
   258                     }
   259                 }
   260             } catch (IOException ex) {
   261                 mal = ex;
   262             }
   263         }
   264         if (url == null) {
   265             IllegalStateException ise = new IllegalStateException("Can't find resouce: " + resource + " relative to " + myCls);
   266             if (mal != null) {
   267                 ise.initCause(mal);
   268             }
   269             throw ise;
   270         }
   271         
   272         Fn.Presenter dfnr = null;
   273         for (Object o : context) {
   274             if (o instanceof Fn.Presenter) {
   275                 dfnr = (Fn.Presenter)o;
   276                 break;
   277             }
   278         }
   279 
   280         if (dfnr == null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class)) {
   281             dfnr = o;
   282             break;
   283         }
   284         
   285         if (dfnr == null) {
   286             throw new IllegalStateException("Can't find any Fn.Presenter");
   287         }
   288         
   289         final ClassLoader loader;
   290         if (FnUtils.isJavaScriptCapable(myCls.getClassLoader())) {
   291             loader = myCls.getClassLoader();
   292         } else {
   293             FImpl impl = new FImpl(myCls.getClassLoader());
   294             loader = FnUtils.newLoader(impl, dfnr, myCls.getClassLoader().getParent());
   295         }
   296 
   297         final Fn.Presenter currentP = dfnr;
   298         class OnPageLoad implements Runnable {
   299             Class<?> newClazz;
   300             @Override
   301             public void run() {
   302                 try {
   303                     if (newClazz == null) {
   304                         Thread.currentThread().setContextClassLoader(loader);
   305                         newClazz = Class.forName(myCls.getName(), true, loader);
   306                         if (browserClass != null) {
   307                             browserClass[0] = newClazz;
   308                         }
   309                         Contexts.Builder cb = Contexts.newBuilder();
   310                         if (!Contexts.fillInByProviders(newClazz, cb)) {
   311                             LOG.log(Level.WARNING, "Using empty technology for {0}", newClazz);
   312                         }
   313                         if (currentP instanceof Executor) {
   314                             cb.register(Executor.class, (Executor)currentP, 1000);
   315                         }
   316                         cb.register(Fn.Presenter.class, currentP, 1000);
   317                         BrwsrCtx c = cb.build();
   318                         c.execute(this);
   319                         return;
   320                     }
   321                     
   322                     Throwable firstError = null;
   323                     if (onLoad != null) {
   324                         try {
   325                             FnContext.currentPresenter(currentP);
   326                             onLoad.run();
   327                         } catch (Throwable ex) {
   328                             firstError = ex;
   329                         } finally {
   330                             FnContext.currentPresenter(null);
   331                         }
   332                     }
   333                     INIT: if (methodName != null) {
   334                         if (methodArgs.length == 0) {
   335                             try {
   336                                 Method m = newClazz.getMethod(methodName);
   337                                 FnContext.currentPresenter(currentP);
   338                                 m.invoke(null);
   339                                 firstError = null;
   340                                 break INIT;
   341                             } catch (Throwable ex) {
   342                                 firstError = ex;
   343                             } finally {
   344                                 FnContext.currentPresenter(null);
   345                             }
   346                         }
   347                         try {
   348                             Method m = newClazz.getMethod(methodName, String[].class);
   349                             FnContext.currentPresenter(currentP);
   350                             m.invoke(m, (Object) methodArgs);
   351                             firstError = null;
   352                         } catch (Throwable ex) {
   353                             LOG.log(Level.SEVERE, "Can't call " + methodName + " with args " + Arrays.toString(methodArgs), ex);
   354                         } finally {
   355                             FnContext.currentPresenter(null);
   356                         }
   357                     }
   358                     if (firstError != null) {
   359                         LOG.log(Level.SEVERE, "Can't initialize the view", firstError);
   360                     }
   361                 } catch (ClassNotFoundException ex) {
   362                     LOG.log(Level.SEVERE, "Can't load " + myCls.getName(), ex);
   363                 }
   364             }
   365         }
   366         dfnr.displayPage(url, new OnPageLoad());
   367         return;
   368     }
   369 
   370     private static final class FImpl implements FindResources {
   371         final ClassLoader l;
   372 
   373         public FImpl(ClassLoader l) {
   374             this.l = l;
   375         }
   376 
   377         @Override
   378         public void findResources(String path, Collection<? super URL> results, boolean oneIsEnough) {
   379             if (oneIsEnough) {
   380                 URL u = l.getResource(path);
   381                 if (u != null) {
   382                     results.add(u);
   383                 }
   384             } else {
   385                 try {
   386                     Enumeration<URL> en = l.getResources(path);
   387                     while (en.hasMoreElements()) {
   388                         results.add(en.nextElement());
   389                     }
   390                 } catch (IOException ex) {
   391                     // no results
   392                 }
   393             }
   394         }
   395         
   396     }
   397 }