Abstracting the interface between Browser.Command and Grizzly HTTP server
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 @@
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.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.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 @@
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 @@
* @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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
+ " 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 @@
}
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 @@
}
@Override
- public Thread newThread(Runnable r) {
- Thread t = new Thread(r, "Processor for " + id);
- RUNNER = t;
- return t;
+ public final void execute(final Runnable r) {
+ server.runSafe(this.RUNNER, r, this.presenter);
}
- @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);
- }
- }
-
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 @@
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 @@
}
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 @@
}
return Level.FINE;
}
-
+
void log(int priority, String msg, Object... args) {
Level level = findLevel(priority);
@@ -668,12 +719,12 @@
}
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.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 @@
}
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 @@
} 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 @@
}
}
- 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 @@
}
@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 @@
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 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 @@
@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 @@
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 @@
}
}
- 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 @@
}
}
- @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 @@
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.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 @@
}
}
- @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.
+ */