From 0c17d083729b4e129e6bef59def53dad03d7bb80 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 2 Sep 2021 14:26:01 +0100 Subject: [PATCH] JAL-3851 re-engineering endpoints as separate classes --- src/jalview/rest/Endpoint.java | 86 +++++++++++++ src/jalview/rest/EndpointAsync.java | 185 +++++++++++++++++++++++++++ src/jalview/rest/FetchSequenceEndpoint.java | 48 +++++++ src/jalview/rest/OpenSequenceEndpoint.java | 164 ++++++++++++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 src/jalview/rest/Endpoint.java create mode 100644 src/jalview/rest/EndpointAsync.java create mode 100644 src/jalview/rest/FetchSequenceEndpoint.java create mode 100644 src/jalview/rest/OpenSequenceEndpoint.java diff --git a/src/jalview/rest/Endpoint.java b/src/jalview/rest/Endpoint.java new file mode 100644 index 0000000..5ee2320 --- /dev/null +++ b/src/jalview/rest/Endpoint.java @@ -0,0 +1,86 @@ +package jalview.rest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.bin.Cache; +import jalview.rest.RestHandler.EndpointI; + +public abstract class Endpoint implements EndpointI +{ + private String name = null; + + private API api = null; + + public void setName(String name) + { + this.name = name; + } + + public String getName() + { + return this.name; + } + + protected API getAPI() + { + return this.api; + } + + public abstract void processEndpoint(HttpServletRequest request, + HttpServletResponse response); + + /* + * Shared methods below here + */ + + protected String[] getEndpointPathParameters(HttpServletRequest request) + { + String pathInfo = request.getPathInfo(); + int slashpos = pathInfo.indexOf('/', 1); + return slashpos < 1 ? null + : pathInfo.substring(slashpos + 1).split("/"); + } + + protected void returnError(HttpServletRequest request, + HttpServletResponse response, String message) + { + response.setStatus(500); // set this to something better + String endpointName = getName(); + Cache.error(getAPI().getName() + " error: endpoint " + endpointName + + " failed: '" + message + "'"); + try + { + PrintWriter writer = response.getWriter(); + writer.write("Endpoint " + endpointName + ": " + message); + writer.close(); + } catch (IOException e) + { + Cache.debug(e); + } + } + + protected String getRequestBody(HttpServletRequest request) + throws IOException + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = request.getReader(); + try + { + String line; + while ((line = reader.readLine()) != null) + { + sb.append(line).append('\n'); + } + } finally + { + reader.close(); + } + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/jalview/rest/EndpointAsync.java b/src/jalview/rest/EndpointAsync.java new file mode 100644 index 0000000..511d723 --- /dev/null +++ b/src/jalview/rest/EndpointAsync.java @@ -0,0 +1,185 @@ +package jalview.rest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.CompletableFuture; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.bin.Cache; +import jalview.rest.RestHandler.Status; + +public abstract class EndpointAsync extends Endpoint +{ + private String name = null; + + private String idExtension = null; + + private String id = null; + + private CompletableFuture cf = null; + + protected void setCompletableFuture(CompletableFuture cf) + { + this.cf = cf; + } + + protected CompletableFuture getCompletableFuture() + { + return this.cf; + } + + protected void setId(String idExtension) + { + this.id = getName() + "::" + idExtension; + } + + protected String getId() + { + return this.id; + } + + /* + * Override the three methods + * initialise (get parameters, set id (extension), set cf if using an existing one) + * process (what to do in the cf if not using an existing one) + * finalise (extra stuff to do at the end of the first call to this) + */ + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + // should be overridden + // must setId(request, extension) + setId(request, null); + } + + protected abstract void process(HttpServletRequest request, + HttpServletResponse response); + + protected void finalise(HttpServletRequest request, + HttpServletResponse response) + { + // can be Overridden + } + + protected void passCompletableFuture() + { + // Override this if you want to use an existing CompletableFuture + } + + @Override + public void processEndpoint(HttpServletRequest request, + HttpServletResponse response) + { + initialise(request, response); + final String id = getId(); + + if (checkStatus(request, response)) + return; + + passCompletableFuture(); + if (getCompletableFuture() == null) + { + setCompletableFuture(CompletableFuture.runAsync(() -> { + this.process(request, response); + })); + } + finaliseCompletableFuture(); + + finalise(request, response); + + returnStatus(response); + changeStatus(Status.IN_PROGRESS); + } + + /* + * Shared methods below here + */ + + protected String setId(HttpServletRequest request, String extension) + { + String idString = request.getParameter("id"); + if (idString == null) + { + setId(extension); + idString = getId(); + } + return idString; + } + + protected void changeStatus(Status status) + { + String id = getId(); + // don't change a job's status if it has finished or died + if (API.getStatusMap().get(id) == Status.FINISHED + || API.getStatusMap().get(id) == Status.ERROR) + return; + API.getStatusMap().put(id, status); + } + + protected Status getStatus() + { + return API.getStatusMap().get(getId()); + } + + protected void returnStatus(HttpServletResponse response) + { + String id = getId(); + try + { + PrintWriter writer = response.getWriter(); + if (id != null) + writer.write("id=" + id + "\n"); + if (API.getRequestMap().get(id) != null) + writer.write( + "request=" + API.getRequestMap().get(id).toString() + "\n"); + if (API.getStatusMap().get(id) != null) + { + if (API.getStatusMap().get(id) == Status.ERROR) + response.setStatus(500); + writer.write( + "status=" + API.getStatusMap().get(id).toString() + "\n"); + } + } catch (IOException e) + { + Cache.debug(e); + } + } + + protected boolean checkStatus(HttpServletRequest request, + HttpServletResponse response) + { + String id = getId(); + Status status = API.getStatusMap().get(id); + if (status == null) + { + API.getStatusMap().put(id, Status.STARTED); + API.getRequestMap().put(id, request.getRequestURI()); + } + else + { + returnStatus(response); + return true; + } + return false; + } + + protected void finaliseCompletableFuture() + { + String id = getId(); + cf.whenComplete((Void, e) -> { + if (e != null) + { + Cache.error("Endpoint job " + id + " did not complete"); + Cache.debug(e); + changeStatus(Status.ERROR); + } + else + { + Cache.info("Endpoint job " + id + " completed successfully"); + changeStatus(Status.FINISHED); + } + }); + } +} diff --git a/src/jalview/rest/FetchSequenceEndpoint.java b/src/jalview/rest/FetchSequenceEndpoint.java new file mode 100644 index 0000000..ef102e7 --- /dev/null +++ b/src/jalview/rest/FetchSequenceEndpoint.java @@ -0,0 +1,48 @@ +package jalview.rest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.gui.Desktop; +import jalview.gui.SequenceFetcher; +import jalview.util.DBRefUtils; + +public class FetchSequenceEndpoint extends EndpointAsync +{ + protected String name = "fetchsequence"; + + @Override + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + // note that endpointName should always be "fetchsequence" + + String[] parameters = getEndpointPathParameters(request); + + // check we can run fetchsequence + if (parameters.length < 2) + { + returnError(request, response, + "requires 2 path parameters: dbname, ids"); + return; + } + + String dbName = parameters[0]; + String dbId = parameters[1]; + + setId(request, dbName + "::" + dbId); + if (checkStatus(request, response)) + return; + + String db = DBRefUtils.getCanonicalName(dbName); + Desktop desktop = Desktop.instance; + SequenceFetcher sf = new SequenceFetcher(desktop, db, dbId); + setCompletableFuture(sf.ok_actionPerformed(true)); + } + + protected void process(HttpServletRequest request, + HttpServletResponse response) + { + // all the work being done by the SequenceFetcher! + } +} diff --git a/src/jalview/rest/OpenSequenceEndpoint.java b/src/jalview/rest/OpenSequenceEndpoint.java new file mode 100644 index 0000000..52671ac --- /dev/null +++ b/src/jalview/rest/OpenSequenceEndpoint.java @@ -0,0 +1,164 @@ +package jalview.rest; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.DatatypeConverter; + +import jalview.bin.Cache; +import jalview.gui.CutAndPasteTransfer; +import jalview.gui.Desktop; + +public class OpenSequenceEndpoint extends EndpointAsync +{ + protected String name = "opensequence"; + + private String fileString; + + private String urlString; + + private String method; + + private String dataString; + + private String body; + + private String content; + + private boolean post; + + private boolean data; + + String access = null; + + String ref = null; + + @Override + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + fileString = request.getParameter("file"); + urlString = request.getParameter("url"); + method = request.getMethod().toLowerCase(); + dataString = request.getParameter("data"); + body = null; + post = method.equalsIgnoreCase("post"); + data = dataString != null; + + if (post) + { + access = "post"; + try + { + body = getRequestBody(request); + } catch (IOException e) + { + returnError(request, response, "could not read POST body"); + Cache.debug(e); + return; + } + // for ref see md5 later + } + else if (data) + { + access = "data"; + // for ref see md5 later + } + else if (fileString != null) + { + access = "file"; + ref = fileString; + } + else if (urlString != null) + { + access = "url"; + ref = urlString; + } + + if (access == null) + { + returnError(request, response, + "requires POST body or one of parameters 'data', 'file' or 'url'"); + return; + } + + // final content used in Future + final String content; + if (post || data) + { + content = post ? body : dataString; + try + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(content.getBytes()); + byte[] digest = md5.digest(); + ref = DatatypeConverter.printBase64Binary(digest).toLowerCase(); + } catch (NoSuchAlgorithmException e) + { + Cache.debug(e); + } + } + else + { + content = null; + } + + setId(request, access + "::" + ref); + } + + protected void process(HttpServletRequest request, + HttpServletResponse response) + { + if (post || data) + { + // Sequence file contents being posted + // use File -> Input Alignment -> from Textbox + CutAndPasteTransfer cap = new CutAndPasteTransfer(); + cap.setText(content); + cap.ok_actionPerformed(null); + cap.cancel_actionPerformed(null); + } + else if (fileString != null) + { + // Sequence file on filesystem + // use File -> Input Alignment -> From File + URL url = null; + File file = null; + try + { + url = new URL(fileString); + file = new File(url.toURI()); + } catch (MalformedURLException | URISyntaxException e) + { + returnError(request, response, + "could not resolve file='" + fileString + "'"); + Cache.debug(e); + return; + } + if (!file.exists()) + { + returnError(request, response, + "file='" + fileString + "' does not exist"); + return; + } + Desktop.instance.openFile(file, null, null); + } + else if (urlString != null) + { + boolean success = Desktop.instance.loadUrl(urlString, null); + if (!success) + { + returnError(request, response, + "url='" + urlString + "' could not be opened"); + return; + } + } + } +} -- 1.7.10.2