This is an automated email from the ASF dual-hosted git repository. jtulach pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans-html4j.git
commit c853bf519492ba9c2fa46e02330fd22953d497f0 Author: Jaroslav Tulach <[email protected]> AuthorDate: Tue Mar 31 19:43:42 2020 +0200 Abstracting the interface between Browser.Command and Grizzly HTTP server --- .../netbeans/html/presenters/browser/Browser.java | 359 ++++++++++++--------- .../html/presenters/browser/GrizzlyServer.java | 207 ++++++++++++ .../html/presenters/browser/HttpServer.java | 62 ++++ .../html/presenters/browser/BrowserTest.java | 58 +--- .../html/presenters/browser/DynamicHTTP.java | 68 ++-- .../presenters/browser/JavaScriptUtilities.java | 43 +++ .../netbeans/html/presenters/browser/KOScript.java | 11 +- .../html/presenters/browser/KoBrowserTest.java | 143 ++------ .../html/presenters/browser/ServerFactories.java | 91 ++++++ .../presenters/browser/ServerMimeTypeTest.java | 102 ++++++ .../html/presenters/browser/ServerTest.java | 9 +- .../html/presenters/browser/SimpleServerTest.java | 270 ++++++++++++++++ .../org/netbeans/html/presenters/browser/test.css | 18 ++ .../org/netbeans/html/presenters/browser/test.js | 18 ++ .../netbeans/html/presenters/browser/test.min.js | 18 ++ 15 files changed, 1090 insertions(+), 387 deletions(-) diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java b/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java index 9b80d32..d433644 100644 --- a/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java +++ b/browser/src/main/java/org/netbeans/html/presenters/browser/Browser.java @@ -20,6 +20,7 @@ package org.netbeans.html.presenters.browser; import org.netbeans.html.presenters.render.Show; import java.io.Closeable; +import java.io.File; import java.io.FileNotFoundException; import java.io.Flushable; import java.io.IOException; @@ -31,6 +32,9 @@ import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; @@ -41,16 +45,9 @@ import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; -import org.glassfish.grizzly.PortRange; -import org.glassfish.grizzly.http.server.HttpHandler; -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.grizzly.http.server.NetworkListener; -import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.http.server.Response; -import org.glassfish.grizzly.http.server.ServerConfiguration; -import org.glassfish.grizzly.http.util.HttpStatus; import org.netbeans.html.boot.spi.Fn; import org.netbeans.html.boot.spi.Fn.Presenter; import org.netbeans.html.presenters.spi.ProtoPresenter; @@ -74,12 +71,13 @@ import org.openide.util.lookup.ServiceProvider; public final class Browser implements Fn.Presenter, Fn.KeepAlive, Flushable, Executor, Closeable { static final Logger LOG = Logger.getLogger(Browser.class.getName()); - private final Map<String,Command> SESSIONS = new HashMap<String, Command>(); + private final Map<String,Command> SESSIONS = new HashMap<>(); private final String app; - private HttpServer s; + private HttpServer server; private Runnable onPageLoad; private Command current; private final Config config; + private final Supplier<HttpServer<?, ?, ?, ?>> serverProvider; /** Default constructor. Reads configuration from properties. The actual browser to * be launched can be influenced by value of @@ -111,34 +109,39 @@ Executor, Closeable { * @param config the configuration */ public Browser(Config config) { - this(findCalleeClassName(), config); + this(findCalleeClassName(), config, null); } - - Browser(String app, Config config) { + + Browser(String app, Config config, Supplier<HttpServer<?,?,?, ?>> serverProvider) { + this.serverProvider = serverProvider != null ? serverProvider : GrizzlyServer::new; this.app = app; this.config = new Config(config); } @Override public final void execute(final Runnable r) { - current.runSafe(r, true); + current.execute(r); } @Override public void close() throws IOException { - s.shutdownNow(); + if (server != null) { + server.shutdownNow(); + } } HttpServer server() { - return s; + return server; } static HttpServer findServer(Object obj) { - Command c = null; + Command c; if (obj instanceof Command) { c = (Command) obj; } else if (obj instanceof ProtoPresenter) { c = ((ProtoPresenter) obj).lookup(Command.class); + } else { + throw new IllegalArgumentException("Cannot find server for " + obj); } return c.browser.server(); } @@ -199,23 +202,9 @@ Executor, Closeable { public void flush() throws IOException { throw new UnsupportedOperationException(); } - - private static HttpServer server(RootPage r, Config config) { - int from = 8080; - int to = 65535; - int port = config.getPort(); - if (port != -1) { - from = to = port; - } - HttpServer s = HttpServer.createSimpleServer(null, new PortRange(from, to)); - final ServerConfiguration conf = s.getServerConfiguration(); - conf.addHttpHandler(r, "/"); - return s; - } - + private static URI pageURL(String protocol, HttpServer server, final String page) { - NetworkListener listener = server.getListeners().iterator().next(); - int port = listener.getPort(); + int port = server.getPort(); try { return new URI(protocol + "://localhost:" + port + page); } catch (URISyntaxException ex) { @@ -227,22 +216,33 @@ Executor, Closeable { public final void displayPage(URL page, Runnable onPageLoad) { try { this.onPageLoad = onPageLoad; - s = server(new RootPage(page), config); - s.start(); - show(pageURL("http", s, "/")); + this.server = serverProvider.get(); + int from = 8080; + int to = 65535; + int port = config.getPort(); + if (port != -1) { + from = to = port; + } + server.init(from, to); + + this.server.addHttpHandler(new RootPage(page), "/"); + server.start(); + + show(pageURL("http", server, "/")); } catch (IOException ex) { Logger.getLogger(Browser.class.getName()).log(Level.SEVERE, null, ex); } } /** Parameters to configure {@link Browser}. - * Create an instance and pass it + * Create an instance and pass it * to {@link Browser#Browser(org.netbeans.html.presenters.browser.Browser.Config) } * constructor. */ public final static class Config { String browser; Integer port; + boolean debug; /** * Default constructor. @@ -253,6 +253,7 @@ Executor, Closeable { private Config(Config copy) { this.browser = copy.browser; this.port = copy.port; + this.debug = copy.debug; } /** The command to use when invoking a browser. Possible values: @@ -288,7 +289,20 @@ Executor, Closeable { this.port = port; return this; } - + + /** Enable or disable debugging. The default value is taken from a property + * {@code com.dukescript.presenters.browserDebug}. If the property is + * not specified, then the default value is {@code false}. + * + * @param debug true or false + * @return this instance + * @since 1.8 + */ + Config debug(boolean debug) { + this.debug = debug; + return this; + } + final String getBrowser() { if (browser != null) { return browser; @@ -300,40 +314,40 @@ Executor, Closeable { if (port != null) { return port; } - String port = System.getProperty("com.dukescript.presenters.browserPort"); // NOI18N + String browserPort = System.getProperty("com.dukescript.presenters.browserPort"); // NOI18N try { - return Integer.parseInt(port); + return Integer.parseInt(browserPort); } catch (NumberFormatException ex) { return -1; } } } - - static void cors(Response r) { - r.setCharacterEncoding("UTF-8"); - r.addHeader("Access-Control-Allow-Origin", "*"); - r.addHeader("Access-Control-Allow-Credentials", "true"); - r.addHeader("Access-Control-Allow-Headers", "Content-Type"); - r.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); + + static <Response> void cors(HttpServer<?, Response, ?, ?> s, Response r) { + s.setCharacterEncoding(r, "UTF-8"); + s.addHeader(r, "Access-Control-Allow-Origin", "*"); + s.addHeader(r, "Access-Control-Allow-Credentials", "true"); + s.addHeader(r, "Access-Control-Allow-Headers", "Content-Type"); + s.addHeader(r, "Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); } - private final class RootPage extends HttpHandler { + private final class RootPage extends HttpServer.Handler { private final URL page; public RootPage(URL page) { this.page = page; } - + @Override - public void service(Request rqst, Response rspns) throws Exception { - String path = rqst.getRequestURI(); - cors(rspns); + public <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException { + String path = server.getRequestURI(rqst); + cors(server, rspns); if ("/".equals(path) || "index.html".equals(path)) { Reader is; - String prefix = "http://" + rqst.getServerName() + ":" + rqst.getServerPort() + "/"; - Writer w = rspns.getWriter(); - rspns.setContentType("text/html"); - final Command cmd = new Command(Browser.this, prefix); + String prefix = "http://" + server.getServerName(rqst) + ":" + server.getServerPort(rqst) + "/"; + Writer w = server.getWriter(rspns); + server.setContentType(rspns, "text/html"); + final Command cmd = new Command(server, Browser.this, prefix); try { is = new InputStreamReader(page.openStream()); } catch (IOException ex) { @@ -378,11 +392,11 @@ Executor, Closeable { is.close(); w.close(); } else if (path.equals("/command.js")) { - String id = rqst.getParameter("id"); + String id = server.getParameter(rqst, "id"); Command c = SESSIONS.get(id); if (c == null) { - rspns.getOutputBuffer().write("No command for " + id); - rspns.setStatus(HttpStatus.NOT_FOUND_404); + server.getWriter(rspns).write("No command for " + id); + server.setStatus(rspns, 404); return; } c.service(rqst, rspns); @@ -392,13 +406,39 @@ Executor, Closeable { } URL relative = new URL(page, path); InputStream is; + URLConnection conn; try { - is = relative.openStream(); + conn = relative.openConnection(); + is = conn.getInputStream(); } catch (FileNotFoundException ex) { - rspns.setStatus(HttpStatus.NOT_FOUND_404); + server.setStatus(rspns, 404); return; } - OutputStream out = rspns.getOutputStream(); + String found = null; + if (relative.getProtocol().equals("file")) { + try { + File file = new File(relative.toURI()); + found = Files.probeContentType(file.toPath()); + } catch (URISyntaxException | IOException ignore) { + } + } else { + found = conn.getContentType(); + } + if (found == null || "content/unknown".equals(found)) { + if (path.endsWith(".html")) { + found = "text/html"; + } + if (path.endsWith(".js")) { + found = "text/javascript"; + } + if (path.endsWith(".css")) { + found = "text/css"; + } + } + if (found != null) { + server.setContentType(rspns, found); + } + OutputStream out = server.getOutputStream(rspns); for (;;) { int b = is.read(); if (b == -1) { @@ -414,13 +454,19 @@ Executor, Closeable { private void emitScript(Writer w, String prefix, String id) throws IOException { w.write(" <script id='exec' type='text/javascript'>"); w.write("\n" - + "function waitForCommand() {\n" + + "function waitForCommand(counter) {\n" + " try {\n" + " if (waitForCommand.seenError) {\n" + " console.warn('Disconnected from " + prefix + "');\n" + " return;\n" + " };\n" - + " var request = new XMLHttpRequest();\n" + + " var request = new XMLHttpRequest();\n"); + if (Browser.this.config.debug) { + w.write("" + + " console.log('GET[' + counter + ']....');\n" + ); + } + w.write("" + " request.open('GET', '" + prefix + "command.js?id=" + id + "', true);\n" + " request.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');\n" + " request.onerror = function(ev) {\n" @@ -430,27 +476,73 @@ Executor, Closeable { + " request.onreadystatechange = function() {\n" + " if (this.readyState!==4) return;\n" + " try {\n" + ); + if (Browser.this.config.debug) { + w.write("" + + " console.log('...GET[' + counter + '] got something ' + this.responseText.substring(0,80));\n" + " var cmd = document.getElementById('cmd');\n" + " if (cmd) cmd.innerHTML = this.responseText.substring(0,80);\n" + ); + } + w.write("" + " (0 || eval)(this.responseText);\n" + " } catch (e) {\n" + " console.warn(e); \n" + " } finally {\n" - + " waitForCommand();\n" + + " waitForCommand(counter + 1);\n" + " }\n" + " };\n" + " request.send();\n" + " } catch (e) {\n" + " console.warn(e);\n" - + " waitForCommand();\n" + + " waitForCommand(counter + 1);\n" + " }\n" + "}\n" - + "waitForCommand();\n" + + "waitForCommand(1);\n" ); w.write(" </script>\n"); } } + String createCallbackFn(String prefix, String id) { + StringBuilder sb = new StringBuilder(); + sb.append("this.toBrwsrSrvr = function(name, a1, a2, a3, a4) {\n" + + "var url = '").append(prefix).append("command.js?id=").append(id).append("&name=' + name;\n" + + "var body = 'p0=' + encodeURIComponent(a1);\n" + + "body += '&p1=' + encodeURIComponent(a2);\n" + + "body += '&p2=' + encodeURIComponent(a3);\n" + + "body += '&p3=' + encodeURIComponent(a4);\n" + + "var request = new XMLHttpRequest();\n" + ); + if (Browser.this.config.debug) { + sb.append("" + + "console.log('PUT ... ' + body.substring(0, 80));\n" + + "var now = new Date().getTime();\n" + ); + } + sb.append("" + + "request.open('PUT', url, false);\n" + + "request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8');\n" + + "request.send(body);\n" + + "var txt = request.responseText;\n" + ); + if (Browser.this.config.debug) { + sb.append("" + + "var then = new Date().getTime();\n" + + "if (txt && txt !== 'null') {\n" + + " var cmd = document.getElementById('cmd');\n" + + " if (cmd) cmd.innerHTML = txt.substring(0,80);\n" + + "}\n" + + "console.log('... PUT [' + (then - now) + 'ms]: ' + txt.substring(0, 80));\n" + ); + } + sb.append("" + + "return txt;\n" + + "};\n" + ); + return sb.toString(); + } + private static String findCalleeClassName() { StackTraceElement[] frames = new Exception().getStackTrace(); for (StackTraceElement e : frames) { @@ -477,22 +569,23 @@ Executor, Closeable { } return "org.netbeans.html"; // NOI18N } - - private static final class Command extends Object - implements Executor, ThreadFactory { + + private static final class Command<Request, Response, Runner> extends Object + implements Executor { + private final HttpServer<Request, Response, ?, Runner> server; private final Queue<Object> exec; private final Browser browser; private final String id; private final String prefix; - private final Executor RUN; - private Thread RUNNER; + private Runner RUNNER; private Response suspended; private boolean initialized; private final ProtoPresenter presenter; - Command(Browser browser, String prefix) { - this.RUN = Executors.newSingleThreadExecutor(this); + Command(HttpServer<Request, Response, ?, Runner> s, Browser browser, String prefix) { + this.server = s; this.id = UUID.randomUUID().toString(); + this.RUNNER = s.initializeRunner(this.id); this.exec = new LinkedList<>(); this.prefix = prefix; this.browser = browser; @@ -509,81 +602,54 @@ Executor, Closeable { } @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "Processor for " + id); - RUNNER = t; - return t; - } - - @Override public final void execute(final Runnable r) { - runSafe(r, true); - } - - final void runSafe(final Runnable r, final boolean context) { - class Wrap implements Runnable { - @Override - public void run() { - if (context) { - Closeable c = Fn.activate(Command.this.presenter); - try { - r.run(); - } finally { - try { - c.close(); - } catch (IOException ex) { - // ignore - } - } - } else { - r.run(); - } - } - } - if (RUNNER == Thread.currentThread()) { - if (context) { - Runnable w = new Wrap(); - w.run(); - } else { - r.run(); - } - } else { - Runnable w = new Wrap(); - RUN.execute(w); - } + server.runSafe(this.RUNNER, r, this.presenter); } - + final synchronized void add(Object obj) { if (suspended != null) { - try { - suspended.getWriter().write(obj.toString()); - } catch (IOException ex) { - LOG.log(Level.SEVERE, null, ex); - } - suspended.resume(); + Response rqst = suspended; + server.resume(rqst, () -> { + try (Writer w = server.getWriter(rqst)) { + w.write(obj.toString()); + } catch (IOException ex) { + LOG.log(Level.SEVERE, null, ex); + } + }); suspended = null; return; } exec.add(obj); } - + private synchronized Object take(Response rspns) { Object o = exec.poll(); if (o != null) { return o; } suspended = rspns; - rspns.suspend(); + server.suspend(rspns); return null; } - - void service(Request rqst, Response rspns) throws Exception { - final String methodName = rqst.getParameter("name"); - Writer w = rspns.getWriter(); + + private synchronized boolean initialize(Response rspns) { + if (!initialized) { + initialized = true; + suspended = rspns; + server.suspend(rspns); + execute(browser.onPageLoad); + return true; + } + return false; + } + + void service(Request rqst, Response rspns) throws IOException { + final String methodName = server.getParameter(rqst, "name"); + server.setContentType(rspns, "text/javascript"); + Writer w = server.getWriter(rspns); if (methodName == null) { - if (!initialized) { - initialized = true; - execute(browser.onPageLoad); + if (initialize(rspns)) { + return; } // send new request Object obj = take(rspns); @@ -595,13 +661,10 @@ Executor, Closeable { w.write(s); LOG.log(Level.FINE, "Exec global: {0}", s); } else { - List<String> args = new ArrayList<String>(); - for (;;) { - String p = rqst.getParameter("p" + args.size()); - if (p == null) { - break; - } - args.add(p); + List<String> args = new ArrayList<>(); + String body = server.getBody(rqst); + for (String p : body.split("&")) { + args.add(URLDecoder.decode(p.substring(3), "UTF-8")); } String res; try { @@ -623,19 +686,7 @@ Executor, Closeable { } void callbackFn(ProtoPresenterBuilder.OnPrepared onReady) { - StringBuilder sb = new StringBuilder(); - sb.append("this.toBrwsrSrvr = function(name, a1, a2, a3, a4) {\n" - + "var url = '").append(prefix).append("command.js?id=").append(id).append("&name=' + name;\n" - + "url += '&p0=' + encodeURIComponent(a1);\n" - + "url += '&p1=' + encodeURIComponent(a2);\n" - + "url += '&p2=' + encodeURIComponent(a3);\n" - + "url += '&p3=' + encodeURIComponent(a4);\n" - + "var request = new XMLHttpRequest();\n" - + "request.open('GET', url, false);\n" - + "request.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');\n" - + "request.send();\n" - + "return request.responseText;\n" - + "};\n"); + String sb = this.browser.createCallbackFn(prefix, id); add(sb); onReady.callbackIsPrepared("toBrwsrSrvr"); } @@ -652,7 +703,7 @@ Executor, Closeable { } return Level.FINE; } - + void log(int priority, String msg, Object... args) { Level level = findLevel(priority); @@ -668,12 +719,12 @@ Executor, Closeable { } void dispatch(Runnable r) { - runSafe(r, false); + server.runSafe(RUNNER, r, null); } public void displayPage(URL url, Runnable r) { throw new UnsupportedOperationException(url.toString()); } - } // end of Command + } // end of Command } diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java b/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java new file mode 100644 index 0000000..78b8d33 --- /dev/null +++ b/browser/src/main/java/org/netbeans/html/presenters/browser/GrizzlyServer.java @@ -0,0 +1,207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import org.glassfish.grizzly.PortRange; +import org.glassfish.grizzly.http.io.InputBuffer; +import org.glassfish.grizzly.http.server.HttpHandler; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; +import org.netbeans.html.boot.spi.Fn; + +final class GrizzlyServer extends HttpServer<Request, Response, Object, GrizzlyServer.Context> { + private org.glassfish.grizzly.http.server.HttpServer server; + + @Override + void init(int from, int to) throws IOException { + server = org.glassfish.grizzly.http.server.HttpServer.createSimpleServer(null, new PortRange(from, to)); + } + + @Override + void shutdownNow() { + server.shutdownNow(); + } + + @Override + void addHttpHandler(Handler r, String mapping) { + server.getServerConfiguration().addHttpHandler(new HttpHandler() { + @Override + public void service(Request request, Response response) throws Exception { + r.service(GrizzlyServer.this, request, response); + } + }, mapping); + } + + @Override + int getPort() { + return server.getListeners().iterator().next().getPort(); + } + + @Override + void start() throws IOException { + server.start(); + } + + @Override + String getRequestURI(Request r) { + return r.getRequestURI(); + } + + @Override + String getServerName(Request r) { + return r.getServerName(); + } + + @Override + int getServerPort(Request r) { + return r.getServerPort(); + } + + @Override + String getParameter(Request r, String id) { + return r.getParameter(id); + } + + @Override + String getMethod(Request r) { + return r.getMethod().getMethodString(); + } + + @Override + String getBody(Request r) throws IOException { + final InputBuffer buffer = r.getInputBuffer(); + buffer.processingChars(); + buffer.fillFully(-1); + int len = buffer.availableChar(); + char[] arr = new char[len]; + int reallyRead = buffer.read(arr, 0, len); + assert reallyRead == len; + return new String(arr); + } + + @Override + String getHeader(Request r, String header) { + return r.getHeader(header); + } + + @Override + Writer getWriter(Response r) { + return r.getWriter(); + } + + @Override + void setContentType(Response r, String type) { + r.setContentType(type); + } + + @Override + void setStatus(Response r, int code) { + r.setStatus(code); + } + + @Override + OutputStream getOutputStream(Response r) { + return r.getOutputStream(); + } + + @Override + void suspend(Response r) { + r.suspend(); + } + + @Override + void resume(Response r, Runnable whenReady) { + whenReady.run(); + r.resume(); + } + + @Override + void setCharacterEncoding(Response r, String set) { + r.setCharacterEncoding(set); + } + + @Override + void addHeader(Response r, String name, String value) { + r.addHeader(name, value); + } + + @Override + <WebSocket> void send(WebSocket socket, String s) { + } + + class Context implements ThreadFactory { + private final String id; + Executor RUN; + Thread RUNNER; + + Context(String id) { + this.id = id; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Processor for " + id); + RUNNER = t; + return t; + } + } + + @Override + Context initializeRunner(String id) { + Context c = new Context(id); + c.RUN = Executors.newSingleThreadExecutor(c); + return c; + } + + @Override + final void runSafe(Context c, final Runnable r, final Fn.Presenter presenter) { + class Wrap implements Runnable { + @Override + public void run() { + if (presenter != null) { + try (Closeable c = Fn.activate(presenter)) { + r.run(); + } catch (IOException ex) { + // go on + } + } else { + r.run(); + } + } + } + if (c.RUNNER == Thread.currentThread()) { + if (presenter != null) { + Runnable w = new Wrap(); + w.run(); + } else { + r.run(); + } + } else { + Runnable w = new Wrap(); + c.RUN.execute(w); + } + } +} diff --git a/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java b/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java new file mode 100644 index 0000000..17aad38 --- /dev/null +++ b/browser/src/main/java/org/netbeans/html/presenters/browser/HttpServer.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import org.netbeans.html.boot.spi.Fn; + +abstract class HttpServer<Request, Response, WebSocket, Runner> { + abstract void init(int from, int to) throws IOException; + abstract void start() throws IOException; + abstract void shutdownNow(); + abstract void addHttpHandler(Handler h, String path); + abstract int getPort(); + + abstract String getRequestURI(Request r); + abstract String getServerName(Request r); + abstract int getServerPort(Request r); + abstract String getParameter(Request r, String id); + abstract String getMethod(Request r); + abstract String getBody(Request r) throws IOException; + abstract String getHeader(Request r, String substring); + + abstract Writer getWriter(Response r); + abstract void setContentType(Response r, String texthtml); + abstract void setStatus(Response r, int i); + abstract OutputStream getOutputStream(Response r); + abstract void suspend(Response r); + abstract void resume(Response r, Runnable runWhenResponseIsReady); + abstract void setCharacterEncoding(Response r, String utF8); + abstract void addHeader(Response r, String accessControlAllowOrigin, String string); + + abstract <WebSocket> void send(WebSocket socket, String s); + + abstract Runner initializeRunner(String id); + abstract void runSafe(Runner runner, Runnable code, Fn.Presenter presenter); + + static abstract class Handler { + abstract <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException; + } + + static abstract class WebSocketApplication { + abstract <WebSocket> void onMessage(HttpServer<?, ?, WebSocket, ?> server, WebSocket socket, String text); + } +} diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java index 6ca28b0..d20ee9c 100644 --- a/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/BrowserTest.java @@ -18,73 +18,19 @@ */ package org.netbeans.html.presenters.browser; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Executors; -import net.java.html.boot.BrowserBuilder; -import org.netbeans.html.boot.spi.Fn; import org.netbeans.html.json.tck.JavaScriptTCK; import org.netbeans.html.json.tck.KOTest; import org.testng.annotations.Factory; public class BrowserTest extends JavaScriptTCK { - private static Class<?> browserClass; - private static Fn.Presenter browserPresenter; - public BrowserTest() { } @Factory public static Object[] compatibilityTests() throws Exception { - final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser("BrowserTest", new Browser.Config())). - loadClass(BrowserTest.class). - loadPage("empty.html"). - invoke("initialized"); - - Executors.newSingleThreadExecutor().submit(new Runnable() { - @Override - public void run() { - bb.showAndWait(); - } - }); - - List<Object> res = new ArrayList<Object>(); - Class<? extends Annotation> test = - loadClass().getClassLoader().loadClass(KOTest.class.getName()). - asSubclass(Annotation.class); - - Class[] arr = (Class[]) loadClass().getDeclaredMethod("tests").invoke(null); - for (Class c : arr) { - for (Method m : c.getMethods()) { - if (m.getAnnotation(test) != null) { - res.add(new KOScript(browserPresenter, m)); - } - } - } + List<Object> res = new ArrayList<>(); + ServerFactories.collect("BrowserTest", res, KOTest.class, JavaScriptTCK::testClasses); return res.toArray(); } - - public static Class[] tests() { - return testClasses(); - } - - static synchronized Class<?> loadClass() throws InterruptedException { - while (browserClass == null) { - BrowserTest.class.wait(); - } - return browserClass; - } - - public static synchronized void ready(Class<?> browserCls) throws Exception { - browserClass = browserCls; - browserPresenter = Fn.activePresenter(); - BrowserTest.class.notifyAll(); - } - - public static void initialized() throws Exception { - Class<?> classpathClass = ClassLoader.getSystemClassLoader().loadClass(BrowserTest.class.getName()); - Method m = classpathClass.getMethod("ready", Class.class); - m.invoke(null, BrowserTest.class); - } } diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java b/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java index 0ebbed5..9ad1f18 100644 --- a/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/DynamicHTTP.java @@ -23,43 +23,33 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import org.glassfish.grizzly.http.server.HttpHandler; -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.grizzly.http.server.NetworkListener; -import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.http.server.Response; -import org.glassfish.grizzly.http.server.ServerConfiguration; -import org.glassfish.grizzly.websockets.WebSocket; -import org.glassfish.grizzly.websockets.WebSocketApplication; -import org.glassfish.grizzly.websockets.WebSocketEngine; +import org.netbeans.html.presenters.browser.HttpServer.Handler; +import org.netbeans.html.presenters.browser.HttpServer.WebSocketApplication; -final class DynamicHTTP extends HttpHandler { +final class DynamicHTTP extends Handler { private static final Logger LOG = Logger.getLogger(DynamicHTTP.class.getName()); private static int resourcesCount; - private List<Resource> resources = new ArrayList<Resource>(); - private final ServerConfiguration conf; + private final List<Resource> resources = new ArrayList<>(); private final HttpServer server; DynamicHTTP(HttpServer s) { server = s; - conf = s.getServerConfiguration(); } @Override - public void service(Request request, Response response) throws Exception { - if ("/dynamic".equals(request.getRequestURI())) { - String mimeType = request.getParameter("mimeType"); - List<String> params = new ArrayList<String>(); + public <Request, Response> void service(HttpServer<Request, Response, ?, ?> s, Request request, Response response) throws IOException { + if ("/dynamic".equals(s.getRequestURI(request))) { + String mimeType = s.getParameter(request, "mimeType"); + List<String> params = new ArrayList<>(); boolean webSocket = false; for (int i = 0;; i++) { - String p = request.getParameter("param" + i); + String p = s.getParameter(request, "param" + i); if (p == null) { break; } @@ -69,7 +59,7 @@ final class DynamicHTTP extends HttpHandler { } params.add(p); } - final String cnt = request.getParameter("content"); + final String cnt = s.getParameter(request, "content"); String mangle = cnt.replace("%20", " ").replace("%0A", "\n"); ByteArrayInputStream is = new ByteArrayInputStream(mangle.getBytes("UTF-8")); URI url; @@ -79,36 +69,27 @@ final class DynamicHTTP extends HttpHandler { } else { url = registerResource(res); } - response.getWriter().write(url.toString()); - response.getWriter().write("\n"); + server.getWriter(response).write(url.toString()); + server.getWriter(response).write("\n"); return; } for (Resource r : resources) { - if (r.httpPath.equals(request.getRequestURI())) { - response.setContentType(r.httpType); + if (r.httpPath.equals(s.getRequestURI(request))) { + server.setContentType(response, r.httpType); r.httpContent.reset(); String[] params = null; if (r.parameters.length != 0) { params = new String[r.parameters.length]; for (int i = 0; i < r.parameters.length; i++) { - params[i] = request.getParameter(r.parameters[i]); + params[i] = s.getParameter(request, r.parameters[i]); if (params[i] == null) { if ("http.method".equals(r.parameters[i])) { - params[i] = request.getMethod().toString(); + params[i] = s.getMethod(request); } else if ("http.requestBody".equals(r.parameters[i])) { - Reader rdr = request.getReader(); - StringBuilder sb = new StringBuilder(); - for (;;) { - int ch = rdr.read(); - if (ch == -1) { - break; - } - sb.append((char) ch); - } - params[i] = sb.toString(); + params[i] = s.getBody(request); } else if (r.parameters[i].startsWith("http.header.")) { - params[i] = request.getHeader(r.parameters[i].substring(12)); + params[i] = s.getHeader(request, r.parameters[i].substring(12)); } } if (params[i] == null) { @@ -117,27 +98,26 @@ final class DynamicHTTP extends HttpHandler { } } - copyStream(r.httpContent, response.getOutputStream(), null, params); + copyStream(r.httpContent, server.getOutputStream(response), null, params); } } } private URI registerWebSocket(Resource r) { - WebSocketEngine.getEngine().register("", r.httpPath, new WS(r)); + // WebSocketEngine.getEngine().register("", r.httpPath, new WS(r)); return pageURL("ws", server, r.httpPath); } private URI registerResource(Resource r) { if (!resources.contains(r)) { resources.add(r); - conf.addHttpHandler(this, r.httpPath); + server.addHttpHandler(this, r.httpPath); } return pageURL("http", server, r.httpPath); } private static URI pageURL(String proto, HttpServer server, final String page) { - NetworkListener listener = server.getListeners().iterator().next(); - int port = listener.getPort(); + int port = server.getPort(); try { return new URI(proto + "://localhost:" + port + page); } catch (URISyntaxException ex) { @@ -193,13 +173,13 @@ final class DynamicHTTP extends HttpHandler { } @Override - public void onMessage(WebSocket socket, String text) { + public <WebSocket> void onMessage(HttpServer<?,?, WebSocket, ?> server, WebSocket socket, String text) { try { r.httpContent.reset(); ByteArrayOutputStream out = new ByteArrayOutputStream(); copyStream(r.httpContent, out, null, text); String s = new String(out.toByteArray(), "UTF-8"); - socket.send(s); + server.send(socket, s); } catch (IOException ex) { LOG.log(Level.WARNING, "Error processing message " + text, ex); } diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java b/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java new file mode 100644 index 0000000..0d8b979 --- /dev/null +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/JavaScriptUtilities.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import net.java.html.js.JavaScriptBody; + +final class JavaScriptUtilities { + private JavaScriptUtilities() { + } + + @JavaScriptBody(args = { }, body = + "var h;" + + "if (!!window && !!window.location && !!window.location.href)\n" + + " h = window.location.href;\n" + + "else " + + " h = null;" + + "return h;\n" + ) + static native String findBaseURL(); + + @JavaScriptBody(args = {"value"}, body = "document.getElementById('loaded').innerHTML = value;") + static native void setLoaded(String value); + + @JavaScriptBody(args = {"ms"}, body = "window.setTimeout(function() { window.close(); }, ms);") + static native void closeSoon(int ms); + +} diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java b/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java index 937454d..e2f5217 100644 --- a/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/KOScript.java @@ -38,15 +38,19 @@ public final class KOScript implements ITest, IHookable, Runnable { private Object result; private Object inst; private int cnt; + private final String prefix; + private final Fn updateName; - KOScript(Fn.Presenter p, Method m) { + KOScript(Fn updateName, String prefix, Fn.Presenter p, Method m) { + this.updateName = updateName; + this.prefix = prefix; this.p = p; this.m = m; } @Override public String getTestName() { - return m.getDeclaringClass().getSimpleName() + "." + m.getName(); + return prefix + ":" + m.getDeclaringClass().getSimpleName() + "." + m.getName(); } @Test @@ -77,6 +81,9 @@ public final class KOScript implements ITest, IHookable, Runnable { public void run() { Closeable c = Fn.activate(p); try { + if (updateName != null) { + updateName.invoke(null, getTestName()); + } if (inst == null) { inst = m.getDeclaringClass().newInstance(); } diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java index 241aeee..b3c7f14 100644 --- a/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/KoBrowserTest.java @@ -18,36 +18,26 @@ */ package org.netbeans.html.presenters.browser; -import org.netbeans.html.presenters.browser.Browser; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.logging.ConsoleHandler; import java.util.logging.Level; -import java.util.logging.Logger; import net.java.html.BrwsrCtx; -import net.java.html.boot.BrowserBuilder; -import net.java.html.js.JavaScriptBody; import org.netbeans.html.boot.spi.Fn; import org.netbeans.html.context.spi.Contexts; import org.netbeans.html.json.spi.Technology; import org.netbeans.html.json.spi.Transfer; import org.netbeans.html.json.tck.KOTest; import org.netbeans.html.json.tck.KnockoutTCK; -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.grizzly.http.server.ServerConfiguration; import org.netbeans.html.ko4j.KO4J; import org.openide.util.lookup.ServiceProvider; import static org.testng.Assert.assertNotNull; @@ -55,106 +45,29 @@ import org.testng.annotations.Factory; @ServiceProvider(service = KnockoutTCK.class) public class KoBrowserTest extends KnockoutTCK { - private static final Logger LOG = Logger.getLogger(KoBrowserTest.class.getName()); - private static Class<?> browserClass; - private static Fn.Presenter browserPresenter; - public KoBrowserTest() { } - static Object[] showBrwsr(URI uri, String cmd) throws IOException { - LOG.log(Level.INFO, "Showing {0}", uri); - if (cmd == null) { - try { - LOG.log(Level.INFO, "Trying Desktop.browse on {0} {2} by {1}", new Object[]{ - System.getProperty("java.vm.name"), - System.getProperty("java.vm.vendor"), - System.getProperty("java.vm.version"),}); - java.awt.Desktop.getDesktop().browse(uri); - LOG.log(Level.INFO, "Desktop.browse successfully finished"); - return null; - } catch (UnsupportedOperationException ex) { - LOG.log(Level.INFO, "Desktop.browse not supported: {0}", ex.getMessage()); - LOG.log(Level.FINE, null, ex); - } - } - { - String cmdName = cmd == null ? "xdg-open" : cmd; - String[] cmdArr = { - cmdName, uri.toString() - }; - LOG.log(Level.INFO, "Launching {0}", Arrays.toString(cmdArr)); - final Process process = Runtime.getRuntime().exec(cmdArr); - return new Object[]{process, null}; - } - } - @Factory public static Object[] compatibilityTests() throws Exception { Browser.LOG.setLevel(Level.FINE); Browser.LOG.addHandler(new ConsoleHandler()); - - final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser("KoBrowserTest", new Browser.Config())). - loadClass(KoBrowserTest.class). - loadPage("empty.html"). - invoke("initialized"); - - Executors.newSingleThreadExecutor().submit(new Runnable() { - @Override - public void run() { - bb.showAndWait(); - } - }); - - List<Object> res = new ArrayList<Object>(); - Class<? extends Annotation> test = - loadClass().getClassLoader().loadClass(KOTest.class.getName()). - asSubclass(Annotation.class); - - Class[] arr = (Class[]) loadClass().getDeclaredMethod("tests").invoke(null); - final HttpServer s = Browser.findServer(browserPresenter); - ServerConfiguration conf = s.getServerConfiguration(); - conf.addHttpHandler(new DynamicHTTP(s), "/dynamic"); - for (Class c : arr) { - for (Method m : c.getMethods()) { - if (m.getAnnotation(test) != null) { - res.add(new KOScript(browserPresenter, m)); - } - } + List<Object> res = new ArrayList<>(); + Fn.Presenter[] all = ServerFactories.collect("KoBrowserTest", res, KOTest.class, KnockoutTCK::testClasses); + for (Fn.Presenter browserPresenter : all) { + final HttpServer s = Browser.findServer(browserPresenter); + s.addHttpHandler(new DynamicHTTP(s), "/dynamic"); } return res.toArray(); } - public static Class[] tests() { - return testClasses(); - } - - static synchronized Class<?> loadClass() throws InterruptedException { - while (browserClass == null) { - KoBrowserTest.class.wait(); - } - return browserClass; - } - - public static synchronized void ready(Class<?> browserCls) throws Exception { - browserClass = browserCls; - browserPresenter = Fn.activePresenter(); - KoBrowserTest.class.notifyAll(); - } - - public static void initialized() throws Exception { - browserPresenter = Fn.activePresenter(); - Class<?> classpathClass = ClassLoader.getSystemClassLoader().loadClass(KoBrowserTest.class.getName()); - Method m = classpathClass.getMethod("ready", Class.class); - m.invoke(null, KoBrowserTest.class); - } - @Override public BrwsrCtx createContext() { KO4J ko = new KO4J(); Contexts.Builder b = Contexts.newBuilder(); b.register(Technology.class, ko.knockout(), 7); b.register(Transfer.class, ko.transfer(), 7); + Fn.Presenter browserPresenter = Fn.activePresenter(); assertNotNull(browserPresenter, "Presenter needs to be registered"); b.register(Executor.class, (Executor)browserPresenter, 10); return b.build(); @@ -174,16 +87,13 @@ public class KoBrowserTest extends KnockoutTCK { return json; } - private Fn jsonFn; private Object putValue(Object json, String key, Object value) { - if (jsonFn == null) { - jsonFn = Fn.activePresenter().defineFn( - "if (json === null) json = new Object();" - + "if (key !== null) json[key] = value;" - + "return json;", - "json", "key", "value" - ); - } + Fn jsonFn = Fn.activePresenter().defineFn( + "if (json === null) json = new Object();" + + "if (key !== null) json[key] = value;" + + "return json;", + "json", "key", "value" + ); try { return jsonFn.invoke(null, json, key, value); } catch (Exception ex) { @@ -191,16 +101,13 @@ public class KoBrowserTest extends KnockoutTCK { } } - private Fn executeScript; @Override public Object executeScript(String script, Object[] arguments) { - if (executeScript == null) { - executeScript = Fn.activePresenter().defineFn( - "var f = new Function(s); " - + "return f.apply(null, args);", - "s", "args" - ); - } + Fn executeScript = Fn.activePresenter().defineFn( + "var f = new Function(s); " + + "return f.apply(null, args);", + "s", "args" + ); try { return executeScript.invoke(null, script, arguments); } catch (Exception ex) { @@ -208,20 +115,10 @@ public class KoBrowserTest extends KnockoutTCK { } } - @JavaScriptBody(args = { }, body = - "var h;" - + "if (!!window && !!window.location && !!window.location.href)\n" - + " h = window.location.href;\n" - + "else " - + " h = null;" - + "return h;\n" - ) - private static native String findBaseURL(); - @Override public URI prepareURL(String content, String mimeType, String[] parameters) { try { - final URL baseURL = new URL(findBaseURL()); + final URL baseURL = new URL(JavaScriptUtilities.findBaseURL()); StringBuilder sb = new StringBuilder(); sb.append("/dynamic?mimeType=").append(mimeType); for (int i = 0; i < parameters.length; i++) { @@ -236,9 +133,7 @@ public class KoBrowserTest extends KnockoutTCK { BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); URI connectTo = new URI(br.readLine()); return connectTo; - } catch (IOException ex) { - throw new IllegalStateException(ex); - } catch (URISyntaxException ex) { + } catch (IOException | URISyntaxException ex) { throw new IllegalStateException(ex); } } diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java new file mode 100644 index 0000000..4b38321 --- /dev/null +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerFactories.java @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.function.Supplier; +import net.java.html.boot.BrowserBuilder; +import org.netbeans.html.boot.spi.Fn; +import org.testng.ITest; +import org.testng.annotations.DataProvider; + +public final class ServerFactories { + private ServerFactories() { + } + + @DataProvider(name = "serverFactories") + public static Object[][] serverFactories() { + Supplier<HttpServer<?,?,?,?>> grizzly = GrizzlyServer::new; + List<Object[]> arr = new ArrayList<>(); + arr.add(new Object[] {"Default", null}); + return arr.toArray(new Object[0][]); + } + + static Fn.Presenter[] collect( + String browserName, Collection<? super ITest> res, + Class<? extends Annotation> test, Supplier<Class[]> tests + ) throws Exception { + final Object[][] factories = serverFactories(); + Fn.Presenter[] arr = new Fn.Presenter[factories.length]; + for (int i = 0; i < factories.length; i++) { + Object[] pair = factories[i]; + arr[i] = collect(browserName, (String) pair[0], (Supplier<HttpServer<?,?,?,?>>) pair[1], res, test, tests); + } + return arr; + } + + static Fn.Presenter collect( + String browserName, String prefix, Supplier<HttpServer<?,?,?,?>> serverProvider, + Collection<? super ITest> res, + Class<? extends Annotation> test, Supplier<Class[]> tests + ) throws Exception { + Fn.Presenter[] browserPresenter = { null }; + Fn[] updateName = { null }; + CountDownLatch cdl = new CountDownLatch(1); + final Browser.Config cfg = new Browser.Config().debug(true); + final BrowserBuilder bb = BrowserBuilder.newBrowser(new Browser(browserName, cfg, serverProvider)). + loadPage("empty.html"). + loadFinished(() -> { + browserPresenter[0] = Fn.activePresenter(); + updateName[0] = Fn.define(KOScript.class, + "document.getElementsByTagName('h1')[0].innerHTML='" + browserName + "@' + t;", + "t" + ); + cdl.countDown(); + }); + Executors.newSingleThreadExecutor().submit(bb::showAndWait); + cdl.await(); + Class[] arr = tests.get(); + for (Class c : arr) { + for (Method m : c.getMethods()) { + if (m.getAnnotation(test) != null) { + res.add(new KOScript(updateName[0], prefix, browserPresenter[0], m)); + } + } + } + return browserPresenter[0]; + } + +} diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java new file mode 100644 index 0000000..1170334 --- /dev/null +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerMimeTypeTest.java @@ -0,0 +1,102 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.function.Supplier; +import net.java.html.boot.BrowserBuilder; +import static org.netbeans.html.presenters.browser.JavaScriptUtilities.closeSoon; +import static org.netbeans.html.presenters.browser.JavaScriptUtilities.setLoaded; +import org.testng.Assert; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; +import org.testng.annotations.Test; + +public class ServerMimeTypeTest { + @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories") + public void checkMimeTypes(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws Exception { + final Thread main = Thread.currentThread(); + final int[] loaded = { 0 }; + + Browser server = new Browser( + "test", new Browser.Config().command("NONE"), serverProvider + ); + BrowserBuilder builder = BrowserBuilder.newBrowser(server) + .loadPage("server.html") + .loadFinished(() -> { + setLoaded("" + ++loaded[0]); + closeSoon(5000); + }); + builder.showAndWait(); + + int serverPort = server.server().getPort(); + URL connect = new URL("http://localhost:" + serverPort); + InputStream is = connect.openStream(); + Assert.assertNotNull(is, "Connection opened"); + byte[] arr = new byte[4096]; + int len = is.read(arr); + is.close(); + + final String page = new String(arr, 0, len, "UTF-8"); + assertTrue(page.contains("<h1>Server</h1>"), "Server page loaded OK:\n" + page); + + String cssType = new URL(connect, "test.css").openConnection().getContentType(); + assertMimeType(cssType, "text/css"); + + String jsType = new URL(connect, "test.js").openConnection().getContentType(); + assertMimeType(jsType, "*/javascript"); + + String jsMinType = new URL(connect, "test.min.js").openConnection().getContentType(); + assertMimeType(jsMinType, "*/javascript"); + + URLConnection conn = new URL(connect, "non-existing.file").openConnection(); + assertTrue(conn instanceof HttpURLConnection, "it is HTTP connection: " + conn); + + HttpURLConnection httpConn = (HttpURLConnection) conn; + assertEquals(httpConn.getResponseCode(), 404, "Expecting not exist status"); + + server.close(); + try { + HttpURLConnection url = (HttpURLConnection) connect.openConnection(); + url.setConnectTimeout(3000); + url.setReadTimeout(3000); + InputStream unavailable = url.getInputStream(); + fail("Stream can no longer be opened: " + unavailable); + } catch (IOException ex) { + // OK + } + } + + private void assertMimeType(String type, String exp) { + int semicolon = type.indexOf(';'); + if (semicolon >= 0) { + type = type.substring(0, semicolon); + } + if (exp.startsWith("*")) { + assertTrue(type.endsWith(exp.substring(1)), "Expecting " + exp + " but was: " + type); + } else { + assertEquals(type, exp); + } + } +} diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java index e3e5274..331c17c 100644 --- a/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/ServerTest.java @@ -33,7 +33,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import net.java.html.boot.BrowserBuilder; -import net.java.html.js.JavaScriptBody; +import static org.netbeans.html.presenters.browser.JavaScriptUtilities.closeSoon; +import static org.netbeans.html.presenters.browser.JavaScriptUtilities.setLoaded; import org.testng.Assert; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; @@ -109,12 +110,6 @@ public class ServerTest { } } - @JavaScriptBody(args = { "value" }, body = "document.getElementById('loaded').innerHTML = value;") - private static native void setLoaded(String value); - - @JavaScriptBody(args = { "ms" }, body = "window.setTimeout(function() { window.close(); }, ms);") - private static native void closeSoon(int ms); - private static void show(URI page) throws IOException { ExecutorService background = Executors.newSingleThreadExecutor(); Future<Void> future = background.submit((Callable<Void>) () -> { diff --git a/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java b/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java new file mode 100644 index 0000000..4dddb18 --- /dev/null +++ b/browser/src/test/java/org/netbeans/html/presenters/browser/SimpleServerTest.java @@ -0,0 +1,270 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.html.presenters.browser; + +import java.io.IOException; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import static org.testng.Assert.*; +import org.testng.annotations.Test; + +public class SimpleServerTest { + public SimpleServerTest() { + } + + @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories") + public void testConnectionToTheServer(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException { + if (serverProvider == null) { + return; + } + int min = 42343; + int max = 49343; + HttpServer<?, ?, ?, ?> server = serverProvider.get(); + server.init(min, max); + server.addHttpHandler(new HttpServer.Handler() { + @Override + <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException { + assertEquals(server.getServerName(rqst), "localhost", "Connecting from localhost"); + assertEquals(server.getServerPort(rqst), server.getPort(), "Connecting via local port"); + assertEquals(server.getMethod(rqst), "GET", "Requesting GET"); + + server.setCharacterEncoding(rspns, "UTF-8"); + server.setContentType(rspns, "text/x-test"); + Browser.cors(server, rspns); + try (Writer w = server.getWriter(rspns)) { + final String n = server.getParameter(rqst, "name"); + final String reply; + switch (server.getRequestURI(rqst)) { + case "/reply/hi": reply = "Ahoj " + n + "!"; break; + case "/reply/tchus": reply = "Ciao " + n + "!"; break; + default: reply = "What?"; + } + w.write(reply); + } + } + }, "/reply"); + server.start(); + + int realPort = server.getPort(); + assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort); + + final String baseUri = "http://localhost:" + realPort; + assertURL("Ahoj John!", baseUri, "/reply/hi?name=John"); + assertURL("Ciao John!", baseUri, "/reply/tchus?name=John"); + + server.shutdownNow(); + + } + + private static void assertURL(String msg, String baseUri, final String path) throws IOException, MalformedURLException { + URL url = new URL(baseUri + path); + URLConnection conn = url.openConnection(); + + final String contentAndAttribs = conn.getContentType(); + assertNotNull(contentAndAttribs, "Content-Type specified"); + int semicolon = contentAndAttribs.indexOf(';'); + final String content = semicolon == -1 ? contentAndAttribs : contentAndAttribs.substring(0, semicolon); + assertEquals(content, "text/x-test"); + + byte[] arr = new byte[8192]; + int len = conn.getInputStream().read(arr); + assertNotEquals(len, -1, "Something shall be read"); + + String txt = new String(arr, 0, len, StandardCharsets.UTF_8); + assertEquals(txt, msg, "Message from the handler delivered"); + + assertEquals(conn.getHeaderField("Access-Control-Allow-Origin"), "*"); + } + + @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories") + public void testHeadersAndBody(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException { + if (serverProvider == null) { + return; + } + int min = 42343; + int max = 49343; + HttpServer<?, ?, ?, ?> server = serverProvider.get(); + server.init(min, max); + server.addHttpHandler(new HttpServer.Handler() { + @Override + <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException { + StringBuilder sb = new StringBuilder(server.getBody(rqst)); + + server.setCharacterEncoding(rspns, "UTF-8"); + server.setContentType(rspns, "text/plain"); + try (Writer w = server.getWriter(rspns)) { + final String action = server.getHeader(rqst, "action"); + assertNotNull(action, "action is specified"); + String reply; + switch (action) { + case "reverse": reply = sb.reverse().toString(); break; + case "upper": reply = sb.toString().toUpperCase(); break; + default: reply = "What?"; + } + w.write(reply); + } + } + }, "/action"); + server.start(); + + int realPort = server.getPort(); + assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort); + + final String baseUri = "http://localhost:" + realPort; + assertReadURL("reverse", "Ahoj", baseUri, "johA"); + assertReadURL("upper", "Ahoj", baseUri, "AHOJ"); + + server.shutdownNow(); + + } + + private static void assertReadURL(String action, String data, String baseUri, final String exp) throws IOException, MalformedURLException { + URL url = new URL(baseUri + "/action"); + URLConnection conn = url.openConnection(); + conn.addRequestProperty("action", action); + conn.setDoOutput(true); + conn.connect(); + conn.getOutputStream().write(data.getBytes()); + + + final String contentAndAttribs = conn.getContentType(); + assertNotNull(contentAndAttribs, "Content-Type specified"); + int semicolon = contentAndAttribs.indexOf(';'); + final String content = semicolon == -1 ? contentAndAttribs : contentAndAttribs.substring(0, semicolon); + assertEquals(content, "text/plain"); + + byte[] arr = new byte[8192 * 8]; + int offset = 0; + for (;;) { + int len = conn.getInputStream().read(arr, offset, arr.length - offset); + if (len == -1) { + break; + } + offset += len; + } + assertNotEquals(offset, 0, "Something shall be read"); + + String txt = new String(arr, 0, offset, StandardCharsets.UTF_8); + assertEquals(txt, exp, "Message from the handler delivered"); + } + + @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories") + public void testWaitForData(String name, Supplier<HttpServer<?,?,?,?>> serverProvider) throws IOException { + if (serverProvider == null) { + return; + } + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + int min = 32343; + int max = 33343; + HttpServer<?, ?, ?, ?> server = serverProvider.get(); + server.init(min, max); + + class HandlerImpl extends HttpServer.Handler { + @Override + <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException { + server.setCharacterEncoding(rspns, "UTF-8"); + server.setContentType(rspns, "text/x-test"); + Browser.cors(server, rspns); + server.suspend(rspns); + exec.schedule((Callable <Void>) () -> { + server.resume(rspns, () -> { + Writer w = server.getWriter(rspns); + try { + w.write("Finished!"); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + return null; + }, 1, TimeUnit.SECONDS); + } + } + server.addHttpHandler(new HandlerImpl(), "/async"); + server.start(); + + int realPort = server.getPort(); + assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort); + + final String baseUri = "http://localhost:" + realPort; + assertURL("Finished!", baseUri, "/async"); + + exec.shutdown(); + server.shutdownNow(); + } + + @Test(dataProviderClass = ServerFactories.class, dataProvider = "serverFactories") + public void testEnormousBody(String name, Supplier<HttpServer<?,?,?, ?>> serverProvider) throws IOException { + if (serverProvider == null) { + return; + } + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + int min = 32343; + int max = 33343; + HttpServer<?, ?, ?, ?> server = serverProvider.get(); + server.init(min, max); + + String id = veryLongId(); + + class HandlerImpl extends HttpServer.Handler { + @Override + <Request, Response> void service(HttpServer<Request, Response, ?, ?> server, Request rqst, Response rspns) throws IOException { + server.setCharacterEncoding(rspns, "UTF-8"); + server.setContentType(rspns, "text/plain"); + Browser.cors(server, rspns); + + assertEquals("lower", server.getHeader(rqst, "action")); + + String gotId = server.getBody(rqst); + if (!gotId.equals(id)) { + fail("Id as expected by " + server + " isn't the same " + id.length() + " != " + gotId.length()); + } + server.getWriter(rspns).write(gotId.toLowerCase()); + } + } + server.addHttpHandler(new HandlerImpl(), "/action"); + server.start(); + + int realPort = server.getPort(); + assertTrue(realPort <= max && realPort >= min, "Port from range (" + min + ", " + max + ") selected: " + realPort); + + final String baseUri = "http://localhost:" + realPort; + assertReadURL("lower", id, baseUri, id.toLowerCase()); + + exec.shutdown(); + server.shutdownNow(); + } + + private static String veryLongId() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + final int max = 'Z' - 'A'; + int ch = 'A' + (i % max); + sb.append((char) ch); + } + return sb.toString(); + } +} diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css new file mode 100644 index 0000000..51da6c0 --- /dev/null +++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.css @@ -0,0 +1,18 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js new file mode 100644 index 0000000..51da6c0 --- /dev/null +++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.js @@ -0,0 +1,18 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ diff --git a/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js new file mode 100644 index 0000000..51da6c0 --- /dev/null +++ b/browser/src/test/resources/org/netbeans/html/presenters/browser/test.min.js @@ -0,0 +1,18 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected] For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists
