From d6bdee9f63270a6983eda7998571126d66e62455 Mon Sep 17 00:00:00 2001 From: Ben Soares Date: Tue, 22 Mar 2022 18:42:30 +0000 Subject: [PATCH] JAL-3851 merged to develop 2022-03-22 --- doc/api_specification.md | 40 +++ src/jalview/bin/Jalview.java | 33 +++ src/jalview/gui/AlignFrame.java | 66 +++++ src/jalview/gui/Desktop.java | 173 +++++++----- src/jalview/gui/SequenceFetcher.java | 70 ++++- src/jalview/httpserver/AbstractRequestHandler.java | 6 +- src/jalview/httpserver/HttpServer.java | 65 ++++- src/jalview/rest/API.java | 84 ++++++ src/jalview/rest/AbstractEndpoint.java | 219 +++++++++++++++ src/jalview/rest/AbstractEndpointAsync.java | 281 ++++++++++++++++++++ src/jalview/rest/FetchSequencesEndpoint.java | 99 +++++++ src/jalview/rest/GetCrossReferencesEndpoint.java | 141 ++++++++++ src/jalview/rest/HighlightSequenceEndpoint.java | 91 +++++++ src/jalview/rest/InputAlignmentEndpoint.java | 184 +++++++++++++ src/jalview/rest/RestHandler.java | 200 +++++++++++++- src/jalview/rest/SelectSequencesEndpoint.java | 103 +++++++ utils/eclipse/eclipse_run.sh | 3 + wget_commands.sh | 10 + 18 files changed, 1768 insertions(+), 100 deletions(-) create mode 100644 doc/api_specification.md create mode 100644 src/jalview/rest/API.java create mode 100644 src/jalview/rest/AbstractEndpoint.java create mode 100644 src/jalview/rest/AbstractEndpointAsync.java create mode 100644 src/jalview/rest/FetchSequencesEndpoint.java create mode 100644 src/jalview/rest/GetCrossReferencesEndpoint.java create mode 100644 src/jalview/rest/HighlightSequenceEndpoint.java create mode 100644 src/jalview/rest/InputAlignmentEndpoint.java create mode 100644 src/jalview/rest/SelectSequencesEndpoint.java create mode 100755 utils/eclipse/eclipse_run.sh create mode 100644 wget_commands.sh diff --git a/doc/api_specification.md b/doc/api_specification.md new file mode 100644 index 0000000..8ec1baa --- /dev/null +++ b/doc/api_specification.md @@ -0,0 +1,40 @@ +# Jalview Rest(-like) API + +## Launch Jalview with API active on specified port + +`java -jar jalview_all.jar -nosplash -nowebservicediscovery -startapi -serverport 2021` +(-nosplash and -nowebservicediscovery just to save time/output) + +## Open a MSA window with existing data + +`*POST* http://localhost:2021/jalview/api/inputalignment` +with body of HTTP request being content of file (automatic parsing by Jalview) + +`*GET* http://localhost:2021/jalview/api/inputalignment?data=` +with being content of file (limited size) + +`*GET* http://localhost:2021/jalview/api/inputalignment?file=` +with being a file + +`*GET* http://localhost:2021/jalview/api/inputalignment?url=` +with being a URL to a file + +## Open a MSA window with online resource fetch + +`http://localhost:2021/jalview/api/fetchsequences//` + is one of "ensemble", "pdb", "ensembl-tr", "ensembl-gn", ["uniprotkb/swiss-prot", "uniprotkb/trembl", ""uniprot/sptrembl", "uniprot/swissprot" not yet implemented] + ids for fetch + +### optional parameters +`id=` + +### Returns (as key=value pairs in the body of the response) +``` +id= +status= + + + + + + diff --git a/src/jalview/bin/Jalview.java b/src/jalview/bin/Jalview.java index fc4c821..2499cb8 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; @@ -56,6 +57,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; @@ -68,6 +70,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; @@ -490,6 +493,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); + Console.info("Set suggested server port to " + port); + } catch (NumberFormatException e) + { + Console.warn( + "server_port '" + sPort + "' not parseable as Integer"); + } + } + // Start a TestListener + if (aparser.contains("startapi")) + { + try + { + API api = API.getInstance(); + Console.info(api.getName() + " started at " + + HttpServer.getInstance().getUri().toString()); + } catch (BindException e) + { + Console.warn("Could not open a genomeapi", 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/AlignFrame.java b/src/jalview/gui/AlignFrame.java index e24cbea..168f6c5 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -57,9 +57,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.Enumeration; +import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Vector; +import java.util.concurrent.CompletableFuture; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; @@ -4465,6 +4468,22 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, return showp; } + public List getProducts() + { + SequenceI[] seqs = viewport.getAlignment().getSequencesArray(); + AlignmentI dataset = viewport.getAlignment().getDataset(); + + boolean dna = viewport.getAlignment().isNucleotide(); + + if (seqs == null || seqs.length == 0) + { + // nothing to see here. + return null; + } + + return new CrossRef(seqs, dataset).findXrefSourcesForSequences(dna); + } + /** * Finds and displays cross-references for the selected sequences (protein * products for nucleotide sequences, dna coding sequences for peptides). @@ -4476,12 +4495,37 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, * @param source * the database to show cross-references for */ + /* protected void showProductsFor(final SequenceI[] sel, final boolean _odna, final String source) { new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this)) .start(); } + */ + protected void showProductsFor(final SequenceI[] sel, final boolean _odna, + final String source) + { + showProductsFor(sel, _odna, source, false, null); + } + + public CompletableFuture showProductsFor(final SequenceI[] sel, + final boolean _odna, final String source, boolean returnFuture, + String id) + { + CompletableFuture cf = CompletableFuture + .runAsync(() -> runCrossRefActionAndCacheAlignFrame(sel, _odna, + source, returnFuture, id)); + return returnFuture ? cf : null; + } + + private void runCrossRefActionAndCacheAlignFrame(SequenceI[] sel, + boolean _odna, String source, boolean cacheAlignFrame, String id) + { + final AlignFrame af = this; + CrossRefAction.getHandlerFor(sel, _odna, source, af).run(); + af.cacheAlignFrameFromRestId(id); + } /** * Construct and display a new frame containing the translation of this @@ -5419,6 +5463,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, } } } + }); } }).start(); @@ -5905,6 +5950,27 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener, { return lastFeatureSettingsBounds; } + + /* + * Caching hashmaps for jalview.rest.API + */ + private static Map alignFrameMap = null; + + public static AlignFrame getAlignFrameFromRestId(String id) + { + if (id == null || alignFrameMap == null) + return null; + return alignFrameMap.get(id); + } + + public void cacheAlignFrameFromRestId(String id) + { + if (id == null) + return; + if (alignFrameMap == null) + alignFrameMap = new HashMap<>(); + alignFrameMap.put(id, this); + } } class PrintThread extends Thread diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index aeb0fac..b829b5f 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -20,8 +20,6 @@ */ package jalview.gui; -import java.util.Locale; - import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; @@ -64,6 +62,7 @@ import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.ListIterator; +import java.util.Locale; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -482,6 +481,8 @@ public class Desktop extends jalview.jbgui.GDesktop showMemusage.setSelected(selmemusage); desktop.setBackground(Color.white); + this.setIconImages(ChannelProperties.getIconList()); + getContentPane().setLayout(new BorderLayout()); // alternate config - have scrollbars - see notes in JAL-153 // JScrollPane sp = new JScrollPane(); @@ -1016,7 +1017,7 @@ public class Desktop extends jalview.jbgui.GDesktop frame.setIcon(false); } catch (java.beans.PropertyVetoException ex) { - + // System.err.println(ex.toString()); } } }); @@ -1149,7 +1150,7 @@ public class Desktop extends jalview.jbgui.GDesktop : protocols.get(i); FileFormatI format = null; - if (fileName.endsWith(".jar")) + if (fileName.toLowerCase(Locale.ROOT).endsWith(".jar")) { format = FileFormat.Jalview; @@ -1202,31 +1203,37 @@ public class Desktop extends jalview.jbgui.GDesktop 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 * @@ -1289,51 +1296,13 @@ public class Desktop extends jalview.jbgui.GDesktop : ((JComboBox) history).getEditor().getItem() .toString().trim()); - if (url.toLowerCase(Locale.ROOT).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); } } }; @@ -1346,6 +1315,52 @@ public class Desktop extends jalview.jbgui.GDesktop MessageManager.getString("action.ok")); } + public boolean loadUrl(String url, AlignViewport viewport) + { + if (url.toLowerCase(Locale.ROOT).endsWith(".jar")) + { + if (viewport != null) + { + new FileLoader().LoadFile(viewport, url, DataSourceType.URL, + FileFormat.Jalview); + } + else + { + new FileLoader().LoadFile(url, DataSourceType.URL, + FileFormat.Jalview); + } + } + else + { + 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 * @@ -3217,6 +3232,28 @@ public class Desktop extends jalview.jbgui.GDesktop Transferable t) throws Exception { + // BH 2018 changed List to List to allow for File from + // SwingJS + + // DataFlavor[] flavors = t.getTransferDataFlavors(); + // for (int i = 0; i < flavors.length; i++) { + // if (flavors[i].isFlavorJavaFileListType()) { + // evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); + // List list = (List) t.getTransferData(flavors[i]); + // for (int j = 0; j < list.size(); j++) { + // File file = (File) list.get(j); + // byte[] data = getDroppedFileBytes(file); + // fileName.setText(file.getName() + " - " + data.length + " " + + // evt.getLocation()); + // JTextArea target = (JTextArea) ((DropTarget) + // evt.getSource()).getComponent(); + // target.setText(new String(data)); + // } + // dtde.dropComplete(true); + // return; + // } + // + DataFlavor uriListFlavor = new DataFlavor( "text/uri-list;class=java.lang.String"), urlFlavour = null; try diff --git a/src/jalview/gui/SequenceFetcher.java b/src/jalview/gui/SequenceFetcher.java index e596fbf..7d74f82 100755 --- a/src/jalview/gui/SequenceFetcher.java +++ b/src/jalview/gui/SequenceFetcher.java @@ -31,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; @@ -168,10 +169,18 @@ public class SequenceFetcher extends JPanel implements Runnable * @param guiIndic * @param selectedDb * @param queryString + * @param interactive */ public SequenceFetcher(IProgressIndicator guiIndic, final String selectedDb, final String queryString) { + this(guiIndic, selectedDb, queryString, true); + } + + public SequenceFetcher(IProgressIndicator guiIndic, + final String selectedDb, final String queryString, + boolean interactive) + { this.progressIndicator = guiIndic; getSequenceFetcherSingleton(); this.guiWindow = progressIndicator; @@ -181,7 +190,7 @@ public class SequenceFetcher extends JPanel implements Runnable alignFrame = (AlignFrame) progressIndicator; } - jbInit(selectedDb); + jbInit(selectedDb, interactive); textArea.setText(queryString); frame = new JInternalFrame(); @@ -198,7 +207,7 @@ public class SequenceFetcher extends JPanel implements Runnable .getString("label.additional_sequence_fetcher")); } - private void jbInit(String selectedDb) + private void jbInit(String selectedDb, boolean interactive) { this.setLayout(new BorderLayout()); @@ -356,6 +365,18 @@ public class SequenceFetcher extends JPanel implements Runnable jScrollPane1.getViewport().add(textArea); idsPanel.add(jScrollPane1, BorderLayout.CENTER); + // En/disable or show/hide interactive elements + database.setEnabled(interactive); + exampleAccession.setVisible(interactive); + replacePunctuation.setVisible(interactive); + okBtn.setVisible(interactive); + exampleBtn.setVisible(interactive); + closeBtn.setVisible(interactive); + backBtn.setVisible(interactive); + jLabel1.setVisible(interactive); + clear.setVisible(interactive); + textArea.setEnabled(interactive); + this.add(actionPanel, BorderLayout.SOUTH); this.add(idsPanel, BorderLayout.CENTER); this.add(databasePanel, BorderLayout.NORTH); @@ -466,7 +487,7 @@ public class SequenceFetcher extends JPanel implements Runnable * * @param e */ - protected void close_actionPerformed(ActionEvent e) + public void close_actionPerformed(ActionEvent e) { try { @@ -485,6 +506,12 @@ public class SequenceFetcher extends JPanel implements Runnable */ public void ok_actionPerformed() { + ok_actionPerformed(false, null); + } + + public CompletableFuture ok_actionPerformed(boolean returnFuture, + String id) + { /* * tidy inputs and check there is something to search for */ @@ -505,14 +532,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); @@ -521,8 +548,17 @@ public class SequenceFetcher extends JPanel implements Runnable closeBtn.setEnabled(false); backBtn.setEnabled(false); - Thread worker = new Thread(this); - worker.start(); + CompletableFuture worker = CompletableFuture + .runAsync(() -> runAndCacheAlignFrame(returnFuture, id)); + + return returnFuture ? worker : null; + } + + private void runAndCacheAlignFrame(boolean cacheAlignFrame, String id) + { + AlignFrame af = this.run(cacheAlignFrame); + if (cacheAlignFrame && id != null && af != null) + af.cacheAlignFrameFromRestId(id); } private void resetDialog() @@ -537,6 +573,11 @@ public class SequenceFetcher extends JPanel implements Runnable @Override public void run() { + run(false); + } + + public AlignFrame run(boolean returnAlignFrame) + { boolean addToLast = false; List aresultq = new ArrayList<>(); List presultTitle = new ArrayList<>(); @@ -676,9 +717,10 @@ public class SequenceFetcher extends JPanel implements Runnable : MessageManager.getString("status.processing"), Thread.currentThread().hashCode()); // process results + AlignFrame af = null; while (presult.size() > 0) { - parseResult(presult.remove(0), presultTitle.remove(0), null, + af = parseResult(presult.remove(0), presultTitle.remove(0), null, preferredFeatureColours); } // only remove visual delay after we finished parsing. @@ -706,6 +748,7 @@ public class SequenceFetcher extends JPanel implements Runnable showErrorMessage(sb.toString()); } resetDialog(); + return returnAlignFrame ? af : null; } /** @@ -882,20 +925,21 @@ public class SequenceFetcher extends JPanel implements Runnable * @param preferredFeatureColours * @return the alignment */ - public AlignmentI parseResult(AlignmentI al, String title, + public AlignFrame parseResult(AlignmentI al, String title, FileFormatI currentFileFormat, FeatureSettingsModelI preferredFeatureColours) { + AlignFrame af = alignFrame; if (al != null && al.getHeight() > 0) { if (title == null) { title = getDefaultRetrievalTitle(); } - if (alignFrame == null) + if (af == null) { - AlignFrame af = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH, + af = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); if (currentFileFormat != null) { @@ -935,10 +979,10 @@ public class SequenceFetcher extends JPanel implements Runnable } else { - alignFrame.viewport.addAlignment(al, title); + af.viewport.addAlignment(al, title); } } - return al; + return af; } void showErrorMessage(final String error) diff --git a/src/jalview/httpserver/AbstractRequestHandler.java b/src/jalview/httpserver/AbstractRequestHandler.java index ece2df0..f8c5441 100644 --- a/src/jalview/httpserver/AbstractRequestHandler.java +++ b/src/jalview/httpserver/AbstractRequestHandler.java @@ -31,6 +31,8 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; +import jalview.bin.Console; + /** * * @author gmcarstairs @@ -67,8 +69,8 @@ public abstract class AbstractRequestHandler extends AbstractHandler /* * Set server error status on response */ - System.err.println("Exception handling request " - + request.getRequestURI() + " : " + t.getMessage()); + Console.error("Exception handling request " + request.getRequestURI(), + t); if (response.isCommitted()) { /* diff --git a/src/jalview/httpserver/HttpServer.java b/src/jalview/httpserver/HttpServer.java index a18d38d..057c714 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,38 @@ public class HttpServer + " handler on " + handler.getUri()); } } + + /** + * Gets the ContextHandler attached to this handler. Useful for obtaining the + * full path used to access a given handler. + * + * @param handler + */ + public ContextHandler getContextHandler(AbstractRequestHandler handler) + { + return myHandlers.get(handler); + } + + /** + * 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..93e7622 --- /dev/null +++ b/src/jalview/rest/API.java @@ -0,0 +1,84 @@ +package jalview.rest; + +import java.net.BindException; +import java.util.HashMap; +import java.util.Map; + +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 Map objectMap = 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(new FetchSequencesEndpoint(this)); + addEndpoint(new InputAlignmentEndpoint(this)); + addEndpoint(new HighlightSequenceEndpoint(this)); + addEndpoint(new SelectSequencesEndpoint(this)); + addEndpoint(new GetCrossReferencesEndpoint(this)); + + setPath(MY_PATH); + this.registerHandler(); + + init = true; + } + + /* + * Shared methods below here + */ + + @Override + public String getPath() + { + return MY_PATH; + } + + protected static Map getStatusMap() + { + return statusMap; + } + + protected static Map getRequestMap() + { + return requestMap; + } + + protected static Map getObjectMap() + { + return objectMap; + } +} diff --git a/src/jalview/rest/AbstractEndpoint.java b/src/jalview/rest/AbstractEndpoint.java new file mode 100644 index 0000000..deb4683 --- /dev/null +++ b/src/jalview/rest/AbstractEndpoint.java @@ -0,0 +1,219 @@ +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.Console; +import jalview.gui.AlignFrame; +import jalview.gui.Desktop; +import jalview.rest.RestHandler.EndpointI; + +public abstract class AbstractEndpoint implements EndpointI +{ + private final String path; + + private API api; + + private final String name; + + private final String parameters; + + private final String description; + + public AbstractEndpoint(API api, String path, String name, + String parameters, String description) + { + this.api = api; + this.path = path; + this.name = name; + this.parameters = parameters; + this.description = description; + } + + public String getPath() + { + return this.path; + } + + protected API getAPI() + { + return this.api; + } + + public String getName() + { + return this.name; + } + + public String getParameters() + { + return this.parameters; + } + + public String getDescription() + { + return this.description; + } + + 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) + { + String okString = request.getParameter("ok"); + boolean ok = (okString != null && okString.equalsIgnoreCase("true")); + /* + * Annoyingly jetty is not adding content to anything other than a few + * 20x status codes. Possibly it is closing the PrintWriter. + * Find a fix for this! + ****************************************************/ + response.setStatus(ok ? HttpServletResponse.SC_OK : // + // HttpServletResponse.SC_BAD_REQUEST // + HttpServletResponse.SC_PARTIAL_CONTENT // + ); + + String endpointName = getPath(); + Console.error(getAPI().getName() + " error: endpoint " + endpointName + + " failed: '" + message + "'"); + try + { + PrintWriter writer = response.getWriter(); + writer.write("message=Endpoint " + endpointName + ": " + message); + } catch (IOException e) + { + Console.info("Exception writing to REST response", 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(); + } + + protected boolean checkParameters(HttpServletRequest request, + HttpServletResponse response, int i) + { + String[] parameters = getEndpointPathParameters(request); + + // check we can run fetchsequence + if (parameters.length < i) + { + returnError(request, response, + "requires parameters:" + getParameters() + "\n" + getName() + + ": " + getDescription()); + return false; + } + return true; + } + + public int[][] parseIntRanges(String rangesString) + { + if (rangesString.equals("*")) + { + return new int[][] { { -1 }, { -1 } }; + } + String[] rangeStrings = rangesString.split(","); + int[][] ranges = new int[2][rangeStrings.length]; + for (int i = 0; i < rangeStrings.length; i++) + { + String range = rangeStrings[i]; + try + { + int hyphenpos = range.indexOf('-'); + if (hyphenpos < 0) + { + ranges[0][i] = Integer.parseInt(range); + ranges[1][i] = ranges[0][i]; + } + else + { + ranges[0][i] = Integer.parseInt(range.substring(0, hyphenpos)); + ranges[1][i] = Integer.parseInt(range.substring(hyphenpos + 1)); + } + } catch (NumberFormatException nfe) + { + return null; + } + } + return ranges; + } + + /* + * Get all AlignFrames or just one if requested to work on a specific window (fromId query string parameter) + */ + protected AlignFrame[] getAlignFrames(HttpServletRequest request, + boolean all) + { + return getAlignFrames(request, "fromId", all); + } + + protected AlignFrame[] getAlignFrames(HttpServletRequest request, + String idParam, boolean all) + { + String fromIdString = request.getParameter(idParam); + + if (fromIdString != null) + { + AlignFrame af = AlignFrame.getAlignFrameFromRestId(fromIdString); + return af == null ? null : new AlignFrame[] { af }; + } + else if (all) + { + return Desktop.getAlignFrames(); + } + else + { + return null; + } + } + + protected AlignFrame getAlignFrameFromId(HttpServletRequest request) + { + return getAlignFrameFromId(request, "fromId"); + } + + protected AlignFrame getAlignFrameFromId(HttpServletRequest request, + String idParam) + { + AlignFrame[] afs = getAlignFrames(request, idParam, false); + return (afs == null || afs.length < 1 || afs[0] == null) ? null + : afs[0]; + } + + protected boolean deleteFromCache() + { + return false; + } + +} diff --git a/src/jalview/rest/AbstractEndpointAsync.java b/src/jalview/rest/AbstractEndpointAsync.java new file mode 100644 index 0000000..cb1ff34 --- /dev/null +++ b/src/jalview/rest/AbstractEndpointAsync.java @@ -0,0 +1,281 @@ +package jalview.rest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.bin.Console; +import jalview.rest.RestHandler.Status; + +public abstract class AbstractEndpointAsync extends AbstractEndpoint +{ + public AbstractEndpointAsync(API api, String path, String name, + String parameters, String description) + { + super(api, path, name, parameters, description); + } + + protected String idExtension = null; + + protected String id = null; + + protected CompletableFuture cf = null; + + protected Map> cfMap = new HashMap<>(); + + protected Map objectsPassedToProcessAsync = new HashMap<>(); + + private Status tempStatus = null; + + protected void setCompletableFuture(CompletableFuture cf) + { + this.cf = cf; + if (getId() != null) + cfMap.put(getId(), cf); + } + + protected CompletableFuture getCompletableFuture() + { + if (cf == null && getId() != null && cfMap.get(getId()) != null) + cf = cfMap.get(getId()); + return this.cf; + } + + protected void setId(String id) + { + this.id = id; + } + + protected void setIdExtension(String idExtension) + { + setId(getPath() + "::" + 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 processAsync(HttpServletRequest request, + HttpServletResponse response, final Map finalMap); + + protected void finalise(HttpServletRequest request, + HttpServletResponse response) + { + // can be Overridden + } + + @Override + public void processEndpoint(HttpServletRequest request, + HttpServletResponse response) + { + tempStatus = null; + // subclass method + initialise(request, response); + + if (checkStatus(request, response, Status.STARTED)) + { + String alreadyFinishedString = null; + if (getStatus() == Status.FINISHED) + { + alreadyFinishedString = finishedResponseString(request, response); + } + returnStatus(request, response, alreadyFinishedString); + return; + } + + if (getCompletableFuture() == null) + { + final Map finalObjectMap = objectsPassedToProcessAsync; + setCompletableFuture(CompletableFuture.runAsync(() -> { + // subclass method + try + { + this.processAsync(request, response, finalObjectMap); + } catch (ClassCastException e) + { + Console.info("Something went wrong with async endpoint execution" + + getName(), e); + } + })); + } + addWhenCompleteCompletableFuture(); + + // subclass method + finalise(request, response); + + returnStatus(response); + changeStatus(Status.IN_PROGRESS); + } + + protected void atEnd() + { + } + + protected String finishedResponseString(HttpServletRequest request, + HttpServletResponse response) + { + return null; + } + + /* + * Shared methods below here + */ + + protected String setId(HttpServletRequest request, String extension) + { + String idString = request.getParameter("id"); + if (idString == null) + { + setIdExtension(extension); + } + else + { + setId(idString); + } + return getId(); + } + + protected void changeStatus(Status status) + { + String id = getId(); + // don't change a job's status if it has finished or died + if (getStatus() == Status.FINISHED || getStatus() == Status.ERROR) + return; + tempStatus = status; + if (status != Status.NOT_RUN) + API.getStatusMap().put(id, status); + } + + protected Status getStatus() + { + Status status = API.getStatusMap().get(getId()); + return status == null ? tempStatus : status; + } + + protected void returnStatus(HttpServletResponse response) + { + returnStatus(null, response, null); + } + + protected void returnStatus(HttpServletRequest request, + HttpServletResponse response, String message) + { + 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 (getStatus() != null) + { + switch (getStatus()) + { + case STARTED: + response.setStatus(HttpServletResponse.SC_ACCEPTED); + break; + case IN_PROGRESS: + response.setStatus(HttpServletResponse.SC_ACCEPTED); + break; + case FINISHED: + response.setStatus(HttpServletResponse.SC_CREATED); + break; + case ERROR: + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + message); + break; + } + writer.write("status=" + getStatus().toString() + "\n"); + } + if (message != null) + { + writer.write(message); + } + } catch (IOException e) + { + Console.debug("Exception writing to REST response", e); + } + } + + protected boolean checkStatus(HttpServletRequest request, + HttpServletResponse response) + { + return checkStatus(request, response, null); + } + + protected boolean checkStatus(HttpServletRequest request, + HttpServletResponse response, Status set) + { + String id = getId(); + Status status = getStatus(); + if (status == null) + { + if (set != null) + changeStatus(set); + API.getRequestMap().put(id, request.getRequestURI()); + return false; + } + else + { + return true; + } + } + + protected void addWhenCompleteCompletableFuture() + { + String id = getId(); + cf.whenComplete((Void, e) -> { + if (e != null) + { + Console.error("Endpoint job " + id + " did not complete", e); + changeStatus(Status.ERROR); + } + else + { + Console.info("Endpoint job " + id + " completed successfully"); + changeStatus(Status.FINISHED); + atEnd(); + } + }); + } + + @Override + protected void returnError(HttpServletRequest request, + HttpServletResponse response, String message) + { + changeStatus(Status.NOT_RUN); + super.returnError(request, response, message); + } + + @Override + protected boolean deleteFromCache() + { + return false; + } +} diff --git a/src/jalview/rest/FetchSequencesEndpoint.java b/src/jalview/rest/FetchSequencesEndpoint.java new file mode 100644 index 0000000..7fa95e2 --- /dev/null +++ b/src/jalview/rest/FetchSequencesEndpoint.java @@ -0,0 +1,99 @@ +package jalview.rest; + +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.api.AlignmentViewPanel; +import jalview.bin.Console; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; +import jalview.gui.Desktop; +import jalview.gui.SequenceFetcher; +import jalview.util.DBRefUtils; + +public class FetchSequencesEndpoint extends AbstractEndpointAsync +{ + public FetchSequencesEndpoint(API api) + { + super(api, path, name, parameters, description); + } + + private static final String path = "fetchsequences"; + + private static final String name = "Fetch Sequences"; + + private static final String parameters = "/"; + + private static final String description = "Fetch sequences from online resource"; + + private SequenceFetcher sf; + + @Override + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + if (!checkParameters(request, response, 2)) + { + return; + } + String[] parameters = getEndpointPathParameters(request); + + 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; + sf = new SequenceFetcher(desktop, db, dbId, false); + setCompletableFuture(sf.ok_actionPerformed(true, getId())); + } + + protected void processAsync(HttpServletRequest request, + HttpServletResponse response, Map map) + { + // all the work being done by the SequenceFetcher! + Console.warn("THIS SHOULD NOT BE RUN"); + } + + @Override + protected void atEnd() + { + sf.close_actionPerformed(null); + } + + @Override + protected String finishedResponseString(HttpServletRequest request, + HttpServletResponse response) + { + AlignFrame af = getAlignFrameFromId(request, "id"); + if (af == null) + return null; + List aps = (List) af + .getAlignPanels(); + StringBuilder sb = new StringBuilder(); + for (AlignmentViewPanel ap : aps) + { + AlignmentI al = ap.getAlignment(); + if (al == null) + continue; + List seqs = (List) al.getSequences(); + for (SequenceI seq : seqs) + { + if (sb.length() > 0) + sb.append(","); + sb.append(seq.getName()); + } + } + sb.insert(0, "sequences="); + sb.append("\n"); + return sb.toString(); + } + +} diff --git a/src/jalview/rest/GetCrossReferencesEndpoint.java b/src/jalview/rest/GetCrossReferencesEndpoint.java new file mode 100644 index 0000000..611c2a3 --- /dev/null +++ b/src/jalview/rest/GetCrossReferencesEndpoint.java @@ -0,0 +1,141 @@ +package jalview.rest; + +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.api.AlignmentViewPanel; +import jalview.bin.Console; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; + +public class GetCrossReferencesEndpoint extends AbstractEndpointAsync +{ + public GetCrossReferencesEndpoint(API api) + { + super(api, path, name, parameters, description); + } + + private static final String path = "getcrossreferences"; + + private static final String name = "Get Cross References"; + + private static final String parameters = "/?fromId="; + + private static final String description = "Get Cross References for alignment "; + + private String resourceName; + + private AlignFrame af; + + private SequenceI[] seqs; + + @Override + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + if (!checkParameters(request, response, 1)) + { + return; + } + String[] parameters = getEndpointPathParameters(request); + + resourceName = parameters[0]; + + String fromId = request.getParameter("fromId"); + setId(request, resourceName + "::" + fromId); + + if (checkStatus(request, response)) + { + return; + } + + af = getAlignFrameFromId(request); + if (af == null) + { + returnError(request, response, "Could not find results"); + return; + } + + if (af.canShowProducts()) + { + List products = af.getProducts(); + if (products == null) + { + returnError(request, response, + "no cross reference products available"); + return; + } + if (!products.contains(resourceName)) + { + StringBuilder sb = new StringBuilder(); + sb.append("cross reference product '").append(resourceName) + .append("' not available: available products are\n"); + sb.append("products="); + boolean first = true; + for (String p : products) + { + if (!first) + { + sb.append(","); + first = false; + } + sb.append(p); + } + sb.append("\n"); + returnError(request, response, sb.toString()); + return; + } + } + + seqs = af.getViewport().getAlignment().getSequencesArray(); + if (seqs == null || seqs.length == 0) + { + // nothing to do + returnError(request, response, "no sequences selected"); + return; + } + + final boolean dna = af.getViewport().getAlignment().isNucleotide(); + setCompletableFuture( + af.showProductsFor(seqs, dna, resourceName, true, getId())); + } + + protected void processAsync(HttpServletRequest request, + HttpServletResponse response, Map map) + { + // all the work being done by the AlignFrame! + Console.warn("THIS SHOULD NOT BE RUN"); + } + + protected String finishedResponseString(HttpServletRequest request, + HttpServletResponse response) + { + AlignFrame af = AlignFrame.getAlignFrameFromRestId(getId()); + if (af == null) + { + return null; + } + List aps = (List) af + .getAlignPanels(); + StringBuilder sb = new StringBuilder(); + for (AlignmentViewPanel ap : aps) + { + AlignmentI al = ap.getAlignment(); + List seqs = (List) al.getSequences(); + for (SequenceI seq : seqs) + { + if (sb.length() > 0) + sb.append(","); + sb.append(seq.getName()); + } + } + sb.insert(0, "sequences="); + sb.append("\n"); + return sb.toString(); + } + +} diff --git a/src/jalview/rest/HighlightSequenceEndpoint.java b/src/jalview/rest/HighlightSequenceEndpoint.java new file mode 100644 index 0000000..f228473 --- /dev/null +++ b/src/jalview/rest/HighlightSequenceEndpoint.java @@ -0,0 +1,91 @@ +package jalview.rest; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.api.AlignmentViewPanel; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; +import jalview.structure.StructureSelectionManager; + +public class HighlightSequenceEndpoint extends AbstractEndpoint +{ + public HighlightSequenceEndpoint(API api) + { + super(api, path, name, parameters, description); + } + + protected static final String path = "highlightsequence"; + + private static final String name = "Highlight sequence position"; + + private static final String parameters = "/"; + + private static final String description = "Highlight the specified sequences at the specified position"; + + public void processEndpoint(HttpServletRequest request, + HttpServletResponse response) + { + if (!checkParameters(request, response, 2)) + { + return; + } + String[] parameters = getEndpointPathParameters(request); + + String posString = parameters[1]; + int pos = -1; + try + { + pos = Integer.parseInt(posString); + } catch (NumberFormatException e) + { + returnError(request, response, + "Could not parse postition integer " + posString); + } + + String sequenceNames = parameters[0]; + + Map ssmMap = new HashMap<>(); + AlignFrame[] alignFrames = getAlignFrames(request, true); + if (alignFrames == null) + { + returnError(request, response, "could not find results"); + return; + } + for (int i = 0; i < alignFrames.length; i++) + { + AlignFrame af = alignFrames[i]; + List aps = (List) af + .getAlignPanels(); + for (AlignmentViewPanel ap : aps) + { + StructureSelectionManager ssm = ap.getStructureSelectionManager(); + AlignmentI al = ap.getAlignment(); + List seqs = (List) al.getSequences(); + for (SequenceI seq : seqs) + { + if (sequenceNames.equals(seq.getName())) + { + ssmMap.put(seq, ssm); + } + } + } + } + // highlight + for (SequenceI seq : ssmMap.keySet()) + { + StructureSelectionManager ssm = ssmMap.get(seq); + if (ssm == null) + { + continue; + } + ssm.mouseOverSequence(seq, pos, -1, null); + } + + } +} \ No newline at end of file diff --git a/src/jalview/rest/InputAlignmentEndpoint.java b/src/jalview/rest/InputAlignmentEndpoint.java new file mode 100644 index 0000000..095fd24 --- /dev/null +++ b/src/jalview/rest/InputAlignmentEndpoint.java @@ -0,0 +1,184 @@ +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 java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.DatatypeConverter; + +import jalview.bin.Console; +import jalview.gui.CutAndPasteTransfer; +import jalview.gui.Desktop; + +public class InputAlignmentEndpoint extends AbstractEndpointAsync +{ + public InputAlignmentEndpoint(API api) + { + super(api, path, name, parameters, description); + } + + protected static final String path = "inputalignment"; + + private static final String name = "Input Alignment"; + + private static final String parameters = "POST | GET ?[data=|file=|url=]"; + + private static final String description = "Input an alignment from POST request body, GET request 'data' parameter, local file in 'file' parameter, url in 'url' parameter"; + + private String fileString; + + private String urlString; + + private String method; + + private String data; + + private String body; + + private boolean isPost; + + private boolean hasData; + + private String access = null; + + private String ref = null; + + @Override + protected void initialise(HttpServletRequest request, + HttpServletResponse response) + { + fileString = request.getParameter("file"); + urlString = request.getParameter("url"); + method = request.getMethod().toLowerCase(); + data = request.getParameter("data"); + body = null; + isPost = method.equalsIgnoreCase("post"); + hasData = data != null; + + if (isPost) + { + access = "post"; + try + { + body = getRequestBody(request); + } catch (IOException e) + { + returnError(request, response, "could not read POST body"); + Console.debug("Could not read POST body", e); + return; + } + // for ref see md5 later + } + else if (hasData) + { + 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 + String content; + if (isPost || hasData) + { + content = isPost ? body : data; + objectsPassedToProcessAsync.put("content", content); // needed as "final + // String" in process + try + { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(content.getBytes()); + byte[] digest = md5.digest(); + ref = DatatypeConverter.printBase64Binary(digest).toLowerCase(); + } catch (NoSuchAlgorithmException e) + { + Console.debug("Could not find MD5 algorithm", e); + } + } + else + { + content = null; + } + objectsPassedToProcessAsync.put("content", content); + + setId(request, access + "::" + ref); + } + + protected void process(HttpServletRequest request, + HttpServletResponse response) + { + processAsync(request, response, null); + } + + protected void processAsync(HttpServletRequest request, + HttpServletResponse response, final Map finalMap) + { + String content = (String) finalMap.get("content"); + if (isPost || hasData) + { + // 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 + "'"); + Console.debug("Could not resolve file '" + fileString + "'", 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; + } + } + } +} diff --git a/src/jalview/rest/RestHandler.java b/src/jalview/rest/RestHandler.java index a37882f..caec830 100644 --- a/src/jalview/rest/RestHandler.java +++ b/src/jalview/rest/RestHandler.java @@ -20,24 +20,64 @@ */ 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 org.eclipse.jetty.server.handler.ContextHandler; + +import jalview.bin.Console; +import jalview.httpserver.AbstractRequestHandler; +import jalview.httpserver.HttpServer; + /** * 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, NOT_RUN + } + + public interface EndpointI + { + public String getPath(); + + public String getName(); + + public String getParameters(); + + public String getDescription(); + + public void processEndpoint(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; + + protected Map getEndpoints() + { + return endpoints; + } + /** * Singleton instance of this class */ @@ -66,9 +106,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 +130,53 @@ 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); + return; + } + + String endpointName = getRequestedEndpointName(request); + + if (!endpoints.containsKey(endpointName) + || endpoints.get(endpointName) == null) + { + + response.setHeader("Cache-Control", "no-cache/no-store"); + response.setHeader("Content-type", "text/plain"); + PrintWriter writer = response.getWriter(); + writer.write(missingEndpointMessage == null + ? "REST endpoint '" + endpointName + "' not defined" + : missingEndpointMessage); + writer.write("\n"); + writer.write("Available endpoints are:\n"); + ContextHandler ch = HttpServer.getInstance().getContextHandler(this); + String base = HttpServer.getInstance().getUri().toString(); + String contextPath = ch == null ? "" : ch.getContextPath(); + for (String key : endpoints.keySet()) + { + writer.write(base + contextPath + "/" + key + "\n"); + } + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + 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(); + EndpointI ep = endpoints.get(endpointName); + ep.processEndpoint(request, response); + + return; } /** @@ -112,4 +188,102 @@ 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(EndpointI ep) + { + if (endpoints == null) + { + endpoints = new HashMap<>(); + } + AbstractEndpoint e = (AbstractEndpoint) ep; + endpoints.put(ep.getPath(), ep); + return true; + } + + protected String getRequestedEndpointName(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(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + String endpointName = getRequestedEndpointName(request); + Console.error(getName() + " error: endpoint " + endpointName + + " failed: '" + message + "'"); + try + { + PrintWriter writer = response.getWriter(); + writer.write("Endpoint " + endpointName + ": " + message); + } catch (IOException e) + { + Console.debug("Could not write to REST response for endpoint " + + endpointName, 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) + { + Console.debug("Could not write status to REST response for id:" + id, + 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/SelectSequencesEndpoint.java b/src/jalview/rest/SelectSequencesEndpoint.java new file mode 100644 index 0000000..7ca5d23 --- /dev/null +++ b/src/jalview/rest/SelectSequencesEndpoint.java @@ -0,0 +1,103 @@ +package jalview.rest; + +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import jalview.api.AlignViewportI; +import jalview.api.AlignmentViewPanel; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceGroup; +import jalview.datamodel.SequenceI; +import jalview.gui.AlignFrame; + +public class SelectSequencesEndpoint extends AbstractEndpoint +{ + public SelectSequencesEndpoint(API api) + { + super(api, path, name, parameters, description); + } + + protected static final String path = "selectsequences"; + + private static final String name = "Select sequence(s) positions"; + + private static final String parameters = "/"; + + private static final String description = "Select the specified sequence(s) with the specified range"; + + public void processEndpoint(HttpServletRequest request, + HttpServletResponse response) + { + if (!checkParameters(request, response, 2)) + { + return; + } + String[] parameters = getEndpointPathParameters(request); + + String rangesString = parameters[1]; + int[][] ranges = parseIntRanges(rangesString); + if (ranges == null || ranges.length < 2 || ranges[0].length < 1) + { + returnError(request, response, + "couldn't parse range '" + rangesString + "'"); + return; + } + if (ranges[0].length > 1) + { + returnError(request, response, + "only provide 1 range '" + rangesString + "'"); + return; + } + int start = ranges[0][0]; + int end = ranges[1][0]; + + String sequenceNamesString = parameters[0]; + List sequenceNames = Arrays + .asList(sequenceNamesString.split(",")); + + AlignFrame[] alignFrames = getAlignFrames(request, true); + if (alignFrames == null) + { + returnError(request, response, "could not find results"); + return; + } + for (int i = 0; i < alignFrames.length; i++) + { + AlignFrame af = alignFrames[i]; + List aps = (List) af + .getAlignPanels(); + for (AlignmentViewPanel ap : aps) + { + AlignViewportI avp = ap.getAlignViewport(); + AlignmentI al = ap.getAlignment(); + List seqs = (List) al.getSequences(); + SequenceGroup stretchGroup = new SequenceGroup(); + for (SequenceI seq : seqs) + { + if (sequenceNames.contains(seq.getName()) + || (sequenceNamesString.equals("*") + && alignFrames.length == 1)) + { + stretchGroup.addSequence(seq, false); + } + } + if (start == -1 && end == -1) // "*" as range + { + stretchGroup.setStartRes(al.getStartRes()); + stretchGroup.setEndRes(al.getEndRes()); + } + else + { + stretchGroup.setStartRes(start); + stretchGroup.setEndRes(end); + } + avp.setSelectionGroup(stretchGroup); + ap.paintAlignment(false, false); + avp.sendSelection(); + } + } + } +} \ No newline at end of file diff --git a/utils/eclipse/eclipse_run.sh b/utils/eclipse/eclipse_run.sh new file mode 100755 index 0000000..820c2ad --- /dev/null +++ b/utils/eclipse/eclipse_run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +java -cp "bin/main:j11lib/*:resources" jalview.bin.Launcher ${@} diff --git a/wget_commands.sh b/wget_commands.sh new file mode 100644 index 0000000..58fd7c0 --- /dev/null +++ b/wget_commands.sh @@ -0,0 +1,10 @@ +wget -q -S -O - 'http://localhost:2021/jalview/api/fetchsequences/ensembl/ENSG00000139618?id=myBRCA2' +wget -q -S -O - 'http://localhost:2021/jalview/api/getcrossreferences/UNIPROT?fromId=myBRCA2' +wget -q -S -O - "http://localhost:2021/jalview/api/highlightsequence/ENST00000544455/30000?fromId=myBRCA2" +N=22946; while [ $N -lt 23002 ]; do wget -q -S -O - "http://localhost:2021/jalview/api/highlightsequence/ENST00000544455/${N}?fromId=myBRCA2"; N=$((N+1)); sleep 0.2; done +wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/*/*?fromId=myBRCA2' +wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/*/22946-23001?fromId=myBRCA2' +wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/ENSG00000139618,ENST00000544455,ENST00000530893,ENST00000380152,ENST00000680887,ENST00000614259,ENST00000665585,ENST00000528762/22946-23001?fromId=myBRCA2' +wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/ENSG00000139618,ENST00000544455,ENST00000380152,ENST00000680887,ENST00000614259,ENST00000528762/22946-23001?fromId=myBRCA2' +### open 3D structure 6hqu and repeat highlight + -- 1.7.10.2