2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
6 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7 * Other names may be trademarks of their respective owners.
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]"
29 * The Original Software is NetBeans. The Initial Developer of the Original
30 * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
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.
43 package net.java.html.boot;
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;
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;
73 /** Use this builder to launch your Java/HTML based application. Typical
74 * usage in a main method of your application looks like this:
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()};
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.
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
97 * <groupId>org.netbeans.html</groupId>
98 * <artifactId>net.java.html.boot.fx</artifactId>
99 * <scope>runtime</scope>
100 * </dependency>
103 * @author Jaroslav Tulach
105 public final class BrowserBuilder {
106 private static final Logger LOG = Logger.getLogger(BrowserBuilder.class.getName());
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;
118 private BrowserBuilder(Object[] context) {
119 this.context = context;
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.
135 * @param context any instances that should be available to the builder -
136 * implementation dependant
137 * @return new browser builder
139 public static BrowserBuilder newBrowser(Object... context) {
140 return new BrowserBuilder(context);
143 /** The class to load when the browser is initialized. This class
144 * is loaded by a special classloader (that supports {@link JavaScriptBody}
147 * @param mainClass the class to load and resolve when the browser is ready
148 * @return this builder
150 public BrowserBuilder loadClass(Class<?> mainClass) {
151 this.clazz = mainClass;
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.
160 * @param r the code to run when the page is loaded
161 * @return this builder
164 public BrowserBuilder loadFinished(Runnable r) {
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
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>.
184 * @param page the location (relative, absolute, or URL) of a page to load
185 * @return this builder
187 public BrowserBuilder loadPage(String page) {
188 this.resource = page;
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.
198 * @param locale the locale to use or <code>null</code> if no suffix search should be performed
199 * @return this builder
202 public BrowserBuilder locale(Locale locale) {
203 this.locale = locale;
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}.
213 * @param methodName name of a method to seek for
214 * @param args parameters to pass to the method
215 * @return this builder
217 public BrowserBuilder invoke(String methodName, String... args) {
218 this.methodName = methodName;
219 this.methodArgs = args;
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
230 * @param l the loader to use (or <code>null</code>)
231 * @return this builder
234 public BrowserBuilder classloader(ClassLoader l) {
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.
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
246 public void showAndWait() {
247 if (resource == null) {
248 throw new NullPointerException("Need to specify resource via loadPage method");
251 final Class<?> myCls;
254 } else if (onLoad != null) {
255 myCls = onLoad.getClass();
257 throw new NullPointerException("loadClass, neither loadFinished was called!");
259 IOException mal[] = { null };
260 URL url = findLocalizedResourceURL(resource, locale, mal, myCls);
262 Fn.Presenter dfnr = null;
263 for (Object o : context) {
264 if (o instanceof Fn.Presenter) {
265 dfnr = (Fn.Presenter)o;
270 if (dfnr == null && loader != null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class, loader)) {
275 if (dfnr == null) for (Fn.Presenter o : ServiceLoader.load(Fn.Presenter.class)) {
281 throw new IllegalStateException("Can't find any Fn.Presenter");
284 final ClassLoader activeLoader;
285 if (loader != null) {
286 if (!FnContext.isJavaScriptCapable(loader)) {
287 throw new IllegalStateException("Loader cannot resolve @JavaScriptBody: " + loader);
289 activeLoader = loader;
290 } else if (FnContext.isJavaScriptCapable(myCls.getClassLoader())) {
291 activeLoader = myCls.getClassLoader();
293 if (!FnContext.isAsmPresent()) {
294 throw new IllegalStateException("Cannot find asm-5.0.jar classes!");
296 FImpl impl = new FImpl(myCls.getClassLoader());
297 activeLoader = FnUtils.newLoader(impl, dfnr, myCls.getClassLoader().getParent());
300 final Fn.Presenter dP = dfnr;
302 class OnPageLoad implements Runnable {
306 final Fn.Presenter aP = Fn.activePresenter();
307 final Fn.Presenter currentP = aP != null ? aP : dP;
309 Thread.currentThread().setContextClassLoader(activeLoader);
310 final Class<?> newClazz = Class.forName(myCls.getName(), true, activeLoader);
311 if (browserClass != null) {
312 browserClass[0] = newClazz;
314 Contexts.Builder cb = Contexts.newBuilder(context);
315 if (!Contexts.fillInByProviders(newClazz, cb)) {
316 LOG.log(Level.WARNING, "Using empty technology for {0}", newClazz);
318 if (currentP instanceof Executor) {
319 cb.register(Executor.class, (Executor)currentP, 1000);
321 cb.register(Fn.Presenter.class, currentP, 1000);
322 BrwsrCtx c = cb.build();
324 class CallInitMethod implements Runnable {
327 Throwable firstError = null;
328 if (onLoad != null) {
330 FnContext.currentPresenter(currentP);
332 } catch (Throwable ex) {
335 FnContext.currentPresenter(null);
338 INIT: if (methodName != null) {
339 if (methodArgs.length == 0) {
341 Method m = newClazz.getMethod(methodName);
342 FnContext.currentPresenter(currentP);
346 } catch (Throwable ex) {
349 FnContext.currentPresenter(null);
353 Method m = newClazz.getMethod(methodName, String[].class);
354 FnContext.currentPresenter(currentP);
355 m.invoke(m, (Object) methodArgs);
357 } catch (Throwable ex) {
358 LOG.log(Level.SEVERE, "Can't call " + methodName + " with args " + Arrays.toString(methodArgs), ex);
360 FnContext.currentPresenter(null);
363 if (firstError != null) {
364 LOG.log(Level.SEVERE, "Can't initialize the view", firstError);
369 c.execute(new CallInitMethod());
370 } catch (ClassNotFoundException ex) {
371 LOG.log(Level.SEVERE, "Can't load " + myCls.getName(), ex);
375 dfnr.displayPage(url, new OnPageLoad());
378 private static URL findResourceURL(String resource, String suffix, IOException[] mal, Class<?> relativeTo) {
379 if (suffix != null) {
380 int lastDot = resource.lastIndexOf('.');
382 resource = resource.substring(0, lastDot) + suffix + resource.substring(lastDot);
384 resource = resource + suffix;
390 String baseURL = System.getProperty("browser.rootdir"); // NOI18N
391 if (baseURL != null) {
392 URL u = new File(baseURL, resource).toURI().toURL();
399 URL u = new URL(resource);
400 if (suffix == null || isReal(u)) {
405 } catch (MalformedURLException ex) {
410 url = relativeTo.getResource(resource);
413 final ProtectionDomain pd = relativeTo.getProtectionDomain();
414 if (pd != null && pd.getCodeSource() != null) {
415 URL jar = pd.getCodeSource().getLocation();
417 URL u = new URL(jar, resource);
421 } catch (MalformedURLException ex) {
422 ex.initCause(mal[0]);
428 URL res = BrowserBuilder.class.getResource("html4j.txt");
429 LOG.log(Level.FINE, "Found html4j {0}", res);
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);
443 base = new URL(base, "..");
446 } catch (IOException ex) {
454 static URL findLocalizedResourceURL(String resource, Locale l, IOException[] mal, Class<?> relativeTo) {
457 url = findResourceURL(resource, "_" + l.getLanguage() + "_" + l.getCountry(), mal, relativeTo);
461 url = findResourceURL(resource, "_" + l.getLanguage(), mal, relativeTo);
466 return findResourceURL(resource, null, mal, relativeTo);
469 private static boolean isReal(URL u) {
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());
479 InputStream is = conn.getInputStream();
481 LOG.log(Level.FINE, "found real url: {0}", u);
483 } catch (IOException ignore) {
484 LOG.log(Level.FINE, "Cannot open " + u, ignore);
489 private static final class FImpl implements FindResources {
492 public FImpl(ClassLoader l) {
497 public void findResources(String path, Collection<? super URL> results, boolean oneIsEnough) {
499 URL u = l.getResource(path);
505 Enumeration<URL> en = l.getResources(path);
506 while (en.hasMoreElements()) {
507 results.add(en.nextElement());
509 } catch (IOException ex) {