From f5ab749a02896c2d1d552653a11ab8d538000954 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Thu, 2 Sep 2021 14:25:15 +0100 Subject: [PATCH] JAL-3851 API handling of rest calls --- src/jalview/bin/Jalview.java | 33 +++ src/jalview/gui/Desktop.java | 153 ++++++------ src/jalview/gui/SequenceFetcher.java | 64 ++--- src/jalview/httpserver/HttpServer.java | 54 ++++- src/jalview/rest/API.java | 402 ++++++++++++++++++++++++++++++++ src/jalview/rest/RestHandler.java | 184 +++++++++++++-- 6 files changed, 768 insertions(+), 122 deletions(-) create mode 100644 src/jalview/rest/API.java diff --git a/src/jalview/bin/Jalview.java b/src/jalview/bin/Jalview.java index 4c21624..3810d3f 100755 --- a/src/jalview/bin/Jalview.java +++ b/src/jalview/bin/Jalview.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.net.BindException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -54,6 +55,7 @@ import jalview.ext.so.SequenceOntology; import jalview.gui.AlignFrame; import jalview.gui.Desktop; import jalview.gui.PromptUserConfig; +import jalview.httpserver.HttpServer; import jalview.io.AppletFormatAdapter; import jalview.io.BioJsHTMLOutput; import jalview.io.DataSourceType; @@ -66,6 +68,7 @@ import jalview.io.HtmlSvgOutput; import jalview.io.IdentifyFile; import jalview.io.NewickFile; import jalview.io.gff.SequenceOntologyFactory; +import jalview.rest.API; import jalview.schemes.ColourSchemeI; import jalview.schemes.ColourSchemeProperty; import jalview.util.ChannelProperties; @@ -475,6 +478,36 @@ public class Jalview } } + // set the jetty port if suggested + String sPort = aparser.getValue("serverport"); + if (sPort != null) + { + int port = 0; + try + { + port = Integer.parseInt(sPort); + HttpServer.setSuggestedPort(port); + Cache.info("Set suggested server port to " + port); + } catch (NumberFormatException e) + { + Cache.warn("server_port '" + sPort + "' not parseable as Integer"); + } + } + // Start a TestListener + if (aparser.contains("genomeapi")) + { + try + { + API gb = API.getInstance(); + Cache.info(gb.getName() + " started at " + + HttpServer.getInstance().getUri().toString()); + } catch (BindException e) + { + Cache.warn("Could not open a genomeapi"); + Cache.error(e); + } + } + // Move any new getdown-launcher-new.jar into place over old // getdown-launcher.jar String appdirString = System.getProperty("getdownappdir"); diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 195a313..b50ff07 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -398,16 +398,17 @@ public class Desktop extends jalview.jbgui.GDesktop + Cache.getProperty("VERSION")); /** - * Set taskbar "grouped windows" name for linux desktops (works in GNOME and KDE). - * This uses sun.awt.X11.XToolkit.awtAppClassName which is not officially documented or - * guaranteed to exist, so we access it via reflection. - * There appear to be unfathomable criteria about what this string can contain, and it if doesn't - * meet those criteria then "java" (KDE) or "jalview-bin-Jalview" (GNOME) is used. - * "Jalview", "Jalview Develop" and "Jalview Test" seem okay, but "Jalview non-release" does not. - * The reflection access may generate a warning: - * WARNING: An illegal reflective access operation has occurred - * WARNING: Illegal reflective access by jalview.gui.Desktop () to field sun.awt.X11.XToolkit.awtAppClassName - * which I don't think can be avoided. + * Set taskbar "grouped windows" name for linux desktops (works in GNOME and + * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not + * officially documented or guaranteed to exist, so we access it via + * reflection. There appear to be unfathomable criteria about what this + * string can contain, and it if doesn't meet those criteria then "java" + * (KDE) or "jalview-bin-Jalview" (GNOME) is used. "Jalview", "Jalview + * Develop" and "Jalview Test" seem okay, but "Jalview non-release" does + * not. The reflection access may generate a warning: WARNING: An illegal + * reflective access operation has occurred WARNING: Illegal reflective + * access by jalview.gui.Desktop () to field + * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided. */ if (Platform.isLinux()) { @@ -417,7 +418,8 @@ public class Desktop extends jalview.jbgui.GDesktop Field[] declaredFields = xToolkit.getClass().getDeclaredFields(); Field awtAppClassNameField = null; - if (Arrays.stream(declaredFields).anyMatch(f -> f.getName().equals("awtAppClassName"))) + if (Arrays.stream(declaredFields) + .anyMatch(f -> f.getName().equals("awtAppClassName"))) { awtAppClassNameField = xToolkit.getClass() .getDeclaredField("awtAppClassName"); @@ -441,8 +443,9 @@ public class Desktop extends jalview.jbgui.GDesktop } /** - * APQHandlers sets handlers for About, Preferences and Quit actions peculiar to macOS's application menu. - * APQHandlers will check to see if a handler is supported before setting it. + * APQHandlers sets handlers for About, Preferences and Quit actions + * peculiar to macOS's application menu. APQHandlers will check to see if a + * handler is supported before setting it. */ try { @@ -1187,33 +1190,38 @@ public class Desktop extends jalview.jbgui.GDesktop { File selectedFile = chooser.getSelectedFile(); Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent()); - FileFormatI format = chooser.getSelectedFormat(); - - /* - * Call IdentifyFile to verify the file contains what its extension implies. - * Skip this step for dynamically added file formats, because - * IdentifyFile does not know how to recognise them. - */ - if (FileFormats.getInstance().isIdentifiable(format)) - { - try - { - format = new IdentifyFile().identify(selectedFile, - DataSourceType.FILE); - } catch (FileFormatException e) - { - // format = null; //?? - } - } - - new FileLoader().LoadFile(viewport, selectedFile, - DataSourceType.FILE, format); + openFile(selectedFile, format, viewport); } }); chooser.showOpenDialog(this); } + public void openFile(File selectedFile, FileFormatI format, + AlignViewport viewport) + { + + /* + * Call IdentifyFile to verify the file contains what its extension implies. + * Skip this step for dynamically added file formats, because + * IdentifyFile does not know how to recognise them. + */ + if (FileFormats.getInstance().isIdentifiable(format)) + { + try + { + format = new IdentifyFile().identify(selectedFile, + DataSourceType.FILE); + } catch (FileFormatException e) + { + // format = null; //?? + } + } + + new FileLoader().LoadFile(viewport, selectedFile, DataSourceType.FILE, + format); + } + /** * Shows a dialog for input of a URL at which to retrieve alignment data * @@ -1277,51 +1285,14 @@ public class Desktop extends jalview.jbgui.GDesktop : ((JComboBox) history).getEditor().getItem() .toString().trim()); - if (url.toLowerCase().endsWith(".jar")) - { - if (viewport != null) - { - new FileLoader().LoadFile(viewport, url, DataSourceType.URL, - FileFormat.Jalview); - } - else - { - new FileLoader().LoadFile(url, DataSourceType.URL, - FileFormat.Jalview); - } - } - else + if (!loadUrl(url, viewport)) { - FileFormatI format = null; - try - { - format = new IdentifyFile().identify(url, DataSourceType.URL); - } catch (FileFormatException e) - { - // TODO revise error handling, distinguish between - // URL not found and response not valid - } - - if (format == null) - { - String msg = MessageManager - .formatMessage("label.couldnt_locate", url); - JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg, - MessageManager.getString("label.url_not_found"), - JvOptionPane.WARNING_MESSAGE); - - return; - } - if (viewport != null) - { - new FileLoader().LoadFile(viewport, url, DataSourceType.URL, - format); - } - else - { - new FileLoader().LoadFile(url, DataSourceType.URL, format); - } + String msg = MessageManager.formatMessage("label.couldnt_locate", + url); + JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg, + MessageManager.getString("label.url_not_found"), + JvOptionPane.WARNING_MESSAGE); } } }; @@ -1334,6 +1305,34 @@ public class Desktop extends jalview.jbgui.GDesktop MessageManager.getString("action.ok")); } + public boolean loadUrl(String url, AlignViewport viewport) + { + FileFormatI format = null; + try + { + format = new IdentifyFile().identify(url, DataSourceType.URL); + } catch (FileFormatException e) + { + // TODO revise error handling, distinguish between + // URL not found and response not valid + } + + if (format == null) + { + return false; + } + + if (viewport != null) + { + new FileLoader().LoadFile(viewport, url, DataSourceType.URL, format); + } + else + { + new FileLoader().LoadFile(url, DataSourceType.URL, format); + } + return true; + } + /** * Opens the CutAndPaste window for the user to paste an alignment in to * diff --git a/src/jalview/gui/SequenceFetcher.java b/src/jalview/gui/SequenceFetcher.java index 8b5d3b7..bba5ac0 100755 --- a/src/jalview/gui/SequenceFetcher.java +++ b/src/jalview/gui/SequenceFetcher.java @@ -20,21 +20,6 @@ */ package jalview.gui; -import jalview.api.FeatureSettingsModelI; -import jalview.bin.Cache; -import jalview.datamodel.AlignmentI; -import jalview.datamodel.DBRefEntry; -import jalview.datamodel.SequenceI; -import jalview.fts.core.GFTSPanel; -import jalview.fts.service.pdb.PDBFTSPanel; -import jalview.fts.service.uniprot.UniprotFTSPanel; -import jalview.io.FileFormatI; -import jalview.io.gff.SequenceOntologyI; -import jalview.util.DBRefUtils; -import jalview.util.MessageManager; -import jalview.util.Platform; -import jalview.ws.seqfetcher.DbSourceProxy; - import java.awt.BorderLayout; import java.awt.Font; import java.awt.event.ActionEvent; @@ -46,6 +31,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.concurrent.CompletableFuture; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -57,6 +43,21 @@ import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.SwingConstants; +import jalview.api.FeatureSettingsModelI; +import jalview.bin.Cache; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.DBRefEntry; +import jalview.datamodel.SequenceI; +import jalview.fts.core.GFTSPanel; +import jalview.fts.service.pdb.PDBFTSPanel; +import jalview.fts.service.uniprot.UniprotFTSPanel; +import jalview.io.FileFormatI; +import jalview.io.gff.SequenceOntologyI; +import jalview.util.DBRefUtils; +import jalview.util.MessageManager; +import jalview.util.Platform; +import jalview.ws.seqfetcher.DbSourceProxy; + /** * A panel where the use may choose a database source, and enter one or more * accessions, to retrieve entries from the database. @@ -145,8 +146,8 @@ public class SequenceFetcher extends JPanel implements Runnable frame = new JInternalFrame(); frame.setContentPane(this); - Desktop.addInternalFrame(frame, getFrameTitle(), true, 400, - Platform.isAMacAndNotJS() ? 240 : 180); + Desktop.addInternalFrame(frame, getFrameTitle(), true, 400, + Platform.isAMacAndNotJS() ? 240 : 180); } private String getFrameTitle() @@ -414,6 +415,11 @@ public class SequenceFetcher extends JPanel implements Runnable */ public void ok_actionPerformed() { + ok_actionPerformed(false); + } + + public CompletableFuture ok_actionPerformed(boolean returnFuture) + { /* * tidy inputs and check there is something to search for */ @@ -424,9 +430,9 @@ public class SequenceFetcher extends JPanel implements Runnable text = text.replace(",", ";"); } text = text.replaceAll("(\\s|[; ])+", ";"); - if (!t0.equals(text)) + if (!t0.equals(text)) { - textArea.setText(text); + textArea.setText(text); } if (text.isEmpty()) { @@ -434,14 +440,14 @@ public class SequenceFetcher extends JPanel implements Runnable showErrorMessage( "Please enter a (semi-colon separated list of) database id(s)"); resetDialog(); - return; + return null; } if (database.getSelectedIndex() == 0) { // todo i18n showErrorMessage("Please choose a database"); resetDialog(); - return; + return null; } exampleBtn.setEnabled(false); @@ -450,8 +456,10 @@ public class SequenceFetcher extends JPanel implements Runnable closeBtn.setEnabled(false); backBtn.setEnabled(false); - Thread worker = new Thread(this); - worker.start(); + CompletableFuture worker = CompletableFuture + .runAsync(new Thread(this)); + + return returnFuture ? worker : null; } private void resetDialog() @@ -761,12 +769,12 @@ public class SequenceFetcher extends JPanel implements Runnable for (String q : queries) { - // BH 2019.01.25 dbr is never used. -// DBRefEntry dbr = new DBRefEntry(); -// dbr.setSource(proxy.getDbSource()); -// dbr.setVersion(null); + // BH 2019.01.25 dbr is never used. + // DBRefEntry dbr = new DBRefEntry(); + // dbr.setSource(proxy.getDbSource()); + // dbr.setVersion(null); String accId = proxy.getAccessionIdFromQuery(q); -// dbr.setAccessionId(accId); + // dbr.setAccessionId(accId); boolean rfound = false; for (int r = 0, nr = rs.length; r < nr; r++) { diff --git a/src/jalview/httpserver/HttpServer.java b/src/jalview/httpserver/HttpServer.java index a18d38d..397f1a8 100644 --- a/src/jalview/httpserver/HttpServer.java +++ b/src/jalview/httpserver/HttpServer.java @@ -20,8 +20,6 @@ */ package jalview.httpserver; -import jalview.rest.RestHandler; - import java.net.BindException; import java.net.URI; import java.util.Collections; @@ -31,6 +29,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -38,6 +37,8 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import jalview.rest.RestHandler; + /** * An HttpServer built on Jetty. To use it *
    @@ -82,6 +83,14 @@ public class HttpServer */ private URI contextRoot; + /* + * The port of the server. This can be set before starting the instance + * as a suggested port to use (it is not guaranteed). + * The value will be set to the actual port being used after the instance + * is started. + */ + private static int PORT = 0; + /** * Returns the singleton instance of this class. * @@ -126,8 +135,9 @@ public class HttpServer try { /* - * Create a server with a small number of threads; jetty will allocate a - * free port + * Create a server with a small number of threads; + * If PORT has been set then jetty will try and use this, otherwise + * jetty will allocate a free port */ QueuedThreadPool tp = new QueuedThreadPool(4, 1); // max, min server = new Server(tp); @@ -135,6 +145,10 @@ public class HttpServer ServerConnector connector = new ServerConnector(server, 0, 2); // restrict to localhost connector.setHost("localhost"); + if (PORT > 0) + { + connector.setPort(PORT); + } server.addConnector(connector); /* @@ -150,6 +164,15 @@ public class HttpServer contextHandlers = new HandlerCollection(true); server.setHandler(contextHandlers); server.start(); + Connector[] cs = server.getConnectors(); + if (cs.length > 0) + { + if (cs[0] instanceof ServerConnector) + { + ServerConnector c = (ServerConnector) cs[0]; + PORT = c.getPort(); + } + } // System.out.println(String.format( // "HttpServer started with %d threads", server.getThreadPool() // .getThreads())); @@ -297,4 +320,27 @@ public class HttpServer + " handler on " + handler.getUri()); } } + + /** + * This sets the "suggested" port to use. It can only be called once before + * starting the HttpServer instance. After the server has actually started the + * port is set to the actual port being used and cannot be changed. + * + * @param port + * @return successful change + */ + public static boolean setSuggestedPort(int port) + { + if (port < 1 || PORT > 0) + { + return false; + } + PORT = port; + return true; + } + + public static int getPort() + { + return PORT; + } } diff --git a/src/jalview/rest/API.java b/src/jalview/rest/API.java new file mode 100644 index 0000000..aada285 --- /dev/null +++ b/src/jalview/rest/API.java @@ -0,0 +1,402 @@ +package jalview.rest; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.BindException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +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; +import jalview.gui.SequenceFetcher; +import jalview.util.DBRefUtils; + +public class API extends RestHandler +{ + private static final String MY_PATH = "api"; + + private static final String MY_NAME = "Jalview API"; + + private static Map statusMap = new HashMap<>(); + + private static Map requestMap = new HashMap<>(); + + private static API instance = null; + + public static API getInstance() throws BindException + { + synchronized (API.class) + { + if (instance == null) + { + instance = new API(); + } + } + return instance; + } + + private API() throws BindException + { + super(); + } + + private boolean init = false; + + @Override + protected void init() throws BindException + { + if (init) + return; + super.init(MY_PATH); + + // add endpoints here + addEndpoint("fetchsequence", (String s, HttpServletRequest rq, + HttpServletResponse rs) -> fetchSequence(s, rq, rs)); + addEndpoint("opensequence", (String s, HttpServletRequest rq, + HttpServletResponse rs) -> openSequence(s, rq, rs)); + addEndpoint("highlight", (String s, HttpServletRequest rq, + HttpServletResponse rs) -> highlight(s, rq, rs)); + + setPath(MY_PATH); + this.registerHandler(); + + init = true; + } + + // for the "fetchsequence" endpoint + private void fetchSequence(String endpointName, + 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]; + + // final for async later + final String id = getId(request, endpointName, dbName + "::" + dbId); + if (checkStatus(request, response, id)) + return; + + String db = DBRefUtils.getCanonicalName(dbName); + Desktop desktop = Desktop.instance; + SequenceFetcher sf = new SequenceFetcher(desktop, db, dbId); + CompletableFuture cf = sf.ok_actionPerformed(true); + + setFuture(cf, id); + returnStatus(response, id); + changeStatus(id, Status.IN_PROGRESS); + } + + // for the "opensequence" endpoint + private void openSequence(String endpointName, HttpServletRequest request, + HttpServletResponse response) + { + // note that endpointName should always be "opensequence" + String fileString = request.getParameter("file"); + String urlString = request.getParameter("url"); + String method = request.getMethod().toLowerCase(); + String dataString = request.getParameter("data"); + String body = null; + boolean post = method.equalsIgnoreCase("post"); + boolean data = dataString != null; + + String access = null; + String ref = 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; + } + + // final for async later + final String id = getId(request, endpointName, access + "::" + ref); + if (checkStatus(request, response, id)) + return; + + CompletableFuture cf = CompletableFuture.runAsync(() -> { + 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; + } + } + }); + + setFuture(cf, id); + returnStatus(response, id); + changeStatus(id, Status.IN_PROGRESS); + } + + // for the "highlight" endpoint + private void highlight(String endpointName, HttpServletRequest request, + HttpServletResponse response) + { + String[] parameters = getEndpointPathParameters(request); + + // check we can run highlight + if (parameters.length < 1) + { + returnError(request, response, "requires 1 path parameters: ranges"); + return; + } + + String rangesString = parameters[0]; + String[] rangeStrings = rangesString.split(","); + int[][] ranges = new int[rangeStrings.length][2]; + for (int i = 0; i < rangeStrings.length; i++) + { + String range = rangeStrings[i]; + try + { + int hyphenpos = range.indexOf('-'); + if (hyphenpos < 0) + { + ranges[i][0] = Integer.parseInt(range); + ranges[i][1] = ranges[i][0]; + } + else + { + ranges[i][0] = Integer.parseInt(range.substring(0, hyphenpos)); + ranges[i][1] = Integer.parseInt(range.substring(hyphenpos)); + } + } catch (NumberFormatException nfe) + { + returnError(request, response, + "couldn't parse ranges component '" + range + "'"); + return; + } + } + + } + + // template endpoint + private void templateEndpoint(String endpointName, + HttpServletRequest request, HttpServletResponse response) + { + // final for async later + final String id = getId(request, endpointName, + "some identifier for this endpoint request"); + if (checkStatus(request, response, id)) + return; + + CompletableFuture cf = CompletableFuture.runAsync(() -> { + // do stuff + }); + + setFuture(cf, id); + returnStatus(response, id); + changeStatus(id, Status.IN_PROGRESS); + } + + /* + * Shared methods below here + */ + + @Override + public String getPath() + { + return MY_PATH; + } + + private void changeStatus(String id, Status status) + { + // don't change a job's status if it has finished or died + if (statusMap.get(id) == Status.FINISHED + || statusMap.get(id) == Status.ERROR) + return; + statusMap.put(id, status); + } + + private Status getStatus(String id) + { + return statusMap.get(id); + } + + protected void returnStatus(HttpServletResponse response, String id) + { + try + { + PrintWriter writer = response.getWriter(); + if (id != null) + writer.write("id=" + id + "\n"); + if (requestMap.get(id) != null) + writer.write("request=" + requestMap.get(id).toString() + "\n"); + if (statusMap.get(id) != null) + { + if (statusMap.get(id) == Status.ERROR) + response.setStatus(500); + writer.write("status=" + statusMap.get(id).toString() + "\n"); + } + } catch (IOException e) + { + Cache.debug(e); + } + } + + private String getId(HttpServletRequest request, String endpointName, + String extra) + { + String idString = request.getParameter("id"); + if (idString == null) + { + idString = endpointName + "::" + extra; + } + return idString; + } + + private boolean checkStatus(HttpServletRequest request, + HttpServletResponse response, String id) + { + Status status = statusMap.get(id); + if (status == null) + { + statusMap.put(id, Status.STARTED); + requestMap.put(id, request.getRequestURI()); + } + else + { + returnStatus(response, id); + return true; + } + return false; + } + + private void setFuture(CompletableFuture cf, String id) + { + cf.whenComplete((Void, e) -> { + if (e != null) + { + Cache.error("API job " + id + " did not complete"); + Cache.debug(e); + changeStatus(id, Status.ERROR); + } + else + { + Cache.info("API job " + id + " completed successfully"); + changeStatus(id, Status.FINISHED); + } + }); + } + + protected static Map getStatusMap() + { + return statusMap; + } + + protected static Map getRequestMap() + { + return requestMap; + } +} diff --git a/src/jalview/rest/RestHandler.java b/src/jalview/rest/RestHandler.java index a37882f..9c103c4 100644 --- a/src/jalview/rest/RestHandler.java +++ b/src/jalview/rest/RestHandler.java @@ -20,24 +20,57 @@ */ package jalview.rest; -import jalview.httpserver.AbstractRequestHandler; - +import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.net.BindException; +import java.util.HashMap; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import jalview.bin.Cache; +import jalview.httpserver.AbstractRequestHandler; + /** * A simple handler to process (or delegate) HTTP requests on /jalview/rest */ public class RestHandler extends AbstractRequestHandler { + + public enum Status + { + STARTED, IN_PROGRESS, FINISHED, ERROR + } + + public interface EndpointI + { + public void setName(String name); + + public String getName(); + + public void processEndpoint(HttpServletRequest request, + HttpServletResponse response); + } + + public interface Endpoint + { + public void processEndpoint(String endpointName, + HttpServletRequest request, HttpServletResponse response); + } + private static final String MY_PATH = "rest"; private static final String MY_NAME = "Rest"; + private String missingEndpointMessage = null; + + private boolean init = false; + + // map of method names and method handlers + private Map endpoints = null; + /** * Singleton instance of this class */ @@ -66,9 +99,9 @@ public class RestHandler extends AbstractRequestHandler * * @throws BindException */ - private RestHandler() throws BindException + protected RestHandler() throws BindException { - setPath(MY_PATH); + init(); /* * We don't register the handler here - this is done as a special case in @@ -90,17 +123,46 @@ public class RestHandler extends AbstractRequestHandler * Currently just echoes the request; add helper classes as required to * process requests */ - final String queryString = request.getQueryString(); - final String reply = "REST not yet implemented; received " - + request.getMethod() + ": " + request.getRequestURL() - + (queryString == null ? "" : "?" + queryString); - System.out.println(reply); + System.out.println(request.toString()); + if (endpoints == null) + { + final String queryString = request.getQueryString(); + final String reply = "REST not yet implemented; received " + + request.getMethod() + ": " + request.getRequestURL() + + (queryString == null ? "" : "?" + queryString); + System.out.println(reply); + + response.setHeader("Cache-Control", "no-cache/no-store"); + response.setHeader("Content-type", "text/plain"); + final PrintWriter writer = response.getWriter(); + writer.write(reply); + writer.close(); + return; + } + + String endpointName = getEndpointName(request); + + if (!endpoints.containsKey(endpointName) + || endpoints.get(endpointName) == null) + { + + response.setHeader("Cache-Control", "no-cache/no-store"); + response.setHeader("Content-type", "text/plain"); + response.setStatus(400); + PrintWriter writer = response.getWriter(); + writer.write(missingEndpointMessage == null + ? "REST endpoint '" + endpointName + "' not defined" + : missingEndpointMessage); + writer.close(); + return; + } response.setHeader("Cache-Control", "no-cache/no-store"); response.setHeader("Content-type", "text/plain"); - final PrintWriter writer = response.getWriter(); - writer.write(reply); - writer.close(); + Endpoint ep = endpoints.get(endpointName); + ep.processEndpoint(endpointName, request, response); + + return; } /** @@ -112,4 +174,100 @@ public class RestHandler extends AbstractRequestHandler return MY_NAME; } -} + /** + * Initialise methods + * + * @throws BindException + */ + protected void init() throws BindException + { + init(MY_PATH); + } + + protected void init(String path) throws BindException + { + setPath(path); + // Override this in extended class + // e.g. registerHandler and addEndpoints + } + + protected boolean addEndpoint(String name, Endpoint ep) + { + if (endpoints == null) + { + endpoints = new HashMap<>(); + } + endpoints.put(name, ep); + return true; + } + + protected String getEndpointName(HttpServletRequest request) + { + String pathInfo = request.getPathInfo(); + int slashpos = pathInfo.indexOf('/', 1); + return slashpos > 1 ? pathInfo.substring(1, slashpos) + : pathInfo.substring(1); + } + + 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 = getEndpointName(request); + Cache.error(this.MY_NAME + " error: endpoint " + endpointName + + " failed: '" + message + "'"); + try + { + PrintWriter writer = response.getWriter(); + writer.write("Endpoint " + endpointName + ": " + message); + writer.close(); + } catch (IOException e) + { + Cache.debug(e); + } + } + + protected void returnStatus(HttpServletResponse response, String id, + Status status) + { + try + { + PrintWriter writer = response.getWriter(); + if (id != null) + writer.write("id=" + id + "\n"); + if (status != null) + writer.write("status=" + status.toString() + "\n"); + } 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 -- 1.7.10.2