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;
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;
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;
}
}
+ // 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");
+ 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())
{
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");
}
/**
- * 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
{
{
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
*
: ((JComboBox<String>) 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);
}
}
};
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
*
*/
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;
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;
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.
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()
*/
public void ok_actionPerformed()
{
+ ok_actionPerformed(false);
+ }
+
+ public CompletableFuture ok_actionPerformed(boolean returnFuture)
+ {
/*
* tidy inputs and check there is something to search for
*/
text = text.replace(",", ";");
}
text = text.replaceAll("(\\s|[; ])+", ";");
- if (!t0.equals(text))
+ if (!t0.equals(text))
{
- textArea.setText(text);
+ textArea.setText(text);
}
if (text.isEmpty())
{
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);
closeBtn.setEnabled(false);
backBtn.setEnabled(false);
- Thread worker = new Thread(this);
- worker.start();
+ CompletableFuture<Void> worker = CompletableFuture
+ .runAsync(new Thread(this));
+
+ return returnFuture ? worker : null;
}
private void resetDialog()
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++)
{
*/
package jalview.httpserver;
-import jalview.rest.RestHandler;
-
import java.net.BindException;
import java.net.URI;
import java.util.Collections;
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;
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
* <ul>
*/
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.
*
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);
ServerConnector connector = new ServerConnector(server, 0, 2);
// restrict to localhost
connector.setHost("localhost");
+ if (PORT > 0)
+ {
+ connector.setPort(PORT);
+ }
server.addConnector(connector);
/*
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()));
+ " 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;
+ }
}
--- /dev/null
+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<String, Status> statusMap = new HashMap<>();
+
+ private static Map<String, String> 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<Void> 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<Void> 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<Void> 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<Void> 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<String, Status> getStatusMap()
+ {
+ return statusMap;
+ }
+
+ protected static Map<String, String> getRequestMap()
+ {
+ return requestMap;
+ }
+}
*/
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<String, Endpoint> endpoints = null;
+
/**
* Singleton instance of this class
*/
*
* @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
* 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;
}
/**
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