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