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.Reader;
27 import java.io.UnsupportedEncodingException;
28 import java.io.Writer;
30 import java.net.URISyntaxException;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Enumeration;
35 import java.util.LinkedHashSet;
36 import java.util.List;
38 import java.util.concurrent.BlockingQueue;
39 import java.util.concurrent.Callable;
40 import java.util.concurrent.CountDownLatch;
41 import java.util.concurrent.LinkedBlockingQueue;
42 import java.util.concurrent.TimeUnit;
43 import java.util.logging.Level;
44 import java.util.logging.Logger;
45 import org.apidesign.bck2brwsr.launcher.InvocationContext.Resource;
46 import org.glassfish.grizzly.PortRange;
47 import org.glassfish.grizzly.http.server.HttpHandler;
48 import org.glassfish.grizzly.http.server.HttpServer;
49 import org.glassfish.grizzly.http.server.NetworkListener;
50 import org.glassfish.grizzly.http.server.Request;
51 import org.glassfish.grizzly.http.server.Response;
52 import org.glassfish.grizzly.http.server.ServerConfiguration;
53 import org.glassfish.grizzly.http.util.HttpStatus;
56 * Lightweight server to launch Bck2Brwsr applications and tests.
57 * Supports execution in native browser as well as Java's internal
60 abstract class BaseHTTPLauncher extends Launcher implements Closeable, Callable<HttpServer> {
61 static final Logger LOG = Logger.getLogger(BaseHTTPLauncher.class.getName());
62 private static final InvocationContext END = new InvocationContext(null, null, null);
63 private final Set<ClassLoader> loaders = new LinkedHashSet<>();
64 private final BlockingQueue<InvocationContext> methods = new LinkedBlockingQueue<>();
66 private final Res resources = new Res();
67 private final String cmd;
68 private Object[] brwsr;
69 private HttpServer server;
70 private CountDownLatch wait;
72 public BaseHTTPLauncher(String cmd) {
74 addClassLoader(BaseHTTPLauncher.class.getClassLoader());
79 InvocationContext runMethod(InvocationContext c) throws IOException {
80 loaders.add(c.clazz.getClassLoader());
84 } catch (InterruptedException ex) {
85 throw new IOException(ex);
90 public void setTimeout(long ms) {
94 public void addClassLoader(ClassLoader url) {
95 this.loaders.add(url);
98 ClassLoader[] loaders() {
99 return loaders.toArray(new ClassLoader[loaders.size()]);
102 public void showURL(String startpage) throws IOException {
103 if (!startpage.startsWith("/")) {
104 startpage = "/" + startpage;
106 HttpServer s = initServer(".", true);
107 int last = startpage.lastIndexOf('/');
108 String prefix = startpage.substring(0, last);
109 String simpleName = startpage.substring(last);
110 s.getServerConfiguration().addHttpHandler(new SubTree(resources, prefix), "/");
113 launchServerAndBrwsr(s, simpleName);
114 } catch (URISyntaxException | InterruptedException ex) {
115 throw new IOException(ex);
119 void showDirectory(File dir, String startpage) throws IOException {
120 if (!startpage.startsWith("/")) {
121 startpage = "/" + startpage;
123 HttpServer s = initServer(dir.getPath(), false);
125 launchServerAndBrwsr(s, startpage);
126 } catch (URISyntaxException | InterruptedException ex) {
127 throw new IOException(ex);
132 public void initialize() throws IOException {
135 } catch (InterruptedException ex) {
136 final InterruptedIOException iio = new InterruptedIOException(ex.getMessage());
139 } catch (Exception ex) {
140 if (ex instanceof IOException) {
141 throw (IOException)ex;
143 if (ex instanceof RuntimeException) {
144 throw (RuntimeException)ex;
146 throw new IOException(ex);
150 private HttpServer initServer(String path, boolean addClasses) throws IOException {
151 HttpServer s = HttpServer.createSimpleServer(path, new PortRange(8080, 65535));
153 final ServerConfiguration conf = s.getServerConfiguration();
155 conf.addHttpHandler(new VM(), "/bck2brwsr.js");
156 conf.addHttpHandler(new Classes(resources), "/classes/");
161 private void executeInBrowser() throws InterruptedException, URISyntaxException, IOException {
162 wait = new CountDownLatch(1);
163 server = initServer(".", true);
164 final ServerConfiguration conf = server.getServerConfiguration();
166 class DynamicResourceHandler extends HttpHandler {
167 private final InvocationContext ic;
168 public DynamicResourceHandler(InvocationContext ic) {
169 if (ic == null || ic.resources.isEmpty()) {
170 throw new NullPointerException();
173 for (Resource r : ic.resources) {
174 conf.addHttpHandler(this, r.httpPath);
178 public void close() {
179 conf.removeHttpHandler(this);
183 public void service(Request request, Response response) throws Exception {
184 for (Resource r : ic.resources) {
185 if (r.httpPath.equals(request.getRequestURI())) {
186 LOG.log(Level.INFO, "Serving HttpResource for {0}", request.getRequestURI());
187 response.setContentType(r.httpType);
188 r.httpContent.reset();
189 String[] params = null;
190 if (r.parameters.length != 0) {
191 params = new String[r.parameters.length];
192 for (int i = 0; i < r.parameters.length; i++) {
193 params[i] = request.getParameter(r.parameters[i]);
194 if (params[i] == null) {
195 if ("http.method".equals(r.parameters[i])) {
196 params[i] = request.getMethod().toString();
197 } else if ("http.requestBody".equals(r.parameters[i])) {
198 Reader rdr = request.getReader();
199 StringBuilder sb = new StringBuilder();
207 params[i] = sb.toString();
210 if (params[i] == null) {
216 copyStream(r.httpContent, response.getOutputStream(), null, params);
222 conf.addHttpHandler(new Page(resources, harnessResource()), "/execute");
224 conf.addHttpHandler(new HttpHandler() {
226 List<InvocationContext> cases = new ArrayList<>();
227 DynamicResourceHandler prev;
229 public void service(Request request, Response response) throws Exception {
230 String id = request.getParameter("request");
231 String value = request.getParameter("result");
232 if (value != null && value.indexOf((char)0xC5) != -1) {
233 value = toUTF8(value);
237 InvocationContext mi = null;
240 if (id != null && value != null) {
241 LOG.log(Level.INFO, "Received result for case {0} = {1}", new Object[]{id, value});
242 value = decodeURL(value);
243 int indx = Integer.parseInt(id);
244 cases.get(indx).result(value, null);
245 if (++indx < cases.size()) {
246 mi = cases.get(indx);
247 LOG.log(Level.INFO, "Re-executing case {0}", indx);
251 if (!cases.isEmpty()) {
252 LOG.info("Re-executing test cases");
268 response.getWriter().write("");
271 LOG.log(Level.INFO, "End of data reached. Exiting.");
275 if (!mi.resources.isEmpty()) {
276 prev = new DynamicResourceHandler(mi);
280 final String cn = mi.clazz.getName();
281 final String mn = mi.methodName;
282 LOG.log(Level.INFO, "Request for {0} case. Sending {1}.{2}", new Object[]{caseNmbr, cn, mn});
283 response.getWriter().write("{"
284 + "className: '" + cn + "', "
285 + "methodName: '" + mn + "', "
286 + "request: " + caseNmbr
288 if (mi.html != null) {
289 response.getWriter().write(", html: '");
290 response.getWriter().write(encodeJSON(mi.html));
291 response.getWriter().write("'");
293 response.getWriter().write("}");
297 this.brwsr = launchServerAndBrwsr(server, "/execute");
300 private static String encodeJSON(String in) {
301 StringBuilder sb = new StringBuilder();
302 for (int i = 0; i < in.length(); i++) {
303 char ch = in.charAt(i);
304 if (ch < 32 || ch == '\'' || ch == '"') {
306 String hs = "0000" + Integer.toHexString(ch);
307 hs = hs.substring(hs.length() - 4);
313 return sb.toString();
317 public void shutdown() throws IOException {
320 int prev = methods.size();
322 if (wait != null && wait.await(timeOut, TimeUnit.MILLISECONDS)) {
325 } catch (InterruptedException ex) {
326 throw new IOException(ex);
328 if (prev == methods.size()) {
331 "Timeout and no test has been executed meanwhile (at {0}). Giving up.",
337 "Timeout, but tests got from {0} to {1}. Trying again.",
338 new Object[]{prev, methods.size()}
341 stopServerAndBrwsr(server, brwsr);
344 static void copyStream(InputStream is, OutputStream os, String baseURL, String... params) throws IOException {
350 if (ch == '$' && params.length > 0) {
351 int cnt = is.read() - '0';
352 if (baseURL != null && cnt == 'U' - '0') {
353 os.write(baseURL.getBytes("UTF-8"));
355 if (cnt >= 0 && cnt < params.length) {
356 os.write(params[cnt].getBytes("UTF-8"));
368 private Object[] launchServerAndBrwsr(HttpServer server, final String page) throws IOException, URISyntaxException, InterruptedException {
370 NetworkListener listener = server.getListeners().iterator().next();
371 int port = listener.getPort();
373 URI uri = new URI("http://localhost:" + port + page);
374 return showBrwsr(uri);
376 private static String toUTF8(String value) throws UnsupportedEncodingException {
377 byte[] arr = new byte[value.length()];
378 for (int i = 0; i < arr.length; i++) {
379 arr[i] = (byte)value.charAt(i);
381 return new String(arr, "UTF-8");
384 private static String decodeURL(String s) {
386 int pos = s.indexOf('%');
390 int i = Integer.parseInt(s.substring(pos + 1, pos + 2), 16);
391 s = s.substring(0, pos) + (char)i + s.substring(pos + 2);
395 private void stopServerAndBrwsr(HttpServer server, Object[] brwsr) throws IOException {
399 Process process = (Process)brwsr[0];
402 InputStream stdout = process.getInputStream();
403 InputStream stderr = process.getErrorStream();
404 drain("StdOut", stdout);
405 drain("StdErr", stderr);
409 res = process.waitFor();
410 } catch (InterruptedException ex) {
411 throw new IOException(ex);
413 LOG.log(Level.INFO, "Exit code: {0}", res);
415 deleteTree((File)brwsr[1]);
418 private static void drain(String name, InputStream is) throws IOException {
419 int av = is.available();
421 StringBuilder sb = new StringBuilder();
422 sb.append("v== ").append(name).append(" ==v\n");
424 sb.append((char)is.read());
426 sb.append("\n^== ").append(name).append(" ==^");
427 LOG.log(Level.INFO, sb.toString());
431 private void deleteTree(File file) {
435 File[] arr = file.listFiles();
445 public HttpServer call() throws Exception {
450 public void close() throws IOException {
454 protected Object[] showBrwsr(URI uri) throws IOException {
455 LOG.log(Level.INFO, "Showing {0}", uri);
458 LOG.log(Level.INFO, "Trying Desktop.browse on {0} {2} by {1}", new Object[] {
459 System.getProperty("java.vm.name"),
460 System.getProperty("java.vm.vendor"),
461 System.getProperty("java.vm.version"),
463 java.awt.Desktop.getDesktop().browse(uri);
464 LOG.log(Level.INFO, "Desktop.browse successfully finished");
466 } catch (UnsupportedOperationException ex) {
467 LOG.log(Level.INFO, "Desktop.browse not supported: {0}", ex.getMessage());
468 LOG.log(Level.FINE, null, ex);
472 String cmdName = cmd == null ? "xdg-open" : cmd;
474 cmdName, uri.toString()
476 LOG.log(Level.INFO, "Launching {0}", Arrays.toString(cmdArr));
477 final Process process = Runtime.getRuntime().exec(cmdArr);
478 return new Object[] { process, null };
482 abstract void generateBck2BrwsrJS(StringBuilder sb, Res loader) throws IOException;
483 abstract String harnessResource();
486 public InputStream get(String resource) throws IOException {
487 for (ClassLoader l : loaders) {
489 Enumeration<URL> en = l.getResources(resource);
490 while (en.hasMoreElements()) {
491 u = en.nextElement();
494 return u.openStream();
497 throw new IOException("Can't find " + resource);
501 private static class Page extends HttpHandler {
502 final String resource;
503 private final String[] args;
504 private final Res res;
506 public Page(Res res, String resource, String... args) {
508 this.resource = resource;
509 this.args = args.length == 0 ? new String[] { "$0" } : args;
513 public void service(Request request, Response response) throws Exception {
514 String r = computePage(request);
515 if (r.startsWith("/")) {
518 String[] replace = {};
519 if (r.endsWith(".html")) {
520 response.setContentType("text/html");
521 LOG.info("Content type text/html");
524 if (r.endsWith(".xhtml")) {
525 response.setContentType("application/xhtml+xml");
526 LOG.info("Content type application/xhtml+xml");
529 OutputStream os = response.getOutputStream();
530 try (InputStream is = res.get(r)) {
531 copyStream(is, os, request.getRequestURL().toString(), replace);
532 } catch (IOException ex) {
533 response.setDetailMessage(ex.getLocalizedMessage());
535 response.setStatus(404);
539 protected String computePage(Request request) {
542 r = request.getHttpHandlerPath();
548 private static class SubTree extends Page {
550 public SubTree(Res res, String resource, String... args) {
551 super(res, resource, args);
555 protected String computePage(Request request) {
556 return resource + request.getHttpHandlerPath();
562 private class VM extends HttpHandler {
564 public void service(Request request, Response response) throws Exception {
565 response.setCharacterEncoding("UTF-8");
566 response.setContentType("text/javascript");
567 StringBuilder sb = new StringBuilder();
568 generateBck2BrwsrJS(sb, BaseHTTPLauncher.this.resources);
569 response.getWriter().write(sb.toString());
573 private static class Classes extends HttpHandler {
574 private final Res loader;
576 public Classes(Res loader) {
577 this.loader = loader;
581 public void service(Request request, Response response) throws Exception {
582 String res = request.getHttpHandlerPath();
583 if (res.startsWith("/")) {
584 res = res.substring(1);
586 try (InputStream is = loader.get(res)) {
587 response.setContentType("text/javascript");
588 Writer w = response.getWriter();
590 for (int i = 0;; i++) {
604 w.append(Integer.toString(b));
607 } catch (IOException ex) {
608 response.setStatus(HttpStatus.NOT_FOUND_404);
610 response.setDetailMessage(ex.getMessage());