rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/Bck2BrwsrLauncher.java
Rather than piggybacking on first alert call, use the fact that the server and FX Web View are in the same VM and notify the view that bck2brwsr.js is about to be served from the server.
2 * Back 2 Browser Bytecode Translator
3 * Copyright (C) 2012 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 2 of the License.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. Look for COPYING file in the top folder.
16 * If not, see http://opensource.org/licenses/GPL-2.0.
18 package org.apidesign.bck2brwsr.launcher;
20 import java.io.Closeable;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InterruptedIOException;
25 import java.io.OutputStream;
26 import java.io.UnsupportedEncodingException;
27 import java.io.Writer;
29 import java.net.URISyntaxException;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Enumeration;
34 import java.util.LinkedHashSet;
35 import java.util.List;
37 import java.util.concurrent.BlockingQueue;
38 import java.util.concurrent.CountDownLatch;
39 import java.util.concurrent.LinkedBlockingQueue;
40 import java.util.concurrent.TimeUnit;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
43 import org.apidesign.bck2brwsr.launcher.InvocationContext.Resource;
44 import org.apidesign.vm4brwsr.Bck2Brwsr;
45 import org.glassfish.grizzly.PortRange;
46 import org.glassfish.grizzly.http.server.HttpHandler;
47 import org.glassfish.grizzly.http.server.HttpServer;
48 import org.glassfish.grizzly.http.server.NetworkListener;
49 import org.glassfish.grizzly.http.server.Request;
50 import org.glassfish.grizzly.http.server.Response;
51 import org.glassfish.grizzly.http.server.ServerConfiguration;
52 import org.glassfish.grizzly.http.util.HttpStatus;
55 * Lightweight server to launch Bck2Brwsr applications and tests.
56 * Supports execution in native browser as well as Java's internal
59 class Bck2BrwsrLauncher extends Launcher implements Closeable {
60 private static final Logger LOG = Logger.getLogger(Bck2BrwsrLauncher.class.getName());
61 private static final InvocationContext END = new InvocationContext(null, null, null);
62 private final Set<ClassLoader> loaders = new LinkedHashSet<>();
63 private final BlockingQueue<InvocationContext> methods = new LinkedBlockingQueue<>();
65 private final Res resources = new Res();
66 private final String cmd;
67 private Object[] brwsr;
68 private HttpServer server;
69 private CountDownLatch wait;
71 public Bck2BrwsrLauncher(String cmd) {
76 InvocationContext runMethod(InvocationContext c) throws IOException {
77 loaders.add(c.clazz.getClassLoader());
81 } catch (InterruptedException ex) {
82 throw new IOException(ex);
87 public void setTimeout(long ms) {
91 public void addClassLoader(ClassLoader url) {
92 this.loaders.add(url);
95 ClassLoader[] loaders() {
96 return loaders.toArray(new ClassLoader[loaders.size()]);
99 public void showURL(String startpage) throws IOException {
100 if (!startpage.startsWith("/")) {
101 startpage = "/" + startpage;
103 HttpServer s = initServer(".", true);
104 int last = startpage.lastIndexOf('/');
105 String prefix = startpage.substring(0, last);
106 String simpleName = startpage.substring(last);
107 s.getServerConfiguration().addHttpHandler(new SubTree(resources, prefix), "/");
109 launchServerAndBrwsr(s, simpleName);
110 } catch (URISyntaxException | InterruptedException ex) {
111 throw new IOException(ex);
115 void showDirectory(File dir, String startpage) throws IOException {
116 if (!startpage.startsWith("/")) {
117 startpage = "/" + startpage;
119 HttpServer s = initServer(dir.getPath(), false);
121 launchServerAndBrwsr(s, startpage);
122 } catch (URISyntaxException | InterruptedException ex) {
123 throw new IOException(ex);
128 public void initialize() throws IOException {
131 } catch (InterruptedException ex) {
132 final InterruptedIOException iio = new InterruptedIOException(ex.getMessage());
135 } catch (Exception ex) {
136 if (ex instanceof IOException) {
137 throw (IOException)ex;
139 if (ex instanceof RuntimeException) {
140 throw (RuntimeException)ex;
142 throw new IOException(ex);
146 private HttpServer initServer(String path, boolean addClasses) throws IOException {
147 HttpServer s = HttpServer.createSimpleServer(path, new PortRange(8080, 65535));
149 final ServerConfiguration conf = s.getServerConfiguration();
151 conf.addHttpHandler(new VM(), "/bck2brwsr.js");
152 conf.addHttpHandler(new Classes(resources), "/classes/");
157 private void executeInBrowser() throws InterruptedException, URISyntaxException, IOException {
158 wait = new CountDownLatch(1);
159 server = initServer(".", true);
160 final ServerConfiguration conf = server.getServerConfiguration();
162 class DynamicResourceHandler extends HttpHandler {
163 private final InvocationContext ic;
164 public DynamicResourceHandler(InvocationContext ic) {
165 if (ic == null || ic.resources.isEmpty()) {
166 throw new NullPointerException();
169 for (Resource r : ic.resources) {
170 conf.addHttpHandler(this, r.httpPath);
174 public void close() {
175 conf.removeHttpHandler(this);
179 public void service(Request request, Response response) throws Exception {
180 for (Resource r : ic.resources) {
181 if (r.httpPath.equals(request.getRequestURI())) {
182 LOG.log(Level.INFO, "Serving HttpResource for {0}", request.getRequestURI());
183 response.setContentType(r.httpType);
184 r.httpContent.reset();
185 String[] params = null;
186 if (r.parameters.length != 0) {
187 params = new String[r.parameters.length];
188 for (int i = 0; i < r.parameters.length; i++) {
189 params[i] = request.getParameter(r.parameters[i]);
193 copyStream(r.httpContent, response.getOutputStream(), null, params);
199 conf.addHttpHandler(new Page(resources,
200 "org/apidesign/bck2brwsr/launcher/harness.xhtml"
203 conf.addHttpHandler(new HttpHandler() {
205 List<InvocationContext> cases = new ArrayList<>();
206 DynamicResourceHandler prev;
208 public void service(Request request, Response response) throws Exception {
209 String id = request.getParameter("request");
210 String value = request.getParameter("result");
211 if (value != null && value.indexOf((char)0xC5) != -1) {
212 value = toUTF8(value);
216 InvocationContext mi = null;
219 if (id != null && value != null) {
220 LOG.log(Level.INFO, "Received result for case {0} = {1}", new Object[]{id, value});
221 value = decodeURL(value);
222 int indx = Integer.parseInt(id);
223 cases.get(indx).result(value, null);
224 if (++indx < cases.size()) {
225 mi = cases.get(indx);
226 LOG.log(Level.INFO, "Re-executing case {0}", indx);
230 if (!cases.isEmpty()) {
231 LOG.info("Re-executing test cases");
247 response.getWriter().write("");
250 LOG.log(Level.INFO, "End of data reached. Exiting.");
254 if (!mi.resources.isEmpty()) {
255 prev = new DynamicResourceHandler(mi);
259 final String cn = mi.clazz.getName();
260 final String mn = mi.methodName;
261 LOG.log(Level.INFO, "Request for {0} case. Sending {1}.{2}", new Object[]{caseNmbr, cn, mn});
262 response.getWriter().write("{"
263 + "className: '" + cn + "', "
264 + "methodName: '" + mn + "', "
265 + "request: " + caseNmbr
267 if (mi.html != null) {
268 response.getWriter().write(", html: '");
269 response.getWriter().write(encodeJSON(mi.html));
270 response.getWriter().write("'");
272 response.getWriter().write("}");
276 this.brwsr = launchServerAndBrwsr(server, "/execute");
279 private static String encodeJSON(String in) {
280 StringBuilder sb = new StringBuilder();
281 for (int i = 0; i < in.length(); i++) {
282 char ch = in.charAt(i);
283 if (ch < 32 || ch == '\'' || ch == '"') {
285 String hs = "0000" + Integer.toHexString(ch);
286 hs = hs.substring(hs.length() - 4);
292 return sb.toString();
296 public void shutdown() throws IOException {
299 int prev = methods.size();
301 if (wait != null && wait.await(timeOut, TimeUnit.MILLISECONDS)) {
304 } catch (InterruptedException ex) {
305 throw new IOException(ex);
307 if (prev == methods.size()) {
310 "Timeout and no test has been executed meanwhile (at {0}). Giving up.",
316 "Timeout, but tests got from {0} to {1}. Trying again.",
317 new Object[]{prev, methods.size()}
320 stopServerAndBrwsr(server, brwsr);
323 static void copyStream(InputStream is, OutputStream os, String baseURL, String... params) throws IOException {
329 if (ch == '$' && params.length > 0) {
330 int cnt = is.read() - '0';
331 if (baseURL != null && cnt == 'U' - '0') {
332 os.write(baseURL.getBytes("UTF-8"));
334 if (cnt >= 0 && cnt < params.length) {
335 os.write(params[cnt].getBytes("UTF-8"));
347 private Object[] launchServerAndBrwsr(HttpServer server, final String page) throws IOException, URISyntaxException, InterruptedException {
349 NetworkListener listener = server.getListeners().iterator().next();
350 int port = listener.getPort();
352 URI uri = new URI("http://localhost:" + port + page);
353 return showBrwsr(uri);
355 private static String toUTF8(String value) throws UnsupportedEncodingException {
356 byte[] arr = new byte[value.length()];
357 for (int i = 0; i < arr.length; i++) {
358 arr[i] = (byte)value.charAt(i);
360 return new String(arr, "UTF-8");
363 private static String decodeURL(String s) {
365 int pos = s.indexOf('%');
369 int i = Integer.parseInt(s.substring(pos + 1, pos + 2), 16);
370 s = s.substring(0, pos) + (char)i + s.substring(pos + 2);
374 private void stopServerAndBrwsr(HttpServer server, Object[] brwsr) throws IOException {
378 Process process = (Process)brwsr[0];
381 InputStream stdout = process.getInputStream();
382 InputStream stderr = process.getErrorStream();
383 drain("StdOut", stdout);
384 drain("StdErr", stderr);
388 res = process.waitFor();
389 } catch (InterruptedException ex) {
390 throw new IOException(ex);
392 LOG.log(Level.INFO, "Exit code: {0}", res);
394 deleteTree((File)brwsr[1]);
397 private static void drain(String name, InputStream is) throws IOException {
398 int av = is.available();
400 StringBuilder sb = new StringBuilder();
401 sb.append("v== ").append(name).append(" ==v\n");
403 sb.append((char)is.read());
405 sb.append("\n^== ").append(name).append(" ==^");
406 LOG.log(Level.INFO, sb.toString());
410 private void deleteTree(File file) {
414 File[] arr = file.listFiles();
424 public void close() throws IOException {
428 protected Object[] showBrwsr(URI uri) throws IOException {
429 LOG.log(Level.INFO, "Showing {0}", uri);
432 LOG.log(Level.INFO, "Trying Desktop.browse on {0} {2} by {1}", new Object[] {
433 System.getProperty("java.vm.name"),
434 System.getProperty("java.vm.vendor"),
435 System.getProperty("java.vm.version"),
437 java.awt.Desktop.getDesktop().browse(uri);
438 LOG.log(Level.INFO, "Desktop.browse successfully finished");
440 } catch (UnsupportedOperationException ex) {
441 LOG.log(Level.INFO, "Desktop.browse not supported: {0}", ex.getMessage());
442 LOG.log(Level.FINE, null, ex);
446 String cmdName = cmd == null ? "xdg-open" : cmd;
448 cmdName, uri.toString()
450 LOG.log(Level.INFO, "Launching {0}", Arrays.toString(cmdArr));
451 final Process process = Runtime.getRuntime().exec(cmdArr);
452 return new Object[] { process, null };
456 void generateBck2BrwsrJS(StringBuilder sb, Bck2Brwsr.Resources loader) throws IOException {
457 Bck2Brwsr.generate(sb, loader);
459 "(function WrapperVM(global) {"
460 + " function ldCls(res) {\n"
461 + " var request = new XMLHttpRequest();\n"
462 + " request.open('GET', '/classes/' + res, false);\n"
463 + " request.send();\n"
464 + " if (request.status !== 200) return null;\n"
465 + " var arr = eval('(' + request.responseText + ')');\n"
468 + " var prevvm = global.bck2brwsr;\n"
469 + " global.bck2brwsr = function() {\n"
470 + " var args = Array.prototype.slice.apply(arguments);\n"
471 + " args.unshift(ldCls);\n"
472 + " return prevvm.apply(null, args);\n"
477 private class Res implements Bck2Brwsr.Resources {
479 public InputStream get(String resource) throws IOException {
480 for (ClassLoader l : loaders) {
482 Enumeration<URL> en = l.getResources(resource);
483 while (en.hasMoreElements()) {
484 u = en.nextElement();
487 return u.openStream();
490 throw new IOException("Can't find " + resource);
494 private static class Page extends HttpHandler {
495 final String resource;
496 private final String[] args;
497 private final Res res;
499 public Page(Res res, String resource, String... args) {
501 this.resource = resource;
502 this.args = args.length == 0 ? new String[] { "$0" } : args;
506 public void service(Request request, Response response) throws Exception {
507 String r = computePage(request);
508 if (r.startsWith("/")) {
511 String[] replace = {};
512 if (r.endsWith(".html")) {
513 response.setContentType("text/html");
514 LOG.info("Content type text/html");
517 if (r.endsWith(".xhtml")) {
518 response.setContentType("application/xhtml+xml");
519 LOG.info("Content type application/xhtml+xml");
522 OutputStream os = response.getOutputStream();
523 try (InputStream is = res.get(r)) {
524 copyStream(is, os, request.getRequestURL().toString(), replace);
525 } catch (IOException ex) {
526 response.setDetailMessage(ex.getLocalizedMessage());
528 response.setStatus(404);
532 protected String computePage(Request request) {
535 r = request.getHttpHandlerPath();
541 private static class SubTree extends Page {
543 public SubTree(Res res, String resource, String... args) {
544 super(res, resource, args);
548 protected String computePage(Request request) {
549 return resource + request.getHttpHandlerPath();
555 private class VM extends HttpHandler {
557 public void service(Request request, Response response) throws Exception {
558 response.setCharacterEncoding("UTF-8");
559 response.setContentType("text/javascript");
560 StringBuilder sb = new StringBuilder();
561 generateBck2BrwsrJS(sb, Bck2BrwsrLauncher.this.resources);
562 response.getWriter().write(sb.toString());
566 private static class Classes extends HttpHandler {
567 private final Res loader;
569 public Classes(Res loader) {
570 this.loader = loader;
574 public void service(Request request, Response response) throws Exception {
575 String res = request.getHttpHandlerPath();
576 if (res.startsWith("/")) {
577 res = res.substring(1);
579 try (InputStream is = loader.get(res)) {
580 response.setContentType("text/javascript");
581 Writer w = response.getWriter();
583 for (int i = 0;; i++) {
597 w.append(Integer.toString(b));
600 } catch (IOException ex) {
601 response.setStatus(HttpStatus.NOT_FOUND_404);
603 response.setDetailMessage(ex.getMessage());