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
jaroslav@123
     1
/**
jaroslav@358
     2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
jaroslav@123
     3
 *
jaroslav@551
     4
 * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
jaroslav@123
     5
 *
jaroslav@358
     6
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
jaroslav@358
     7
 * Other names may be trademarks of their respective owners.
jaroslav@123
     8
 *
jaroslav@358
     9
 * The contents of this file are subject to the terms of either the GNU
jaroslav@358
    10
 * General Public License Version 2 only ("GPL") or the Common
jaroslav@358
    11
 * Development and Distribution License("CDDL") (collectively, the
jaroslav@358
    12
 * "License"). You may not use this file except in compliance with the
jaroslav@358
    13
 * License. You can obtain a copy of the License at
jaroslav@358
    14
 * http://www.netbeans.org/cddl-gplv2.html
jaroslav@358
    15
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
jaroslav@358
    16
 * specific language governing permissions and limitations under the
jaroslav@358
    17
 * License.  When distributing the software, include this License Header
jaroslav@358
    18
 * Notice in each file and include the License file at
jaroslav@358
    19
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
jaroslav@358
    20
 * particular file as subject to the "Classpath" exception as provided
jaroslav@358
    21
 * by Oracle in the GPL Version 2 section of the License file that
jaroslav@358
    22
 * accompanied this code. If applicable, add the following below the
jaroslav@358
    23
 * License Header, with the fields enclosed by brackets [] replaced by
jaroslav@358
    24
 * your own identifying information:
jaroslav@358
    25
 * "Portions Copyrighted [year] [name of copyright owner]"
jaroslav@358
    26
 *
jaroslav@358
    27
 * Contributor(s):
jaroslav@358
    28
 *
jaroslav@358
    29
 * The Original Software is NetBeans. The Initial Developer of the Original
jaroslav@551
    30
 * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
jaroslav@358
    31
 *
jaroslav@358
    32
 * If you wish your version of this file to be governed by only the CDDL
jaroslav@358
    33
 * or only the GPL Version 2, indicate your decision by adding
jaroslav@358
    34
 * "[Contributor] elects to include this software in this distribution
jaroslav@358
    35
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
jaroslav@358
    36
 * single choice of license, a recipient has the option to distribute
jaroslav@358
    37
 * your version of this file under either the CDDL, the GPL Version 2 or
jaroslav@358
    38
 * to extend the choice of license to its licensees as provided above.
jaroslav@358
    39
 * However, if you add GPL Version 2 code and therefore, elected the GPL
jaroslav@358
    40
 * Version 2 license, then the option applies only if the new code is
jaroslav@358
    41
 * made subject to such option by the copyright holder.
jaroslav@123
    42
 */
jaroslav@123
    43
package net.java.html.boot;
jaroslav@123
    44
jaroslav@202
    45
import java.io.File;
jtulach@653
    46
import java.io.FileNotFoundException;
jaroslav@127
    47
import java.io.IOException;
jtulach@653
    48
import java.io.InputStream;
jaroslav@156
    49
import java.lang.reflect.Method;
jtulach@653
    50
import java.net.JarURLConnection;
jaroslav@148
    51
import java.net.MalformedURLException;
jaroslav@127
    52
import java.net.URL;
jtulach@653
    53
import java.net.URLConnection;
jtulach@653
    54
import java.security.ProtectionDomain;
jaroslav@156
    55
import java.util.Arrays;
jaroslav@127
    56
import java.util.Collection;
jaroslav@127
    57
import java.util.Enumeration;
jaroslav@127
    58
import java.util.ServiceLoader;
jaroslav@574
    59
import java.util.concurrent.Executor;
jaroslav@156
    60
import java.util.logging.Level;
jaroslav@156
    61
import java.util.logging.Logger;
jtulach@569
    62
import net.java.html.BrwsrCtx;
jaroslav@166
    63
import net.java.html.js.JavaScriptBody;
jaroslav@127
    64
import org.apidesign.html.boot.spi.Fn;
jaroslav@574
    65
import org.apidesign.html.context.spi.Contexts;
jaroslav@362
    66
import org.netbeans.html.boot.impl.FindResources;
jaroslav@362
    67
import org.netbeans.html.boot.impl.FnContext;
jaroslav@509
    68
import org.netbeans.html.boot.impl.FnUtils;
jaroslav@127
    69
jaroslav@166
    70
/** Use this builder to launch your Java/HTML based application. Typical
jaroslav@166
    71
 * usage in a main method of your application looks like this: 
jaroslav@166
    72
 * <pre>
jaroslav@166
    73
 * 
jaroslav@166
    74
 * <b>public static void</b> <em>main</em>(String... args) {
jtulach@700
    75
 *     BrowserBuilder.{@link #newBrowser newBrowser()}.
jaroslav@166
    76
 *          {@link #loadClass(java.lang.Class) loadClass(YourMain.class)}.
jaroslav@166
    77
 *          {@link #loadPage(java.lang.String) loadPage("index.html")}.
jaroslav@166
    78
 *          {@link #invoke(java.lang.String, java.lang.String[]) invoke("initialized", args)}.
jaroslav@166
    79
 *          {@link #showAndWait()};
jaroslav@166
    80
 *     System.exit(0);
jaroslav@166
    81
 * }
jaroslav@166
    82
 * </pre>
jaroslav@166
    83
 * The above will load <code>YourMain</code> class via
jaroslav@166
    84
 * a special classloader, it will locate an <code>index.html</code> (relative
jaroslav@166
    85
 * to <code>YourMain</code> class) and show it in a browser window. When the
jaroslav@166
    86
 * initialization is over, a <b>public static</b> method <em>initialized</em>
jaroslav@166
    87
 * in <code>YourMain</code> will be called with provided string parameters.
jaroslav@166
    88
 * <p>
jaroslav@166
    89
 * This module provides only API for building browsers. To use it properly one
jaroslav@166
    90
 * also needs an implementation on the classpath of one's application. For example
jaroslav@166
    91
 * use: <pre>
jaroslav@166
    92
 * &lt;dependency&gt;
jaroslav@361
    93
 *   &lt;groupId&gt;org.netbeans.html&lt;/groupId&gt;
jaroslav@361
    94
 *   &lt;artifactId&gt;net.java.html.boot.fx&lt;/artifactId&gt;
jaroslav@166
    95
 *   &lt;scope&gt;runtime&lt;/scope&gt;
jaroslav@166
    96
 * &lt;/dependency&gt;
jaroslav@166
    97
 * </pre>
jaroslav@123
    98
 *
jaroslav@123
    99
 * @author Jaroslav Tulach <jtulach@netbeans.org>
jaroslav@123
   100
 */
jaroslav@123
   101
public final class BrowserBuilder {
jaroslav@156
   102
    private static final Logger LOG = Logger.getLogger(BrowserBuilder.class.getName());
jaroslav@156
   103
    
jaroslav@127
   104
    private String resource;
jaroslav@127
   105
    private Class<?> clazz;
jaroslav@146
   106
    private Class[] browserClass;
jaroslav@146
   107
    private Runnable onLoad;
jaroslav@156
   108
    private String methodName;
jaroslav@156
   109
    private String[] methodArgs;
jaroslav@288
   110
    private final Object[] context;
jaroslav@156
   111
    
jaroslav@288
   112
    private BrowserBuilder(Object[] context) {
jaroslav@288
   113
        this.context = context;
jaroslav@123
   114
    }
jaroslav@166
   115
jaroslav@166
   116
    /** Entry method to obtain a new browser builder. Follow by calling 
jaroslav@166
   117
     * its instance methods like {@link #loadClass(java.lang.Class)} and
jaroslav@166
   118
     * {@link #loadPage(java.lang.String)}.
jaroslav@166
   119
     * 
jaroslav@288
   120
     * @param context any instances that should be available to the builder -
jaroslav@458
   121
     *   implementation dependant
jaroslav@166
   122
     * @return new browser builder
jaroslav@166
   123
     */
jaroslav@288
   124
    public static BrowserBuilder newBrowser(Object... context) {
jaroslav@288
   125
        return new BrowserBuilder(context);
jaroslav@123
   126
    }
jaroslav@123
   127
    
jaroslav@166
   128
    /** The class to load when the browser is initialized. This class
jaroslav@166
   129
     * is loaded by a special classloader (that supports {@link JavaScriptBody}
jaroslav@166
   130
     * and co.). 
jaroslav@166
   131
     * 
jaroslav@166
   132
     * @param mainClass the class to load and resolve when the browser is ready
jaroslav@166
   133
     * @return this builder
jaroslav@166
   134
     */
jaroslav@127
   135
    public BrowserBuilder loadClass(Class<?> mainClass) {
jaroslav@127
   136
        this.clazz = mainClass;
jaroslav@123
   137
        return this;
jaroslav@123
   138
    }
jtulach@656
   139
    
jtulach@656
   140
    /** Allows one to specify a runnable that should be invoked when a load
jtulach@656
   141
     * of a page is finished. This method may be used in addition or instead
jtulach@656
   142
     * of {@link #loadClass(java.lang.Class)} and 
jtulach@656
   143
     * {@link #invoke(java.lang.String, java.lang.String...)} methods.
jtulach@656
   144
     * 
jtulach@656
   145
     * @param r the code to run when the page is loaded
jtulach@656
   146
     * @return this builder
jtulach@656
   147
     * @since 0.8.1
jtulach@656
   148
     */
jtulach@656
   149
    public BrowserBuilder loadFinished(Runnable r) {
jtulach@656
   150
        this.onLoad = r;
jtulach@656
   151
        return this;
jtulach@656
   152
    }
jaroslav@156
   153
jaroslav@166
   154
    /** Page to load into the browser. If the <code>page</code> represents
jaroslav@166
   155
     * a {@link URL} known to the Java system, the URL is passed to the browser. 
jaroslav@202
   156
     * If system property <code>browser.rootdir</code> is specified, then a
jaroslav@202
   157
     * file <code>page</code> relative to this directory is used as the URL.
jaroslav@202
   158
     * If no such file exists, the system seeks for the 
jaroslav@202
   159
     * resource via {@link Class#getResource(java.lang.String)}
jaroslav@202
   160
     * method (relative to the {@link #loadClass(java.lang.Class) specified class}). 
jaroslav@202
   161
     * If such resource is not found, a file relative to the location JAR
jaroslav@202
   162
     * that contains the {@link #loadClass(java.lang.Class) main class} is 
jaroslav@202
   163
     * searched for.
jaroslav@166
   164
     * 
jaroslav@166
   165
     * @param page the location (relative, absolute, or URL) of a page to load
jaroslav@166
   166
     * @return this browser
jaroslav@166
   167
     */
jaroslav@166
   168
    public BrowserBuilder loadPage(String page) {
jaroslav@166
   169
        this.resource = page;
jaroslav@166
   170
        return this;
jaroslav@166
   171
    }
jaroslav@166
   172
    
jaroslav@166
   173
    /** Specifies callback method to notify the application that the browser is ready.
jaroslav@166
   174
     * There should be a <b>public static</b> method in the class specified
jaroslav@166
   175
     * by {@link #loadClass(java.lang.Class)} which takes an array of {@link String}
jaroslav@166
   176
     * argument. The method is called on the browser dispatch thread one
jaroslav@166
   177
     * the browser finishes loading of the {@link #loadPage(java.lang.String) HTML page}.
jaroslav@166
   178
     * 
jaroslav@166
   179
     * @param methodName name of a method to seek for
jaroslav@166
   180
     * @param args parameters to pass to the method
jaroslav@166
   181
     * @return this builder
jaroslav@166
   182
     */
jaroslav@156
   183
    public BrowserBuilder invoke(String methodName, String... args) {
jaroslav@156
   184
        this.methodName = methodName;
jaroslav@156
   185
        this.methodArgs = args;
jaroslav@156
   186
        return this;
jaroslav@156
   187
    }
jaroslav@166
   188
jaroslav@166
   189
    /** Shows the browser, loads specified page in and executes the 
jaroslav@166
   190
     * {@link #invoke(java.lang.String, java.lang.String[]) initialization method}.
jaroslav@166
   191
     * The method returns when the browser is closed.
jaroslav@166
   192
     * 
jaroslav@166
   193
     * @throws NullPointerException if some of essential parameters (like {@link #loadPage(java.lang.String) page} or
jaroslav@166
   194
     *    {@link #loadClass(java.lang.Class) class} have not been specified
jaroslav@166
   195
     */
jaroslav@123
   196
    public void showAndWait() {
jaroslav@146
   197
        if (resource == null) {
jaroslav@166
   198
            throw new NullPointerException("Need to specify resource via loadPage method");
jaroslav@146
   199
        }
jaroslav@146
   200
        
jaroslav@148
   201
        URL url = null;
jtulach@653
   202
        IOException mal = null;
jaroslav@148
   203
        try {
jaroslav@202
   204
            String baseURL = System.getProperty("browser.rootdir");
jaroslav@202
   205
            if (baseURL != null) {
jaroslav@202
   206
                url = new File(baseURL, resource).toURI().toURL();
jaroslav@202
   207
            } else {
jaroslav@202
   208
                url = new URL(resource);
jaroslav@202
   209
            }
jaroslav@148
   210
        } catch (MalformedURLException ex) {
jaroslav@148
   211
            mal = ex;
jaroslav@148
   212
        }
jtulach@656
   213
        final Class<?> myCls;
jtulach@656
   214
        if (clazz != null) {
jtulach@656
   215
            myCls = clazz;
jtulach@656
   216
        } else if (onLoad != null) {
jtulach@656
   217
            myCls = onLoad.getClass();
jtulach@656
   218
        } else {
jtulach@656
   219
            throw new NullPointerException("loadClass, neither loadFinished was called!");
jtulach@656
   220
        }
jtulach@656
   221
        
jaroslav@128
   222
        if (url == null) {
jtulach@656
   223
            url = myCls.getResource(resource);
jaroslav@148
   224
        }
jaroslav@148
   225
        if (url == null) {
jtulach@656
   226
            final ProtectionDomain pd = myCls.getProtectionDomain();
jtulach@653
   227
            if (pd != null && pd.getCodeSource() != null) {
jtulach@653
   228
                URL jar = pd.getCodeSource().getLocation();
jtulach@653
   229
                try {
jtulach@653
   230
                    url = new URL(jar, resource);
jtulach@653
   231
                } catch (MalformedURLException ex) {
jtulach@653
   232
                    ex.initCause(mal);
jtulach@653
   233
                    mal = ex;
jtulach@653
   234
                }
jtulach@653
   235
            }
jtulach@653
   236
        }
jtulach@653
   237
        if (url == null) {
jtulach@653
   238
            URL res = BrowserBuilder.class.getResource("html4j.txt");
jtulach@653
   239
            LOG.log(Level.FINE, "Found html4j {0}", res);
jtulach@653
   240
            if (res != null) try {
jtulach@653
   241
                URLConnection c = res.openConnection();
jtulach@653
   242
                LOG.log(Level.FINE, "testing : {0}", c);
jtulach@653
   243
                if (c instanceof JarURLConnection) {
jtulach@653
   244
                    JarURLConnection jc = (JarURLConnection)c;
jtulach@653
   245
                    URL base = jc.getJarFileURL();
jtulach@653
   246
                    for (int i = 0; i < 50; i++) {
jtulach@653
   247
                        URL u = new URL(base, resource);
jtulach@653
   248
                        try {
jtulach@653
   249
                            InputStream is = u.openStream();
jtulach@653
   250
                            is.close();
jtulach@653
   251
                            url = u;
jtulach@653
   252
                            LOG.log(Level.FINE, "found real url: {0}", url);
jtulach@653
   253
                            break;
jtulach@653
   254
                        } catch (FileNotFoundException ignore) {
jtulach@653
   255
                            LOG.log(Level.FINE, "Cannot open " + u, ignore);
jtulach@653
   256
                        }
jtulach@653
   257
                        base = new URL(base, "..");
jtulach@653
   258
                    }
jtulach@653
   259
                }
jtulach@653
   260
            } catch (IOException ex) {
jaroslav@202
   261
                mal = ex;
jaroslav@202
   262
            }
jaroslav@202
   263
        }
jaroslav@202
   264
        if (url == null) {
jtulach@656
   265
            IllegalStateException ise = new IllegalStateException("Can't find resouce: " + resource + " relative to " + myCls);
jaroslav@148
   266
            if (mal != null) {
jaroslav@148
   267
                ise.initCause(mal);
jaroslav@148
   268
            }
jaroslav@148
   269
            throw ise;
jaroslav@128
   270
        }
jaroslav@288
   271
        
jaroslav@288
   272
        Fn.Presenter dfnr = null;
jaroslav@288
   273
        for (Object o : context) {
jaroslav@288
   274
            if (o instanceof Fn.Presenter) {
jaroslav@288
   275
                dfnr = (Fn.Presenter)o;
jaroslav@288
   276
                break;
jaroslav@288
   277
            }
jaroslav@288
   278
        }
jaroslav@127
   279
jaroslav@288
   280
        if (dfnr == null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class)) {
jaroslav@288
   281
            dfnr = o;
jaroslav@288
   282
            break;
jaroslav@288
   283
        }
jaroslav@288
   284
        
jaroslav@288
   285
        if (dfnr == null) {
jaroslav@288
   286
            throw new IllegalStateException("Can't find any Fn.Presenter");
jaroslav@288
   287
        }
jaroslav@288
   288
        
jaroslav@288
   289
        final ClassLoader loader;
jtulach@656
   290
        if (FnUtils.isJavaScriptCapable(myCls.getClassLoader())) {
jtulach@656
   291
            loader = myCls.getClassLoader();
jaroslav@509
   292
        } else {
jtulach@656
   293
            FImpl impl = new FImpl(myCls.getClassLoader());
jtulach@656
   294
            loader = FnUtils.newLoader(impl, dfnr, myCls.getClassLoader().getParent());
jaroslav@509
   295
        }
jaroslav@128
   296
jaroslav@288
   297
        final Fn.Presenter currentP = dfnr;
jaroslav@288
   298
        class OnPageLoad implements Runnable {
jtulach@569
   299
            Class<?> newClazz;
jaroslav@288
   300
            @Override
jaroslav@288
   301
            public void run() {
jaroslav@288
   302
                try {
jtulach@569
   303
                    if (newClazz == null) {
jtulach@569
   304
                        Thread.currentThread().setContextClassLoader(loader);
jtulach@656
   305
                        newClazz = Class.forName(myCls.getName(), true, loader);
jtulach@569
   306
                        if (browserClass != null) {
jtulach@569
   307
                            browserClass[0] = newClazz;
jtulach@569
   308
                        }
jaroslav@574
   309
                        Contexts.Builder cb = Contexts.newBuilder();
jaroslav@574
   310
                        if (!Contexts.fillInByProviders(newClazz, cb)) {
jaroslav@574
   311
                            LOG.log(Level.WARNING, "Using empty technology for {0}", newClazz);
jaroslav@574
   312
                        }
jaroslav@574
   313
                        if (currentP instanceof Executor) {
jaroslav@574
   314
                            cb.register(Executor.class, (Executor)currentP, 1000);
jaroslav@574
   315
                        }
jaroslav@574
   316
                        cb.register(Fn.Presenter.class, currentP, 1000);
jaroslav@574
   317
                        BrwsrCtx c = cb.build();
jtulach@569
   318
                        c.execute(this);
jtulach@569
   319
                        return;
jaroslav@288
   320
                    }
jtulach@569
   321
                    
jtulach@656
   322
                    Throwable firstError = null;
jaroslav@288
   323
                    if (onLoad != null) {
jtulach@656
   324
                        try {
jtulach@656
   325
                            FnContext.currentPresenter(currentP);
jtulach@656
   326
                            onLoad.run();
jtulach@656
   327
                        } catch (Throwable ex) {
jtulach@656
   328
                            firstError = ex;
jtulach@656
   329
                        } finally {
jtulach@656
   330
                            FnContext.currentPresenter(null);
jtulach@656
   331
                        }
jaroslav@288
   332
                    }
jaroslav@288
   333
                    INIT: if (methodName != null) {
jaroslav@288
   334
                        if (methodArgs.length == 0) {
jaroslav@156
   335
                            try {
jaroslav@288
   336
                                Method m = newClazz.getMethod(methodName);
jaroslav@309
   337
                                FnContext.currentPresenter(currentP);
jaroslav@288
   338
                                m.invoke(null);
jtulach@662
   339
                                firstError = null;
jaroslav@288
   340
                                break INIT;
jaroslav@277
   341
                            } catch (Throwable ex) {
jaroslav@288
   342
                                firstError = ex;
jaroslav@288
   343
                            } finally {
jaroslav@309
   344
                                FnContext.currentPresenter(null);
jaroslav@156
   345
                            }
jaroslav@156
   346
                        }
jaroslav@288
   347
                        try {
jaroslav@288
   348
                            Method m = newClazz.getMethod(methodName, String[].class);
jaroslav@309
   349
                            FnContext.currentPresenter(currentP);
jaroslav@288
   350
                            m.invoke(m, (Object) methodArgs);
jtulach@662
   351
                            firstError = null;
jaroslav@288
   352
                        } catch (Throwable ex) {
jaroslav@288
   353
                            LOG.log(Level.SEVERE, "Can't call " + methodName + " with args " + Arrays.toString(methodArgs), ex);
jaroslav@288
   354
                        } finally {
jaroslav@309
   355
                            FnContext.currentPresenter(null);
jaroslav@288
   356
                        }
jaroslav@128
   357
                    }
jtulach@656
   358
                    if (firstError != null) {
jtulach@656
   359
                        LOG.log(Level.SEVERE, "Can't initialize the view", firstError);
jtulach@656
   360
                    }
jaroslav@288
   361
                } catch (ClassNotFoundException ex) {
jtulach@656
   362
                    LOG.log(Level.SEVERE, "Can't load " + myCls.getName(), ex);
jaroslav@128
   363
                }
jaroslav@127
   364
            }
jaroslav@127
   365
        }
jaroslav@288
   366
        dfnr.displayPage(url, new OnPageLoad());
jaroslav@288
   367
        return;
jaroslav@127
   368
    }
jaroslav@146
   369
jaroslav@128
   370
    private static final class FImpl implements FindResources {
jaroslav@128
   371
        final ClassLoader l;
jaroslav@127
   372
jaroslav@127
   373
        public FImpl(ClassLoader l) {
jaroslav@127
   374
            this.l = l;
jaroslav@127
   375
        }
jaroslav@127
   376
jaroslav@127
   377
        @Override
jaroslav@127
   378
        public void findResources(String path, Collection<? super URL> results, boolean oneIsEnough) {
jaroslav@127
   379
            if (oneIsEnough) {
jaroslav@127
   380
                URL u = l.getResource(path);
jaroslav@127
   381
                if (u != null) {
jaroslav@127
   382
                    results.add(u);
jaroslav@127
   383
                }
jaroslav@127
   384
            } else {
jaroslav@127
   385
                try {
jaroslav@127
   386
                    Enumeration<URL> en = l.getResources(path);
jaroslav@127
   387
                    while (en.hasMoreElements()) {
jaroslav@127
   388
                        results.add(en.nextElement());
jaroslav@127
   389
                    }
jaroslav@127
   390
                } catch (IOException ex) {
jaroslav@127
   391
                    // no results
jaroslav@127
   392
                }
jaroslav@127
   393
            }
jaroslav@127
   394
        }
jaroslav@127
   395
        
jaroslav@123
   396
    }
jaroslav@123
   397
}