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