diff --git a/src/main/java/org/arl/fjage/Agent.java b/src/main/java/org/arl/fjage/Agent.java index 1f4e6b33..cd27eee3 100644 --- a/src/main/java/org/arl/fjage/Agent.java +++ b/src/main/java/org/arl/fjage/Agent.java @@ -10,6 +10,7 @@ package org.arl.fjage; +import java.io.File; import java.io.Serializable; import java.util.*; import java.util.TimerTask; @@ -20,7 +21,10 @@ import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; + +import org.arl.fjage.connectors.WebServer; import org.arl.fjage.persistence.Store; +import org.arl.fjage.remote.MasterContainer; import org.arl.fjage.remote.SlaveContainer; /** @@ -696,6 +700,42 @@ public Store getStore() { return Store.getInstance(this); } + /** + * Serve the Agent's store over HTTP. + * + * @param directoryListing true to enable directory listing, false otherwise + * @return true if successful, false otherwise + */ + public boolean enableServeStore(boolean directoryListing){ + WebServer webServer = getWebServer(); + if (webServer != null) { + String path = "store/" + this.getClass().getCanonicalName().replace(".", "/"); + if (webServer.hasContext(path)) return false; + webServer.add("/"+path, new File(path+"/"), new WebServer.WebServerOptions().directoryListed(directoryListing)); + return true; + } + log.warning("No web server found"); + return false; + } + + /** + * Stop serving the Agent's store over HTTP. + * + * @return true if successful, false otherwise + */ + public boolean disableServeStore(){ + WebServer webServer = getWebServer(); + if (webServer != null) { + String path = "store/" + this.getClass().getCanonicalName().replace(".", "/"); + if (!webServer.hasContext(path)) return false; + webServer.remove("/"+path); + return true; + } + log.warning("No web server found"); + return false; + } + + /** * Deep clones an object. This is typically used to explicitly clone a message for * modification when autocloning is not enabled. @@ -845,6 +885,33 @@ public final void run() { platform = null; } + /** + * Finds the web server that provides the websocket connection for the agent's container + * + * @return the web server that provides the websocket connection for the container + */ + WebServer getWebServer() { + if (container instanceof MasterContainer) { + // look for connector that starts with ws:// and get it's port + // ((MasterContainer) container).getConnectors() + for (String connector : ((MasterContainer) container).getConnectors()) { + if (connector.startsWith("ws://")) { + // Parse a string like this and find the port "ws://127.0.0.1:8080/ws" + String[] parts = connector.split(":"); + if (parts.length > 2) { + try { + int port = Integer.parseInt(parts[2].split("/")[0]); + return WebServer.getInstance(port); + } catch (NumberFormatException e) { + log.warning("Invalid port number in connector: "+connector); + } + } + } + } + } + return null; + } + private class InternalRequestSender implements RequestSender { diff --git a/src/main/java/org/arl/fjage/connectors/WebServer.java b/src/main/java/org/arl/fjage/connectors/WebServer.java index 834d90ec..d5d78558 100644 --- a/src/main/java/org/arl/fjage/connectors/WebServer.java +++ b/src/main/java/org/arl/fjage/connectors/WebServer.java @@ -37,10 +37,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * Web server instance manager. @@ -54,8 +51,8 @@ public class WebServer { //////// static attributes and methods - private static Map servers = new HashMap(); - private static java.util.logging.Logger log = java.util.logging.Logger.getLogger(WebServer.class.getName()); + private static final Map servers = new HashMap(); + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(WebServer.class.getName()); static { // disable Jetty logging (except warnings) @@ -257,9 +254,9 @@ public void remove(ContextHandler handler) { * * @param context context path. * @param resource resource path. - * @param cacheControl cache control header. + * @param options WebServerOptions object. */ - public void add(String context, String resource, String cacheControl) { + public void add(String context, String resource, WebServerOptions options) { if(resource.startsWith("/")) resource = resource.substring(1); ArrayList res = new ArrayList<>(); try { @@ -270,11 +267,12 @@ public void add(String context, String resource, String cacheControl) { for (URL r : res){ String staticWebResDir = r.toExternalForm(); ContextHandler handler = new ContextHandler(context); + if (options.directoryListed) log.warning("Directory listing is not supported for resources in jars"); ResourceHandler resHandler = new ResourceHandler(); resHandler.setResourceBase(staticWebResDir); resHandler.setWelcomeFiles(new String[]{ "index.html" }); resHandler.setDirectoriesListed(false); - resHandler.setCacheControl(cacheControl); + resHandler.setCacheControl(options.cacheControl); resHandler.setEtags(true); handler.setHandler(resHandler); staticContexts.put(context, handler); @@ -282,6 +280,17 @@ public void add(String context, String resource, String cacheControl) { } } + /** + * Adds a context to serve static documents. + * + * @param context context path. + * @param resource resource path. + * @param cacheControl cache control header. + */ + public void add(String context, String resource, String cacheControl) { + add(context, resource, new WebServerOptions().cacheControl(cacheControl)); + } + /** * Adds a context to serve static documents. * @@ -289,7 +298,7 @@ public void add(String context, String resource, String cacheControl) { * @param resource resource path. */ public void add(String context, String resource) { - add(context, resource, "public, max-age=31536000"); + add (context, resource, new WebServerOptions()); } /** @@ -297,19 +306,17 @@ public void add(String context, String resource) { * * @param context context path. * @param dir filesystem path of directory to serve files from. - * @param cacheControl cache control header. + * @param options WebServerOptions object. */ - public void add(String context, File dir, String cacheControl) { + public void add(String context, File dir, WebServerOptions options) { try { ContextHandler handler = new ContextHandler(context); - ResourceHandler resHandler = new ResourceHandler(); + ResourceHandler resHandler = options.directoryListed ? new DirectoryHandler() : new ResourceHandler(); resHandler.setResourceBase(dir.getCanonicalPath()); resHandler.setWelcomeFiles(new String[]{ "index.html" }); - resHandler.setDirectoriesListed(false); - resHandler.setCacheControl(cacheControl); + resHandler.setCacheControl(options.cacheControl); resHandler.setEtags(true); handler.setHandler(resHandler); - staticContexts.put(context, handler); add(handler); }catch (IOException ex){ log.warning("Unable to find the directory : " + dir.toString()); @@ -317,6 +324,17 @@ public void add(String context, File dir, String cacheControl) { } } + /** + * Adds a context to serve static documents. + * + * @param context context path. + * @param dir filesystem path of directory to serve files from. + * @param cacheControl cache control header. + */ + public void add(String context, File dir, String cacheControl) { + add (context, dir, new WebServerOptions().cacheControl(cacheControl)); + } + /** * Adds a context to serve static documents. * @@ -324,7 +342,7 @@ public void add(String context, File dir, String cacheControl) { * @param dir filesystem path of directory to serve files from. */ public void add(String context, File dir) { - add(context, dir, "public, max-age=31536000"); + add(context, dir, new WebServerOptions()); } /** @@ -434,4 +452,76 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques baseRequest.setHandled(true); } } + + /** + * Builder style class for configuring web server options. + */ + public static class WebServerOptions { + protected String cacheControl = CACHE; + protected boolean directoryListed = false; + + public WebServerOptions() {} + + public WebServerOptions cacheControl(String cacheControl) { + this.cacheControl = cacheControl; + return this; + } + + public WebServerOptions directoryListed(boolean directoryListed) { + this.directoryListed = directoryListed; + return this; + } + } + + /** + * Context handler for serving Directory listing as plain text + * instead of HTML. If the request is for a directory, and the content-type + * is text/plain, the directory listing is returned as plain text, else the default + * ResourceHandler is used. + */ + private static class DirectoryHandler extends ResourceHandler { + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + if (baseRequest.isHandled()) return; + File base = getBaseResource().getFile(); + if (base != null && target.endsWith("/")) { + if (request.getContentType() != null && request.getContentType().equals("text/plain")) { + String path = request.getPathInfo(); + if (path == null) path = "/"; + File dir = new File(base, path); + if (dir.isDirectory()) { + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_OK); + for (File f: dir.listFiles()) { + if (f.isHidden()) continue; + response.getWriter().println(f.getName()+" "+f.length()+" "+f.lastModified()); + } + baseRequest.setHandled(true); + return; + } + } else if (request.getContentType() != null && request.getContentType().equals("application/json")) { + String path = request.getPathInfo(); + if (path == null) path = "/"; + File dir = new File(getBaseResource().getFile(), path); + if (dir.isDirectory()) { + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().print("["); + boolean first = true; + for (File f: dir.listFiles()) { + if (f.isHidden()) continue; + if (!first) response.getWriter().print(","); + response.getWriter().print("{\"name\":\""+f.getName()+"\",\"size\":"+f.length()+",\"date\":"+f.lastModified()+"}"); + first = false; + } + response.getWriter().print("]"); + baseRequest.setHandled(true); + return; + } + } + } + super.handle(target, baseRequest, request, response); + } + } }