boot/src/main/java/net/java/html/boot/BrowserBuilder.java
author Jaroslav Tulach <jtulach@netbeans.org>
Sat, 02 Aug 2014 12:59:31 +0200
changeset 790 30f20d9c0986
parent 771 ee3614350fc8
child 834 5b6567dcc966
permissions -rw-r--r--
Fixing Javadoc to succeed on JDK8
     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
   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     private ClassLoader loader;
   112     
   113     private BrowserBuilder(Object[] context) {
   114         this.context = context;
   115     }
   116 
   117     /** Entry method to obtain a new browser builder. Follow by calling 
   118      * its instance methods like {@link #loadClass(java.lang.Class)} and
   119      * {@link #loadPage(java.lang.String)}.
   120      * 
   121      * @param context any instances that should be available to the builder -
   122      *   implementation dependant
   123      * @return new browser builder
   124      */
   125     public static BrowserBuilder newBrowser(Object... context) {
   126         return new BrowserBuilder(context);
   127     }
   128     
   129     /** The class to load when the browser is initialized. This class
   130      * is loaded by a special classloader (that supports {@link JavaScriptBody}
   131      * and co.). 
   132      * 
   133      * @param mainClass the class to load and resolve when the browser is ready
   134      * @return this builder
   135      */
   136     public BrowserBuilder loadClass(Class<?> mainClass) {
   137         this.clazz = mainClass;
   138         return this;
   139     }
   140     
   141     /** Allows one to specify a runnable that should be invoked when a load
   142      * of a page is finished. This method may be used in addition or instead
   143      * of {@link #loadClass(java.lang.Class)} and 
   144      * {@link #invoke(java.lang.String, java.lang.String...)} methods.
   145      * 
   146      * @param r the code to run when the page is loaded
   147      * @return this builder
   148      * @since 0.8.1
   149      */
   150     public BrowserBuilder loadFinished(Runnable r) {
   151         this.onLoad = r;
   152         return this;
   153     }
   154 
   155     /** Page to load into the browser. If the <code>page</code> represents
   156      * a {@link URL} known to the Java system, the URL is passed to the browser. 
   157      * If system property <code>browser.rootdir</code> is specified, then a
   158      * file <code>page</code> relative to this directory is used as the URL.
   159      * If no such file exists, the system seeks for the 
   160      * resource via {@link Class#getResource(java.lang.String)}
   161      * method (relative to the {@link #loadClass(java.lang.Class) specified class}). 
   162      * If such resource is not found, a file relative to the location JAR
   163      * that contains the {@link #loadClass(java.lang.Class) main class} is 
   164      * searched for.
   165      * 
   166      * @param page the location (relative, absolute, or URL) of a page to load
   167      * @return this browser
   168      */
   169     public BrowserBuilder loadPage(String page) {
   170         this.resource = page;
   171         return this;
   172     }
   173     
   174     /** Specifies callback method to notify the application that the browser is ready.
   175      * There should be a <b>public static</b> method in the class specified
   176      * by {@link #loadClass(java.lang.Class)} which takes an array of {@link String}
   177      * argument. The method is called on the browser dispatch thread one
   178      * the browser finishes loading of the {@link #loadPage(java.lang.String) HTML page}.
   179      * 
   180      * @param methodName name of a method to seek for
   181      * @param args parameters to pass to the method
   182      * @return this builder
   183      */
   184     public BrowserBuilder invoke(String methodName, String... args) {
   185         this.methodName = methodName;
   186         this.methodArgs = args;
   187         return this;
   188     }
   189 
   190     /** Loader to use when searching for classes to initialize. 
   191      * If specified, this loader is going to be used to load {@link Fn.Presenter}
   192      * and {@link Contexts#fillInByProviders(java.lang.Class, org.apidesign.html.context.spi.Contexts.Builder) fill} {@link BrwsrCtx} in.
   193      * Specifying special classloader may be useful in modular systems, 
   194      * like OSGi, where one needs to load classes from many otherwise independent
   195      * modules.
   196      * 
   197      * @param l the loader to use (or <code>null</code>)
   198      * @return this builder
   199      * @since 0.9
   200      */
   201     public BrowserBuilder classloader(ClassLoader l) {
   202         this.loader = l;
   203         return this;
   204     }
   205 
   206     /** Shows the browser, loads specified page in and executes the 
   207      * {@link #invoke(java.lang.String, java.lang.String[]) initialization method}.
   208      * The method returns when the browser is closed.
   209      * 
   210      * @throws NullPointerException if some of essential parameters (like {@link #loadPage(java.lang.String) page} or
   211      *    {@link #loadClass(java.lang.Class) class} have not been specified
   212      */
   213     public void showAndWait() {
   214         if (resource == null) {
   215             throw new NullPointerException("Need to specify resource via loadPage method");
   216         }
   217         
   218         URL url = null;
   219         IOException mal = null;
   220         try {
   221             String baseURL = System.getProperty("browser.rootdir");
   222             if (baseURL != null) {
   223                 url = new File(baseURL, resource).toURI().toURL();
   224             } else {
   225                 url = new URL(resource);
   226             }
   227         } catch (MalformedURLException ex) {
   228             mal = ex;
   229         }
   230         final Class<?> myCls;
   231         if (clazz != null) {
   232             myCls = clazz;
   233         } else if (onLoad != null) {
   234             myCls = onLoad.getClass();
   235         } else {
   236             throw new NullPointerException("loadClass, neither loadFinished was called!");
   237         }
   238         
   239         if (url == null) {
   240             url = myCls.getResource(resource);
   241         }
   242         if (url == null) {
   243             final ProtectionDomain pd = myCls.getProtectionDomain();
   244             if (pd != null && pd.getCodeSource() != null) {
   245                 URL jar = pd.getCodeSource().getLocation();
   246                 try {
   247                     url = new URL(jar, resource);
   248                 } catch (MalformedURLException ex) {
   249                     ex.initCause(mal);
   250                     mal = ex;
   251                 }
   252             }
   253         }
   254         if (url == null) {
   255             URL res = BrowserBuilder.class.getResource("html4j.txt");
   256             LOG.log(Level.FINE, "Found html4j {0}", res);
   257             if (res != null) try {
   258                 URLConnection c = res.openConnection();
   259                 LOG.log(Level.FINE, "testing : {0}", c);
   260                 if (c instanceof JarURLConnection) {
   261                     JarURLConnection jc = (JarURLConnection)c;
   262                     URL base = jc.getJarFileURL();
   263                     for (int i = 0; i < 50; i++) {
   264                         URL u = new URL(base, resource);
   265                         try {
   266                             InputStream is = u.openStream();
   267                             is.close();
   268                             url = u;
   269                             LOG.log(Level.FINE, "found real url: {0}", url);
   270                             break;
   271                         } catch (FileNotFoundException ignore) {
   272                             LOG.log(Level.FINE, "Cannot open " + u, ignore);
   273                         }
   274                         base = new URL(base, "..");
   275                     }
   276                 }
   277             } catch (IOException ex) {
   278                 mal = ex;
   279             }
   280         }
   281         if (url == null) {
   282             IllegalStateException ise = new IllegalStateException("Can't find resouce: " + resource + " relative to " + myCls);
   283             if (mal != null) {
   284                 ise.initCause(mal);
   285             }
   286             throw ise;
   287         }
   288         
   289         Fn.Presenter dfnr = null;
   290         for (Object o : context) {
   291             if (o instanceof Fn.Presenter) {
   292                 dfnr = (Fn.Presenter)o;
   293                 break;
   294             }
   295         }
   296 
   297         if (dfnr == null && loader != null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class, loader)) {
   298             dfnr = o;
   299             break;
   300         }
   301         
   302         if (dfnr == null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class)) {
   303             dfnr = o;
   304             break;
   305         }
   306         
   307         if (dfnr == null) {
   308             throw new IllegalStateException("Can't find any Fn.Presenter");
   309         }
   310         
   311         final ClassLoader activeLoader;
   312         if (loader != null) {
   313             if (!FnUtils.isJavaScriptCapable(loader)) {
   314                 throw new IllegalStateException("Loader cannot resolve @JavaScriptBody: " + loader);
   315             }
   316             activeLoader = loader;
   317         } else if (FnUtils.isJavaScriptCapable(myCls.getClassLoader())) {
   318             activeLoader = myCls.getClassLoader();
   319         } else {
   320             FImpl impl = new FImpl(myCls.getClassLoader());
   321             activeLoader = FnUtils.newLoader(impl, dfnr, myCls.getClassLoader().getParent());
   322         }
   323         
   324         final Fn.Presenter dP = dfnr;
   325 
   326         class OnPageLoad implements Runnable {
   327             @Override
   328             public void run() {
   329                 try {
   330                     final Fn.Presenter aP = Fn.activePresenter();
   331                     final Fn.Presenter currentP = aP != null ? aP : dP;
   332                     
   333                     Thread.currentThread().setContextClassLoader(activeLoader);
   334                     final Class<?> newClazz = Class.forName(myCls.getName(), true, activeLoader);
   335                     if (browserClass != null) {
   336                         browserClass[0] = newClazz;
   337                     }
   338                     Contexts.Builder cb = Contexts.newBuilder();
   339                     if (!Contexts.fillInByProviders(newClazz, cb)) {
   340                         LOG.log(Level.WARNING, "Using empty technology for {0}", newClazz);
   341                     }
   342                     if (currentP instanceof Executor) {
   343                         cb.register(Executor.class, (Executor)currentP, 1000);
   344                     }
   345                     cb.register(Fn.Presenter.class, currentP, 1000);
   346                     BrwsrCtx c = cb.build();
   347 
   348                     class CallInitMethod implements Runnable {
   349                         @Override
   350                         public void run() {
   351                             Throwable firstError = null;
   352                             if (onLoad != null) {
   353                                 try {
   354                                     FnContext.currentPresenter(currentP);
   355                                     onLoad.run();
   356                                 } catch (Throwable ex) {
   357                                     firstError = ex;
   358                                 } finally {
   359                                     FnContext.currentPresenter(null);
   360                                 }
   361                             }
   362                             INIT: if (methodName != null) {
   363                                 if (methodArgs.length == 0) {
   364                                     try {
   365                                         Method m = newClazz.getMethod(methodName);
   366                                         FnContext.currentPresenter(currentP);
   367                                         m.invoke(null);
   368                                         firstError = null;
   369                                         break INIT;
   370                                     } catch (Throwable ex) {
   371                                         firstError = ex;
   372                                     } finally {
   373                                         FnContext.currentPresenter(null);
   374                                     }
   375                                 }
   376                                 try {
   377                                     Method m = newClazz.getMethod(methodName, String[].class);
   378                                     FnContext.currentPresenter(currentP);
   379                                     m.invoke(m, (Object) methodArgs);
   380                                     firstError = null;
   381                                 } catch (Throwable ex) {
   382                                     LOG.log(Level.SEVERE, "Can't call " + methodName + " with args " + Arrays.toString(methodArgs), ex);
   383                                 } finally {
   384                                     FnContext.currentPresenter(null);
   385                                 }
   386                             }
   387                             if (firstError != null) {
   388                                 LOG.log(Level.SEVERE, "Can't initialize the view", firstError);
   389                             }
   390                         }
   391                     }
   392                     
   393                     c.execute(new CallInitMethod());
   394                 } catch (ClassNotFoundException ex) {
   395                     LOG.log(Level.SEVERE, "Can't load " + myCls.getName(), ex);
   396                 }
   397             }
   398         }
   399         dfnr.displayPage(url, new OnPageLoad());
   400     }
   401 
   402     private static final class FImpl implements FindResources {
   403         final ClassLoader l;
   404 
   405         public FImpl(ClassLoader l) {
   406             this.l = l;
   407         }
   408 
   409         @Override
   410         public void findResources(String path, Collection<? super URL> results, boolean oneIsEnough) {
   411             if (oneIsEnough) {
   412                 URL u = l.getResource(path);
   413                 if (u != null) {
   414                     results.add(u);
   415                 }
   416             } else {
   417                 try {
   418                     Enumeration<URL> en = l.getResources(path);
   419                     while (en.hasMoreElements()) {
   420                         results.add(en.nextElement());
   421                     }
   422                 } catch (IOException ex) {
   423                     // no results
   424                 }
   425             }
   426         }
   427         
   428     }
   429 }