From: Jim Procter Date: Tue, 29 Nov 2022 15:34:41 +0000 (+0000) Subject: Merge branch 'JAL-3878_web_services_overhaul' into try-to-update-slivka-jar X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=8aa8957ae59e9171681ed89c8947dcfba847c1e4;p=jalview.git Merge branch 'JAL-3878_web_services_overhaul' into try-to-update-slivka-jar JAL-3066 JAL-3362 Update 2.12 alpha with latest slivka-client.jar Conflicts: src/jalview/bin/Cache.java src/jalview/gui/AlignFrame.java src/jalview/gui/Desktop.java src/jalview/workers/AlignCalcManager.java src/jalview/ws/jws2/JPredThread.java src/jalview/ws/slivkaws/SlivkaJPredServiceInstance.java src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java src/jalview/ws/slivkaws/SlivkaWSInstance.java swingjs/ver/3.3.1/SwingJS-site.zip swingjs/ver/3.3.1/timestamp --- 8aa8957ae59e9171681ed89c8947dcfba847c1e4 diff --cc src/jalview/bin/Cache.java index cb3a415,3208a36..8d1fdd2 --- a/src/jalview/bin/Cache.java +++ b/src/jalview/bin/Cache.java @@@ -42,17 -36,15 +42,16 @@@ import java.util.Locale import java.util.Properties; import java.util.StringTokenizer; import java.util.TreeSet; +import java.util.regex.Pattern; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.SimpleLayout; +import javax.swing.LookAndFeel; +import javax.swing.UIManager; - import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI; import jalview.datamodel.PDBEntry; +import jalview.gui.Preferences; import jalview.gui.UserDefinedColours; +import jalview.log.JLoggerLog4j; import jalview.schemes.ColourSchemeLoader; import jalview.schemes.ColourSchemes; import jalview.schemes.UserColourScheme; @@@ -353,18 -358,11 +354,19 @@@ public class Cache implements Applicati private void loadPropertiesImpl(String propsFile) { + propertiesFile = propsFile; + String releasePropertiesFile = null; + boolean defaultProperties = false; if (propsFile == null && !propsAreReadOnly) { - propertiesFile = Platform.getUserPath(".jalview_properties"); + // TODO: @bsoares - for 2.12 testing: check test,develop,release props are located correctly + String channelPrefsFilename = ChannelProperties + .getProperty("preferences.filename"); + String releasePrefsFilename = ".jalview_properties"; + propertiesFile = Platform.getUserPath(channelPrefsFilename); + releasePropertiesFile = Platform.getUserPath(releasePrefsFilename); + defaultProperties = true; } else { @@@ -420,8 -409,6 +423,7 @@@ System.out.println("Error reading properties file: " + ex); } } - + /* TO BE REPLACED WITH PROXY_TYPE SETTINGS if (getDefault("USE_PROXY", false)) { String proxyServer = getDefault("PROXY_SERVER", ""), @@@ -521,16 -484,11 +523,13 @@@ && (System.getProperty("java.awt.headless") == null || System .getProperty("java.awt.headless").equals("false"))) { - - new Thread() + class VersionChecker extends Thread { @Override public void run() { + String remoteBuildPropertiesUrl = Cache + .getAppbaseBuildProperties(); - String orgtimeout = System .getProperty("sun.net.client.defaultConnectTimeout"); if (orgtimeout == null) @@@ -1282,10 -1233,9 +1281,9 @@@ { // consider returning more human friendly info // eg 'built from Source' or update channel - return jalview.bin.Cache.getDefault("INSTALLATION", "unknown"); + return Cache.getDefault("INSTALLATION", "unknown"); } - /** * * For AppletParams and Preferences ok_actionPerformed and diff --cc src/jalview/gui/AlignFrame.java index a0ab8cc,3d00450..d18488b --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@@ -182,7 -115,11 +182,10 @@@ import jalview.ws.params.ArgumentI import jalview.ws.params.ParamDatastoreI; import jalview.ws.params.WsParamSetI; import jalview.ws.seqfetcher.DbSourceProxy; - import jalview.ws.slivkaws.SlivkaWSDiscoverer; + import jalview.ws2.WebServiceDiscoverer; + import jalview.ws2.WebServiceI; + import jalview.ws2.operations.Operation; + import jalview.ws2.slivka.SlivkaWSDiscoverer; - import java.io.IOException; import java.util.HashSet; import java.util.Set; @@@ -294,9 -236,10 +302,10 @@@ public class AlignFrame extends GAlignF private int id; private DataSourceType protocol ; + /** * Creates a new AlignFrame object with specific width and height. - * + * * @param al * @param width * @param height @@@ -663,9 -612,10 +678,10 @@@ /** * JavaScript will have this, maybe others. More dependable than a file name * and maintains a reference to the actual bytes loaded. - * + * * @param file */ + public void setFileObject(File file) { this.fileObject = file; @@@ -964,13 -916,21 +982,20 @@@ { buildWebServicesMenu(); } + + @Override + public void servicesChanged(WebServiceDiscoverer discoverer, + Collection services) + { + buildWebServicesMenu(); + } - /* Set up intrinsic listeners for dynamically generated GUI bits. */ private void addServiceListeners() { if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true)) { - WSDiscovererI discoverer = SlivkaWSDiscoverer.getInstance(); - WebServiceDiscoverer discoverer = SlivkaWSDiscoverer.getInstance(); - discoverer.addServiceChangeListener((disc, srvcs) -> buildWebServicesMenu()); ++ SlivkaWSDiscoverer discoverer = SlivkaWSDiscoverer.getInstance(); + discoverer.addServiceChangeListener(this); } if (Cache.getDefault("SHOW_JWS2_SERVICES", true)) { @@@ -1089,9 -1052,10 +1117,10 @@@ /** * Set the enabled state of the 'Run Groovy' option in the Calculate menu - * + * * @param b */ + public void setGroovyEnabled(boolean b) { runGroovy.setEnabled(b); @@@ -1101,9 -1065,10 +1130,10 @@@ /* * (non-Javadoc) - * + * * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long) */ + @Override public void setProgressBar(String message, long id) { @@@ -1124,9 -1089,10 +1154,10 @@@ } /** - * + * * @return true if any progress bars are still active */ + @Override public boolean operationInProgress() { @@@ -1147,9 -1114,10 +1179,10 @@@ /* * Added so Castor Mapping file can obtain Jalview Version */ + public String getVersion() { - return jalview.bin.Cache.getProperty("VERSION"); + return Cache.getProperty("VERSION"); } public FeatureRenderer getFeatureRenderer() @@@ -1697,19 -1660,10 +1737,18 @@@ { String tempFilePath = doBackup ? backupfiles.getTempFilePath() : file; + Console.trace("ALIGNFRAME setting PrintWriter"); PrintWriter out = new PrintWriter(new FileWriter(tempFilePath)); + if (backupfiles != null) + { + Console.trace("ALIGNFRAME about to write to temp file " + + backupfiles.getTempFilePath()); + } - out.print(output); + Console.trace("ALIGNFRAME about to close file"); out.close(); + Console.trace("ALIGNFRAME closed file"); AlignFrame.this.setTitle(file); statusBar.setText(MessageManager.formatMessage( "label.successfully_saved_to_file_in_format", @@@ -1771,9 -1707,10 +1810,10 @@@ * Outputs the alignment to textbox in the requested format, if necessary * first prompting the user for whether to include hidden regions or * non-sequence data - * + * * @param fileFormatName */ + @Override protected void outputText_actionPerformed(String fileFormatName) { @@@ -1855,9 -1795,10 +1898,10 @@@ /** * Creates a PNG image of the alignment and writes it to the given file. If * the file is null, the user is prompted to choose a file. - * + * * @param f */ + @Override public void createPNG(File f) { @@@ -1867,9 -1808,10 +1911,10 @@@ /** * Creates an EPS image of the alignment and writes it to the given file. If * the file is null, the user is prompted to choose a file. - * + * * @param f */ + @Override public void createEPS(File f) { @@@ -1879,9 -1821,10 +1924,10 @@@ /** * Creates an SVG image of the alignment and writes it to the given file. If * the file is null, the user is prompted to choose a file. - * + * * @param f */ + @Override public void createSVG(File f) { @@@ -1949,9 -1894,10 +1997,10 @@@ /** * Close the current view or all views in the alignment frame. If the frame * only contains one view then the alignment will be removed from memory. - * + * * @param closeAllTabs */ + @Override public void closeMenuItem_actionPerformed(boolean closeAllTabs) { @@@ -2003,9 -1949,10 +2052,10 @@@ /** * Close the specified panel and close up tabs appropriately. - * + * * @param panelToClose */ + public void closeView(AlignmentPanel panelToClose) { int index = tabbedPane.getSelectedIndex(); @@@ -2079,9 -2027,10 +2130,10 @@@ } /** - * + * * @return alignment objects for all views */ + AlignmentI[] getViewAlignments() { if (alignPanels != null) @@@ -2220,37 -2173,20 +2276,36 @@@ } /** - * DOCUMENT ME! - * + * Calls AlignmentI.moveSelectedSequencesByOne with current sequence selection + * or the sequence under cursor in keyboard mode + * * @param up - * DOCUMENT ME! + * or down (if !up) */ + public void moveSelectedSequences(boolean up) { SequenceGroup sg = viewport.getSelectionGroup(); if (sg == null) { + if (viewport.cursorMode) + { + sg = new SequenceGroup(); + sg.addSequence(viewport.getAlignment().getSequenceAt( + alignPanel.getSeqPanel().seqCanvas.cursorY), false); + } + else + { + return; + } + } + + if (sg.getSize() < 1) + { return; } - + // TODO: JAL-3733 - add an event to the undo buffer for this ! - viewport.getAlignment().moveSelectedSequencesByOne(sg, viewport.getHiddenRepSequences(), up); alignPanel.paintAlignment(true, false); @@@ -3208,10 -3160,12 +3279,11 @@@ } /** - * DOCUMENT ME! - * + * Opens a Finder dialog + * * @param e - * DOCUMENT ME! */ + @Override public void findMenuItem_actionPerformed(ActionEvent e) { @@@ -3421,9 -3383,10 +3501,10 @@@ /* * (non-Javadoc) - * + * * @see jalview.jbgui.GAlignFrame#followHighlight_actionPerformed() */ + @Override protected void followHighlight_actionPerformed() { @@@ -3748,12 -3724,13 +3842,13 @@@ /** * Action on toggle of the 'Show annotations' menu item. This shows or hides * the annotations panel as a whole. - * + * * The options to show/hide all annotations should be enabled when the panel * is shown, and disabled when the panel is hidden. - * + * * @param e */ + @Override public void annotationPanelMenuItem_actionPerformed(ActionEvent e) { @@@ -3918,9 -3900,10 +4018,10 @@@ * Action on the user checking or unchecking the option to apply the selected * colour scheme to all groups. If unchecked, groups may have their own * independent colour schemes. - * + * * @param selected */ + @Override public void applyToAllGroups_actionPerformed(boolean selected) { @@@ -3957,9 -3941,10 +4059,10 @@@ /** * Actions on setting or changing the alignment colour scheme - * + * * @param cs */ + @Override public void changeColour(ColourSchemeI cs) { @@@ -4126,9 -4119,10 +4237,10 @@@ alignPanel.paintAlignment(true, false); } + /** * DOCUMENT ME! - * + * * @param e * DOCUMENT ME! */ @@@ -4325,8 -4329,9 +4444,9 @@@ * search the alignment and rebuild the sort by annotation score submenu the * last alignment annotation vector hash is stored to minimize cost of * rebuilding in subsequence calls. - * + * */ + @Override public void buildSortByAnnotationScoresMenu() { @@@ -4439,8 -4447,9 +4562,9 @@@ /** * Work out whether the whole set of sequences or just the selected set will * be submitted for multiple alignment. - * + * */ + public jalview.datamodel.AlignmentView gatherSequencesForAlignment() { // Now, check we have enough sequences @@@ -4714,11 -4726,39 +4841,38 @@@ } } + private void buildWebServicesMenu(WebServiceDiscoverer discoverer, final JMenu menu) + { + if (discoverer.hasServices()) + { + var builder = new WebServicesMenuBuilder(); + for (var service : discoverer.getServices()) + builder.addAllOperations(service.getOperations()); + builder.addSelectedHostChangeListener((name, op) -> { + menu.removeAll(); + builder.buildMenu(menu, this); + }); + builder.buildMenu(menu, this); + } + if (discoverer.isRunning()) + { + JMenuItem item = new JMenuItem("Service discovery in progress."); + item.setEnabled(false); + menu.add(item); + } + else if (!discoverer.hasServices()) + { + JMenuItem item = new JMenuItem("No services available."); + item.setEnabled(false); + menu.add(item); + } + } - /** * construct any groupURL type service menu entries. - * + * * @param webService */ + protected void build_urlServiceMenu(JMenu webService) { // TODO: remove this code when 2.7 is released @@@ -4753,9 -4793,10 +4907,10 @@@ * Cross-References menu (formerly called Show Products), with database * sources for which cross-references are found (protein sources for a * nucleotide alignment and vice versa) - * + * * @return true if Show Cross-references menu should be enabled */ + public boolean canShowProducts() { SequenceI[] seqs = viewport.getAlignment().getSequencesArray(); @@@ -4880,9 -4924,10 +5038,10 @@@ /** * Set the file format - * + * * @param format */ + public void setFileFormat(FileFormatI format) { this.currentFileFormat = format; @@@ -5476,9 -5526,10 +5640,10 @@@ /** * find the viewport amongst the tabs in this alignment frame and close that * tab - * + * * @param av */ + public boolean closeView(AlignViewportI av) { if (viewport == av) @@@ -5977,9 -6045,10 +6159,10 @@@ /** * make the given alignmentPanel the currently selected tab - * + * * @param alignmentPanel */ + public void setDisplayedView(AlignmentPanel alignmentPanel) { if (!viewport.getSequenceSetId() @@@ -6047,9 -6118,10 +6232,10 @@@ } /** - * + * * @return alignment panels in this alignment frame */ + public List getAlignPanels() { // alignPanels is never null @@@ -6108,9 -6181,10 +6295,10 @@@ /** * Set visibility of dna/protein complement view (available when shown in a * split frame). - * + * * @param show */ + @Override protected void showComplement_actionPerformed(boolean show) { @@@ -6399,4 -6480,6 +6594,5 @@@ } } } - } + diff --cc src/jalview/gui/Desktop.java index b125cc2,0c3b553..6a10dde --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@@ -194,8 -169,6 +194,7 @@@ public class Desktop extends GDeskto @SuppressWarnings("deprecation") private JalviewChangeSupport changeSupport = new JalviewChangeSupport(); + public static boolean nosplash = false; - /** * news reader - null if it was never started. */ @@@ -422,77 -395,36 +421,78 @@@ */ doConfigureStructurePrefs(); - setTitle("Jalview " + Cache.getProperty("VERSION")); - /* - if (!Platform.isAMac()) - { - // this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); - } - else + setTitle(ChannelProperties.getProperty("app_name") + " " + 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. + */ + if (Platform.isLinux()) + { + if (LaunchUtils.getJavaVersion() >= 11) { - this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + jalview.bin.Console.info( + "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that."); } - */ + try { - APQHandlers.setAPQHandlers(this); - } catch (Throwable t) - { - System.out.println("Error setting APQHandlers: " + t.toString()); - // t.printStackTrace(); - } + Toolkit xToolkit = Toolkit.getDefaultToolkit(); + Field[] declaredFields = xToolkit.getClass().getDeclaredFields(); + Field awtAppClassNameField = null; - addWindowListener(new WindowAdapter() - { + if (Arrays.stream(declaredFields).anyMatch(f -> f.getName().equals("awtAppClassName"))) { + awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName"); + } - @Override - public void windowClosing(WindowEvent ev) + String title = ChannelProperties.getProperty("app_name"); + if (awtAppClassNameField != null) { + awtAppClassNameField.setAccessible(true); + awtAppClassNameField.set(xToolkit, title); + } + else { - quit(); + jalview.bin.Console.debug("XToolkit: awtAppClassName not found"); } - }); + } catch (Exception e) + { + jalview.bin.Console.debug("Error setting awtAppClassName"); + jalview.bin.Console.trace(Cache.getStackTraceString(e)); + } + } + + /** + * 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 { + APQHandlers.setAPQHandlers(this); + } catch (Exception e) { + System.out.println("Cannot set APQHandlers"); + // e.printStackTrace(); + } catch (Throwable t) { + jalview.bin.Console.warn("Error setting APQHandlers: " + t.toString()); + jalview.bin.Console.trace(Cache.getStackTraceString(t)); + } + + setIconImages(ChannelProperties.getIconList()); + + addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent ev) { + quit(); + } + }); boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false); @@@ -541,6 -472,8 +540,7 @@@ setBounds(xPos, yPos, 900, 650); } - getIdentifiersOrgData(); + if (!Platform.isJS()) /** * Java only @@@ -556,24 -489,21 +556,24 @@@ experimentalFeatures.setSelected(showExperimental()); + getIdentifiersOrgData(); - if (Jalview.isInteractive()) { // disabled for SeqCanvasTest checkURLLinks(); // Spawn a thread that shows the splashscreen + + if (!nosplash) { SwingUtilities.invokeLater(new Runnable() - { - @Override - public void run() - { - new SplashScreen(true); - } - }); + { + @Override + public void run() + { + new SplashScreen(true); + } + }); + } // Thread off a new instance of the file chooser - this reduces the // time @@@ -689,26 -628,27 +690,27 @@@ }).start(); } - public void getIdentifiersOrgData() - { - // Thread off the identifiers fetcher - new Thread(new Runnable() - { - @Override - public void run() - { - Cache.log.debug("Downloading data from identifiers.org"); - try - { - UrlDownloadClient.download(IdOrgSettings.getUrl(), - IdOrgSettings.getDownloadLocation()); - } catch (IOException e) + public void getIdentifiersOrgData() { + if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null) { + // Thread off the identifiers fetcher + new Thread(new Runnable() { + @Override + public void run() { - Cache.log.debug("Exception downloading identifiers.org data" - + e.getMessage()); + jalview.bin.Console.debug("Downloading data from identifiers.org"); + try + { + UrlDownloadClient.download(IdOrgSettings.getUrl(), + IdOrgSettings.getDownloadLocation()); + } catch (IOException e) + { + jalview.bin.Console.debug("Exception downloading identifiers.org data" + + e.getMessage()); + } } - } - }).start(); + }).start(); + } + } @Override @@@ -833,8 -778,57 +835,7 @@@ } } -// /** -// * Add an internal frame to the Jalview desktop that is allowed to be resized, -// * has a minimum size of 300px and might or might not be visible -// * -// * @param frame -// * Frame to show -// * @param title -// * Visible Title -// * @param makeVisible -// * When true, display frame immediately, otherwise, caller must call -// * setVisible themselves. -// * @param w -// * width -// * @param h -// * height -// */ -// @Deprecated -// public static synchronized void addInternalFrame( -// final JInternalFrame frame, String title, boolean makeVisible, -// int w, int h) -// { -// // textbox, web services, sequenceFetcher, featureSettings -// getInstance().addFrame(frame, title, makeVisible, w, h, -// FRAME_ALLOW_RESIZE, FRAME_SET_MIN_SIZE_300); -// } -// -// /** -// * Add an internal frame to the Jalview desktop that is visible, has a minimum -// * size of 300px, and may or may not be resizable -// * -// * @param frame -// * Frame to show -// * @param title -// * Visible Title -// * @param w -// * width -// * @param h -// * height -// * @param resizable -// * Allow resize -// */ -// @Deprecated -// public static synchronized void addInternalFrame( -// final JInternalFrame frame, String title, int w, int h, -// boolean resizable) -// { -// // annotation, font, calculation, user-defined colors -// getInstance().addFrame(frame, title, FRAME_MAKE_VISIBLE, w, h, -// resizable, FRAME_SET_MIN_SIZE_300); -// } - /** * Adds and opens the given frame to the desktop that is visible, allowed to * resize, and has a 300px minimum width. @@@ -882,8 -876,7 +883,7 @@@ int w, int h, boolean resizable, boolean ignoreMinSize) { // 15 classes call this method directly. - + - // TODO: allow callers to determine X and Y position of frame (eg. via // bounds object). // TODO: consider fixing method to update entries in the window submenu with @@@ -919,16 -912,14 +919,14 @@@ { openFrameCount++; -- + boolean isEmbedded = (Platform.getEmbeddedAttribute(frame, "id") != null); boolean hasEmbeddedSize = (Platform.getDimIfEmbedded(frame, -1, -1) != null); // Web page embedding allows us to ignore minimum size ignoreMinSize |= hasEmbeddedSize; - + if (!ignoreMinSize) { - // Set default dimension for Alignment Frame window. // The Alignment Frame window could be added from a number of places, // hence, @@@ -1460,8 -1471,6 +1456,7 @@@ message.append(Cache.getDefault("AUTHORFNAMES", DEFAULT_AUTHORS)); message.append(CITATION); + message.append(""); - return message.toString(); } @@@ -2337,9 -2352,6 +2331,8 @@@ 10, getHeight() - fm.getHeight()); } } - + // output debug scale message. Important for jalview.bin.HiDPISettingTest2 + Desktop.debugScaleMessage(Desktop.getDesktopPane().getGraphics()); } } @@@ -2697,15 -2706,9 +2690,14 @@@ startServiceDiscovery(false); } + /** + * start service discovery threads - blocking or non-blocking + * + * @param blocking + */ public void startServiceDiscovery(boolean blocking) { - System.out.println("Starting service discovery"); + jalview.bin.Console.debug("Starting service discovery"); - var tasks = new ArrayList>(); // JAL-940 - JALVIEW 1 services are now being EOLed as of JABA 2.1 release diff --cc src/jalview/gui/SlivkaPreferences.java index 00e4a89,ddca49f..a332b2b --- a/src/jalview/gui/SlivkaPreferences.java +++ b/src/jalview/gui/SlivkaPreferences.java @@@ -1,10 -1,10 +1,11 @@@ package jalview.gui; import jalview.bin.Cache; +import jalview.bin.Console; import jalview.util.MessageManager; import jalview.ws.WSDiscovererI; - import jalview.ws.slivkaws.SlivkaWSDiscoverer; + import jalview.ws2.slivka.SlivkaWSDiscoverer; + import jalview.ws2.WebServiceDiscoverer; import java.awt.BorderLayout; import java.awt.Color; diff --cc src/jalview/io/SequenceAnnotationReport.java index a287cb8,6adc419..d2e8aba --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@@ -551,9 -542,10 +559,9 @@@ public class SequenceAnnotationRepor sb.append(bitScore); maxWidth = Math.max(maxWidth, eValue.length()); maxWidth = Math.max(maxWidth, bitScore.length()); + sb.append("
"); } - sb.append("
"); sb.append(""); - return maxWidth; } diff --cc src/jalview/jbgui/GPreferences.java index b5cb96c,e06e6aa..dfe08ed --- a/src/jalview/jbgui/GPreferences.java +++ b/src/jalview/jbgui/GPreferences.java @@@ -498,14 -430,12 +498,15 @@@ public class GPreferences extends JPane */ if (!Platform.isJS()) { + tabbedPane.add(initHMMERTab(), MessageManager.getString("label.hmmer")); + tabbedPane.add(initStartupTab(), + MessageManager.getString("label.startup")); wsTab.setLayout(new BorderLayout()); tabbedPane.add(wsTab, MessageManager.getString("label.web_services")); - slivkaTab.setLayout(new BorderLayout()); - tabbedPane.add(slivkaTab, "Slivka Services"); } + + slivkaTab.setLayout(new BorderLayout()); + tabbedPane.add(slivkaTab, "Slivka Services"); /* * Handler to validate a tab before leaving it - currently only for diff --cc src/jalview/ws/gui/MsaWSThread.java index 94e65aa,48846ef..9298fb5 --- a/src/jalview/ws/gui/MsaWSThread.java +++ b/src/jalview/ws/gui/MsaWSThread.java @@@ -290,6 -288,7 +290,7 @@@ public class MsaWSThread extends AWSThr } catch (Throwable throwable) { - Cache.log.error("failed to send the job to the alignment server", throwable); ++ Console.error("failed to send the job to the alignment server", throwable); if (!server.handleSubmitError(throwable, j, wsInfo)) { if (throwable instanceof Exception) diff --cc src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java index 82784c3,c42d42e..999951a --- a/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java +++ b/src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java @@@ -82,15 -83,21 +84,21 @@@ public class SlivkaAnnotationServiceIns } Alignment aln = new Alignment(seqs.toArray(new SequenceI[0])); if (annotFile == null - || !new AnnotationFile().readAnnotationFile(aln, annotFile.getURL().toString(), DataSourceType.URL)) + || !new AnnotationFile().readAnnotationFileWithCalcId(aln, service.getId(), annotFile.getContentUrl().toString(), DataSourceType.URL)) { - Cache.log.debug("No annotation from slivka job\n" + annotFile); + Console.debug("No annotation from slivka job\n" + annotFile); } + else { - Cache.log.debug("Annotation file loaded " + annotFile); ++ Console.debug("Annotation file loaded " + annotFile); + } if (featFile == null - || !new FeaturesFile(featFile.getURL().toString(), DataSourceType.URL).parse(aln, featureColours, true)) + || !new FeaturesFile(featFile.getContentUrl().toString(), DataSourceType.URL).parse(aln, featureColours, true)) { - Cache.log.debug("No features from slivka job\n" + featFile); + Console.debug("No features from slivka job\n" + featFile); } + else { - Cache.log.debug("Features feil loaded " + featFile); ++ Console.debug("Features feil loaded " + featFile); + } return Arrays.asList(aln.getAlignmentAnnotation()); } } diff --cc src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java index 9a33b04,b992fbe..374d2eb --- a/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java +++ b/src/jalview/ws/slivkaws/SlivkaMsaServiceInstance.java @@@ -1,5 -1,6 +1,6 @@@ package jalview.ws.slivkaws; -import jalview.bin.Cache; ++ import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceI; import jalview.io.DataSourceType; @@@ -10,7 -11,7 +11,6 @@@ import jalview.ws.api.MultipleSequenceA import jalview.ws.params.ArgumentI; import jalview.ws.params.InvalidArgumentException; import jalview.ws.params.WsParamSetI; -- import java.io.IOError; import java.io.IOException; import java.rmi.ServerError; diff --cc src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java index 0c94c4d,50fa2d6..9369c6b --- a/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java +++ b/src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java @@@ -216,9 -217,9 +218,9 @@@ public class SlivkaWSDiscoverer impleme { List services = new SlivkaClient(url).getServices(); return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK; - } catch (IOException e) + } catch (IOException | org.json.JSONException e) { - Cache.log.error("Slivka could not retrieve services list", e); + Console.error("Slivka could not retrieve services list", e); return STATUS_INVALID; } } diff --cc src/jalview/ws2/PollingTaskExecutor.java index 0000000,3e04c17..92044c6 mode 000000,100644..100644 --- a/src/jalview/ws2/PollingTaskExecutor.java +++ b/src/jalview/ws2/PollingTaskExecutor.java @@@ -1,0 -1,126 +1,127 @@@ + package jalview.ws2; + + import java.util.List; + import java.util.concurrent.CopyOnWriteArrayList; + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; + + import jalview.bin.Cache; ++import jalview.bin.Console; + + public class PollingTaskExecutor + { + private ScheduledExecutorService executor = Executors + .newSingleThreadScheduledExecutor(); + + public void submit(final PollableTaskI task) + { + executor.submit(() -> { + try + { + task.start(); + wsThreadSupport.submitted(task); + } catch (Exception e) + { - Cache.log.error("Failed to submit web service jobs.", e); ++ Console.error("Failed to submit web service jobs.", e); + wsThreadSupport.submissionFailed(task, e); + return; + } + executor.schedule(() -> poll(task), 1, TimeUnit.SECONDS); + }); + } + + private void poll(PollableTaskI task) + { + boolean done; + try + { + done = task.poll(); + } catch (Exception e) + { - Cache.log.error("Failed to poll task.", e); ++ Console.error("Failed to poll task.", e); + wsThreadSupport.pollFailed(task, e); + return; + } + if (!done) + { + executor.schedule(() -> poll(task), 1, TimeUnit.SECONDS); + } + else + { + task.done(); + wsThreadSupport.done(task); + } + } + + private WebServiceThreadSupport wsThreadSupport = new WebServiceThreadSupport(); + + public void addThreadListener(PollableTaskListenerI listener) + { + wsThreadSupport.addListener(listener); + } + + public void removeThreadListener(PollableTaskListenerI listener) + { + wsThreadSupport.removeListener(listener); + } + + public void shutdown() + { + executor.shutdownNow(); + } + + } + + class WebServiceThreadSupport implements PollableTaskListenerI + { + List listeners = new CopyOnWriteArrayList<>(); + + @Override + public void submitted(PollableTaskI task) + { + for (var listener : listeners) + listener.submitted(task); + } + + @Override + public void submissionFailed(PollableTaskI task, Exception e) + { + for (var listener : listeners) + listener.submissionFailed(task, e); + } + + @Override + public void pollFailed(PollableTaskI task, Exception e) + { + for (var listener : listeners) + listener.pollFailed(task, e); + } + + @Override + public void cancelled(PollableTaskI task) + { + for (var listener : listeners) + listener.cancelled(task); + } + + @Override + public void done(PollableTaskI task) + { + for (var listener : listeners) + listener.done(task); + } + + public void addListener(PollableTaskListenerI listener) + { + if (!listeners.contains(listener)) + { + listeners.add(listener); + } + } + + public void removeListener(PollableTaskListenerI listener) + { + listeners.remove(listener); + } + } diff --cc src/jalview/ws2/operations/AlignmentOperation.java index 0000000,89aeb79..d85d012 mode 000000,100644..100644 --- a/src/jalview/ws2/operations/AlignmentOperation.java +++ b/src/jalview/ws2/operations/AlignmentOperation.java @@@ -1,0 -1,834 +1,835 @@@ + package jalview.ws2.operations; + + import static java.lang.String.format; + + import java.awt.event.MouseAdapter; + import java.awt.event.MouseEvent; + import java.io.IOException; + import java.util.ArrayList; + import java.util.Collections; + import java.util.HashMap; + import java.util.Hashtable; + import java.util.LinkedHashMap; + import java.util.List; + import java.util.Map; + import java.util.Objects; + import java.util.concurrent.CompletionStage; + + import javax.swing.JMenu; + import javax.swing.JMenuItem; + import javax.swing.ToolTipManager; + + import jalview.analysis.AlignSeq; + import jalview.analysis.AlignmentSorter; + import jalview.analysis.SeqsetUtils; + import jalview.bin.Cache; ++import jalview.bin.Console; + import jalview.datamodel.AlignedCodonFrame; + import jalview.datamodel.Alignment; + import jalview.datamodel.AlignmentI; + import jalview.datamodel.AlignmentOrder; + import jalview.datamodel.AlignmentView; + import jalview.datamodel.HiddenColumns; + import jalview.datamodel.SequenceI; + import jalview.datamodel.Sequence; + import jalview.gui.AlignFrame; + import jalview.gui.AlignViewport; + import jalview.gui.Desktop; + import jalview.gui.JvSwingUtils; + import jalview.gui.WebserviceInfo; + import jalview.gui.WsJobParameters; + import jalview.util.MathUtils; + import jalview.util.MessageManager; + import jalview.ws.params.ArgumentI; + import jalview.ws.params.WsParamSetI; + import jalview.ws2.MenuEntryProviderI; + import jalview.ws2.ResultSupplier; + import jalview.ws2.WSJob; + import jalview.ws2.WSJobStatus; + import jalview.ws2.PollingTaskExecutor; + import jalview.ws2.WebServiceI; + import jalview.ws2.WebServiceInfoUpdater; + import jalview.ws2.WebServiceWorkerI; + import jalview.ws2.utils.WSJobList; + + /** + * + * @author mmwarowny + * + */ + public class AlignmentOperation implements Operation + { + final WebServiceI service; + + final ResultSupplier supplier; + + public AlignmentOperation(WebServiceI service, + ResultSupplier supplier) + { + this.service = service; + this.supplier = supplier; + } + + @Override + public String getName() + { + return service.getName(); + } + + @Override + public String getTypeName() + { + return "Multiple Sequence Alignment"; + } + + @Override + public String getHostName() + { + return service.getHostName(); + } + + @Override + public int getMinSequences() + { + return 2; + } + + @Override + public int getMaxSequences() + { + return Integer.MAX_VALUE; + } + + @Override + public boolean isProteinOperation() + { + return true; + } + + @Override + public boolean isNucleotideOperation() + { + return true; + } + + @Override + public boolean isAlignmentAnalysis() + { + return false; + } + + @Override + public boolean canSubmitGaps() + { + // hack copied from original jabaws code, don't blame me + return service.getName().contains("lustal"); + } + + @Override + public boolean isInteractive() + { + return false; + } + + @Override + public boolean getFilterNonStandardSymbols() + { + return true; + } + + @Override + public boolean getNeedsAlignedSequences() + { + return false; + } + + @Override + public MenuEntryProviderI getMenuBuilder() + { + return this::buildMenu; + } + + protected void buildMenu(JMenu parent, AlignFrame frame) + { + if (canSubmitGaps()) + { + var alignSubmenu = new JMenu(service.getName()); + buildMenu(alignSubmenu, frame, false); + parent.add(alignSubmenu); + var realignSubmenu = new JMenu(MessageManager.formatMessage( + "label.realign_with_params", service.getName())); + realignSubmenu.setToolTipText(MessageManager + .getString("label.align_sequences_to_existing_alignment")); + buildMenu(realignSubmenu, frame, true); + parent.add(realignSubmenu); + } + else + { + buildMenu(parent, frame, false); + } + } + + protected void buildMenu(JMenu parent, AlignFrame frame, + boolean submitGaps) + { + final String action = submitGaps ? "Align" : "Realign"; + final var calcName = service.getName(); + + String title = frame.getTitle(); + PollingTaskExecutor executor = frame.getViewport().getWSExecutor(); + { + var item = new JMenuItem(MessageManager.formatMessage( + "label.calcname_with_default_settings", calcName)); + item.setToolTipText(MessageManager + .formatMessage("label.action_with_default_settings", action)); + item.addActionListener((event) -> { + final AlignmentView msa = frame.gatherSequencesForAlignment(); + final AlignViewport viewport = frame.getViewport(); + final AlignmentI alignment = frame.getViewport().getAlignment(); + if (msa != null) + { + WebServiceWorkerI worker = new AlignmentWorker(msa, + Collections.emptyList(), title, submitGaps, true, + alignment, viewport); + executor.submit(worker); + } + }); + parent.add(item); + } + + if (service.hasParameters()) + { + var item = new JMenuItem( + MessageManager.getString("label.edit_settings_and_run")); + item.setToolTipText(MessageManager.getString( + "label.view_and_change_parameters_before_alignment")); + item.addActionListener((event) -> { + final AlignmentView msa = frame.gatherSequencesForAlignment(); + final AlignViewport viewport = frame.getViewport(); + final AlignmentI alignment = frame.getViewport().getAlignment(); + if (msa != null) + { + openEditParamsDialog(service, null, null) + .thenAcceptAsync((arguments) -> { + if (arguments != null) + { + WebServiceWorkerI worker = new AlignmentWorker(msa, + arguments, title, submitGaps, true, alignment, + viewport); + executor.submit(worker); + } + }); + } + }); + parent.add(item); + } + + var presets = service.getParamStore().getPresets(); + if (presets != null && presets.size() > 0) + { + final var presetList = new JMenu(MessageManager + .formatMessage("label.run_with_preset_params", calcName)); + final var showToolTipFor = ToolTipManager.sharedInstance() + .getDismissDelay(); + for (final var preset : presets) + { + var item = new JMenuItem(preset.getName()); + final int QUICK_TOOLTIP = 1500; + item.addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + ToolTipManager.sharedInstance().setDismissDelay(QUICK_TOOLTIP); + } + + @Override + public void mouseExited(MouseEvent e) + { + ToolTipManager.sharedInstance().setDismissDelay(showToolTipFor); + } + }); + String tooltip = JvSwingUtils.wrapTooltip(true, + format("%s
%s", + MessageManager.getString( + preset.isModifiable() ? "label.user_preset" + : "label.service_preset"), + preset.getDescription())); + item.setToolTipText(tooltip); + item.addActionListener((event) -> { + final AlignmentView msa = frame.gatherSequencesForAlignment(); + final AlignViewport viewport = frame.getViewport(); + final AlignmentI alignment = frame.getViewport().getAlignment(); + if (msa != null) + { + WebServiceWorkerI worker = new AlignmentWorker(msa, + preset.getArguments(), title, submitGaps, true, + alignment, viewport); + executor.submit(worker); + } + }); + presetList.add(item); + } + parent.add(presetList); + } + } + + private CompletionStage> openEditParamsDialog( + WebServiceI service, WsParamSetI preset, + List arguments) + { + WsJobParameters jobParams; + if (preset == null && arguments != null && arguments.size() > 0) + jobParams = new WsJobParameters(service.getParamStore(), preset, + arguments); + else + jobParams = new WsJobParameters(service.getParamStore(), preset, + null); + var stage = jobParams.showRunDialog(); + return stage.thenApply((startJob) -> { + if (startJob) + { + if (jobParams.getPreset() == null) + { + return jobParams.getJobParams(); + } + else + { + return jobParams.getPreset().getArguments(); + } + } + else + { + return null; + } + }); + } + + /** + * Implementation of the web service worker performing multiple sequence + * alignment. + * + * @author mmwarowny + * + */ + private class AlignmentWorker implements WebServiceWorkerI + { + + private long uid = MathUtils.getUID(); + + private final AlignmentView msa; + + private final AlignmentI dataset; + + private final AlignViewport viewport; + + private final List codonFrame = new ArrayList<>(); + + private List args = Collections.emptyList(); + + private String alnTitle = ""; + + private boolean submitGaps = false; + + private boolean preserveOrder = false; + + private char gapCharacter; + + private WSJobList jobs = new WSJobList(); + + private Map inputs = new LinkedHashMap<>(); + + private WebserviceInfo wsInfo; + + private Map exceptionCount = new HashMap<>(); + + private final int MAX_RETRY = 5; + + AlignmentWorker(AlignmentView msa, List args, + String alnTitle, boolean submitGaps, boolean preserveOrder, + AlignmentI alignment, AlignViewport viewport) + { + this.msa = msa; + this.dataset = alignment.getDataset(); + List cf = Objects.requireNonNullElse( + alignment.getCodonFrames(), Collections.emptyList()); + this.codonFrame.addAll(cf); + this.args = args; + this.alnTitle = alnTitle; + this.submitGaps = submitGaps; + this.preserveOrder = preserveOrder; + this.viewport = viewport; + this.gapCharacter = viewport.getGapCharacter(); + + String panelInfo = String.format("%s using service hosted at %s%n%s", + service.getName(), service.getHostName(), + Objects.requireNonNullElse(service.getDescription(), "")); + wsInfo = new WebserviceInfo(service.getName(), panelInfo, false); + } + + @Override + public long getUID() + { + return uid; + } + + @Override + public WebServiceI getWebService() + { + return service; + } + + @Override + public List getJobs() + { + return Collections.unmodifiableList(jobs); + } + + @Override + public void start() throws IOException + { - Cache.log.info(format("Starting new %s job.", service.getName())); ++ Console.info(format("Starting new %s job.", service.getName())); + String outputHeader = String.format("%s of %s%nJob details%n", + submitGaps ? "Re-alignment" : "Alignment", alnTitle); + SequenceI[][] conmsa = msa.getVisibleContigs('-'); + if (conmsa == null) + { + return; + } + WebServiceInfoUpdater updater = new WebServiceInfoUpdater(wsInfo); + updater.setOutputHeader(outputHeader); + int numValid = 0; + for (int i = 0; i < conmsa.length; i++) + { + JobInput input = JobInput.create(conmsa[i], 2, submitGaps); + WSJob job = new WSJob(service.getProviderName(), service.getName(), + service.getHostName()); + job.setJobNum(wsInfo.addJobPane()); + if (conmsa.length > 1) + { + wsInfo.setProgressName(String.format("region %d", i), + job.getJobNum()); + } + wsInfo.setProgressText(job.getJobNum(), outputHeader); + job.addPropertyChangeListener(updater); + inputs.put(job.getUid(), input); + jobs.add(job); + if (input.isInputValid()) + { + int count; + String jobId = null; + do + { + count = exceptionCount.getOrDefault(job.getUid(), MAX_RETRY); + try + { + jobId = service.submit(input.inputSequences, args); - Cache.log.debug((format("Job %s submitted", job))); ++ Console.debug((format("Job %s submitted", job))); + exceptionCount.remove(job.getUid()); + } catch (IOException e) + { + exceptionCount.put(job.getUid(), --count); + } + } while (jobId == null && count > 0); + if (jobId != null) + { + job.setJobId(jobId); + job.setStatus(WSJobStatus.SUBMITTED); + numValid++; + } + else + { + job.setStatus(WSJobStatus.SERVER_ERROR); + } + } + else + { + job.setStatus(WSJobStatus.INVALID); + job.setErrorLog( + MessageManager.getString("label.empty_alignment_job")); + } + } + if (numValid > 0) + { + // wsInfo.setThisService() should happen here + wsInfo.setVisible(true); + } + else + { + wsInfo.setVisible(false); + // TODO show notification dialog. + // JvOptionPane.showMessageDialog(frame, + // MessageManager.getString("info.invalid_msa_input_mininfo"), + // MessageManager.getString("info.invalid_msa_notenough"), + // JvOptionPane.INFORMATION_MESSAGE); + } + } + + @Override + public boolean poll() + { + boolean done = true; + for (WSJob job : getJobs()) + { + if (!job.getStatus().isDone() && !job.getStatus().isFailed()) + { - Cache.log.debug(format("Polling job %s.", job)); ++ Console.debug(format("Polling job %s.", job)); + try + { + service.updateProgress(job); + exceptionCount.remove(job.getUid()); + } catch (IOException e) + { - Cache.log.error(format("Polling job %s failed.", job), e); ++ Console.error(format("Polling job %s failed.", job), e); + wsInfo.appendProgressText(job.getJobNum(), + MessageManager.formatMessage("info.server_exception", + service.getName(), e.getMessage())); + int count = exceptionCount.getOrDefault(job.getUid(), + MAX_RETRY); + if (--count <= 0) + { + job.setStatus(WSJobStatus.SERVER_ERROR); - Cache.log.warn(format( ++ Console.warn(format( + "Attempts limit exceeded. Droping job %s.", job)); + } + exceptionCount.put(job.getUid(), count); + } catch (OutOfMemoryError e) + { + job.setStatus(WSJobStatus.BROKEN); - Cache.log.error( ++ Console.error( + format("Out of memory when retrieving job %s", job), e); + } - Cache.log.debug( ++ Console.debug( + format("Job %s status is %s", job, job.getStatus())); + } + done &= job.getStatus().isDone() || job.getStatus().isFailed(); + } + updateWSInfoGlobalStatus(); + return done; + } + + private void updateWSInfoGlobalStatus() + { + if (jobs.countRunning() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_RUNNING); + } + else if (jobs.countQueuing() > 0 + || jobs.countSubmitted() < jobs.size()) + { + wsInfo.setStatus(WebserviceInfo.STATE_QUEUING); + } + else + { + if (jobs.countSuccessful() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_OK); + } + else if (jobs.countCancelled() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_CANCELLED_OK); + } + else if (jobs.countFailed() > 0) + { + wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR); + } + } + } + + @Override + public void done() + { + long progbarId = MathUtils.getUID(); + wsInfo.setProgressBar( + MessageManager.getString("status.collecting_job_results"), + progbarId); + Map results = new LinkedHashMap<>(); + for (WSJob job : getJobs()) + { + if (job.getStatus().isFailed()) + continue; + try + { + AlignmentI alignment = supplier.getResult(job, dataset.getSequences(), viewport); + if (alignment != null) + { + results.put(job.getUid(), alignment); + } + } catch (Exception e) + { + if (!service.handleCollectionError(job, e)) + { - Cache.log.error("Couldn't get alignment for job.", e); ++ Console.error("Couldn't get alignment for job.", e); + // TODO: Increment exception count and retry. + job.setStatus(WSJobStatus.SERVER_ERROR); + } + } + } + updateWSInfoGlobalStatus(); + if (results.size() > 0) + { + OutputWrapper out = prepareOutput(results); + wsInfo.showResultsNewFrame.addActionListener(evt -> displayNewFrame( + new Alignment(out.aln), out.alorders, out.hidden)); + wsInfo.setResultsReady(); + } + else + { + wsInfo.setFinishedNoResults(); + } + wsInfo.removeProgressBar(progbarId); + } + + private class OutputWrapper + { + AlignmentI aln; + + List alorders; + + HiddenColumns hidden; + + OutputWrapper(AlignmentI aln, List alorders, + HiddenColumns hidden) + { + this.aln = aln; + this.alorders = alorders; + this.hidden = hidden; + } + } + + private OutputWrapper prepareOutput(Map alignments) + { + List alorders = new ArrayList<>(); + SequenceI[][] results = new SequenceI[jobs.size()][]; + AlignmentOrder[] orders = new AlignmentOrder[jobs.size()]; + for (int i = 0; i < jobs.size(); i++) + { + WSJob job = jobs.get(i); + AlignmentI aln = alignments.get(job.getUid()); + if (aln != null) // equivalent of job.hasResults() + { + /* Get the alignment including any empty sequences in the original + * order with original ids. */ + JobInput input = inputs.get(job.getUid()); + char gapChar = aln.getGapCharacter(); + List emptySeqs = input.emptySequences; + List alnSeqs = aln.getSequences(); + // find the width of the longest sequence + int width = 0; + for (var seq : alnSeqs) + width = Integer.max(width, seq.getLength()); + for (var emptySeq : emptySeqs) + width = Integer.max(width, emptySeq.getLength()); + // pad shorter sequences with gaps + String gapSeq = String.join("", + Collections.nCopies(width, Character.toString(gapChar))); + List seqs = new ArrayList<>( + alnSeqs.size() + emptySeqs.size()); + seqs.addAll(alnSeqs); + seqs.addAll(emptySeqs); + for (var seq : seqs) + { + if (seq.getLength() < width) + seq.setSequence(seq.getSequenceAsString() + + gapSeq.substring(seq.getLength())); + } + SequenceI[] result = seqs.toArray(new SequenceI[0]); + AlignmentOrder msaOrder = new AlignmentOrder(result); + AlignmentSorter.recoverOrder(result); + // temporary workaround for deuniquify + @SuppressWarnings({ "rawtypes", "unchecked" }) + Hashtable names = new Hashtable(input.sequenceNames); + // FIXME first call to deuniquify alters original alignment + SeqsetUtils.deuniquify(names, result); + alorders.add(msaOrder); + results[i] = result; + orders[i] = msaOrder; + } + else + { + results[i] = null; + } + } + + Object[] newView = msa.getUpdatedView(results, orders, gapCharacter); + // free references to original data + for (int i = 0; i < jobs.size(); i++) + { + results[i] = null; + orders[i] = null; + } + SequenceI[] alignment = (SequenceI[]) newView[0]; + HiddenColumns hidden = (HiddenColumns) newView[1]; + Alignment aln = new Alignment(alignment); + aln.setProperty("Alignment Program", service.getName()); + if (dataset != null) + aln.setDataset(dataset); + + propagateDatasetMappings(aln); + return new OutputWrapper(aln, alorders, hidden); + // displayNewFrame(aln, alorders, hidden); + } + + /* + * conserves dataset references to sequence objects returned from web + * services. propagate codon frame data to alignment. + */ + private void propagateDatasetMappings(Alignment aln) + { + if (codonFrame != null) + { + SequenceI[] alignment = aln.getSequencesArray(); + for (SequenceI seq : alignment) + { + for (AlignedCodonFrame acf : codonFrame) + { + if (acf != null && acf.involvesSequence(seq)) + { + aln.addCodonFrame(acf); + break; + } + } + } + } + } + + private void displayNewFrame(AlignmentI aln, + List alorders, HiddenColumns hidden) + { + AlignFrame frame = new AlignFrame(aln, hidden, + AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT); + // TODO store feature renderer settings in worker object + // frame.getFeatureRenderer().transferSettings(featureSettings); + var regions = sortOrders(alorders); + if (alorders.size() == 1) + { + frame.addSortByOrderMenuItem( + format("%s Ordering", service.getName()), alorders.get(0)); + } + else + { + for (int i = 0; i < alorders.size(); i++) + { + final int j = i; + Iterable iter = () -> regions.get(j).stream() + .map(it -> Integer.toString(it)).iterator(); + var orderName = format("%s Region %s Ordering", service.getName(), + String.join(",", iter)); + frame.addSortByOrderMenuItem(orderName, alorders.get(i)); + } + } + + /* TODO + * If alignment was requested from one half of a SplitFrame, show in a + * SplitFrame with the other pane similarly aligned. + */ + + Desktop.addInternalFrame(frame, alnTitle, AlignFrame.DEFAULT_WIDTH, + AlignFrame.DEFAULT_HEIGHT); + } + + private List> sortOrders(List alorders) + { + List> regions = new ArrayList<>(); + for (int i = 0; i < alorders.size(); i++) + { + List regs = new ArrayList<>(); + regs.add(i); + int j = i + 1; + while (j < alorders.size()) + { + if (alorders.get(i).equals(alorders.get(j))) + { + alorders.remove(j); + regs.add(j); + } + else + { + j++; + } + } + regions.add(regs); + } + return regions; + } + } + + private static class JobInput + { + final List inputSequences; + + final List emptySequences; + + @SuppressWarnings("rawtypes") + final Map sequenceNames; + + private JobInput(int numSequences, List inputSequences, + List emptySequences, + @SuppressWarnings("rawtypes") Map names) + { + this.inputSequences = Collections.unmodifiableList(inputSequences); + this.emptySequences = Collections.unmodifiableList(emptySequences); + this.sequenceNames = names; + } + + boolean isInputValid() + { + return inputSequences.size() >= 2; + } + + static JobInput create(SequenceI[] sequences, int minLength, + boolean submitGaps) + { + assert minLength >= 0 : MessageManager.getString( + "error.implementation_error_minlen_must_be_greater_zero"); + int numSeq = 0; + for (SequenceI seq : sequences) + { + if (seq.getEnd() - seq.getStart() >= minLength) + { + numSeq++; + } + } + + List inputSequences = new ArrayList<>(); + List emptySequences = new ArrayList<>(); + @SuppressWarnings("rawtypes") + Map names = new LinkedHashMap<>(); + for (int i = 0; i < sequences.length; i++) + { + SequenceI seq = sequences[i]; + String newName = SeqsetUtils.unique_name(i); + @SuppressWarnings("rawtypes") + Hashtable hash = SeqsetUtils.SeqCharacterHash(seq); + names.put(newName, hash); + if (numSeq > 1 && seq.getEnd() - seq.getStart() >= minLength) + { + String seqString = seq.getSequenceAsString(); + if (!submitGaps) + { + seqString = AlignSeq.extractGaps( + jalview.util.Comparison.GapChars, seqString); + } + inputSequences.add(new Sequence(newName, seqString)); + } + else + { + String seqString = ""; + if (seq.getEnd() >= seq.getStart()) // true if gaps only + { + seqString = seq.getSequenceAsString(); + if (!submitGaps) + { + seqString = AlignSeq.extractGaps( + jalview.util.Comparison.GapChars, seqString); + } + } + emptySequences.add(new Sequence(newName, seqString)); + } + } + + return new JobInput(numSeq, inputSequences, emptySequences, names); + } + } + + } diff --cc src/jalview/ws2/operations/AnnotationServiceWorker.java index 0000000,034b581..417b39c mode 000000,100644..100644 --- a/src/jalview/ws2/operations/AnnotationServiceWorker.java +++ b/src/jalview/ws2/operations/AnnotationServiceWorker.java @@@ -1,0 -1,592 +1,593 @@@ + package jalview.ws2.operations; + + import java.io.IOException; + import java.util.ArrayList; + import java.util.Collections; + import java.util.HashMap; + import java.util.List; + import java.util.Map; + import java.util.Objects; + + import jalview.analysis.AlignSeq; + import jalview.analysis.AlignmentAnnotationUtils; + import jalview.analysis.SeqsetUtils; + import jalview.api.AlignCalcManagerI2; + import jalview.api.AlignViewportI; + import jalview.api.AlignmentViewPanel; + import jalview.api.FeatureColourI; + import jalview.api.PollableAlignCalcWorkerI; + import jalview.bin.Cache; ++import jalview.bin.Console; + import jalview.datamodel.Alignment; + import jalview.datamodel.AlignmentAnnotation; + import jalview.datamodel.AlignmentI; + import jalview.datamodel.AnnotatedCollectionI; + import jalview.datamodel.Annotation; + import jalview.datamodel.ContiguousI; + import jalview.datamodel.Mapping; + import jalview.datamodel.Sequence; + import jalview.datamodel.SequenceI; + import jalview.datamodel.features.FeatureMatcherSetI; + import jalview.gui.AlignFrame; + import jalview.gui.AlignViewport; + import jalview.gui.IProgressIndicator; + import jalview.gui.IProgressIndicatorHandler; + import jalview.io.FeaturesFile; + import jalview.schemes.FeatureSettingsAdapter; + import jalview.schemes.ResidueProperties; + import jalview.util.MapList; + import jalview.workers.AlignCalcManager2; + import jalview.ws.params.ArgumentI; + import jalview.ws2.WSJob; + import jalview.ws2.WSJobStatus; + import jalview.ws2.WebServiceI; + import jalview.ws2.gui.ProgressBarUpdater; + + import static java.lang.String.format; + + public class AnnotationServiceWorker implements PollableAlignCalcWorkerI + { + private AnnotationOperation operation; + private WebServiceI service; + private List args; + private AlignViewport viewport; + private AlignmentViewPanel alignPanel; + List sequences; + private IProgressIndicator progressIndicator; + private AlignFrame frame; + private final AlignCalcManagerI2 calcMan; + private Map seqNames; + /** + * indicates columns consisting of gaps only + */ + boolean[] gapMap = new boolean[0]; + int start, end; + boolean transferSequenceFeatures = false; + private WSJob job; + private List ourAnnots; + + private int exceptionCount = MAX_RETRY; + private static final int MAX_RETRY = 5; + + AnnotationServiceWorker(AnnotationOperation operation, WebServiceI service, + List args, AlignViewport viewport, AlignmentViewPanel alignPanel, + IProgressIndicator progressIndicator, AlignFrame frame, AlignCalcManagerI2 calcMan) + { + this.operation = operation; + this.service = service; + this.args = args; + this.viewport = viewport; + this.alignPanel = alignPanel; + this.progressIndicator = progressIndicator; + this.frame = frame; + this.calcMan = calcMan; + } + + @Override + public String getCalcName() + { + return service.getName(); + } + + @Override + public boolean involves(AlignmentAnnotation annot) + { + return ourAnnots != null && ourAnnots.contains(annot); + } + + @Override + public void updateAnnotation() + { + if (!calcMan.isWorking(this) && job != null && !job.getStatus().isCompleted()) + { + // is it correct to store annotations in a field and use them here? + updateResultAnnotation(ourAnnots); + } + } + + @Override + public void removeAnnotation() + { + if (ourAnnots != null && viewport != null) + { + AlignmentI alignment = viewport.getAlignment(); + synchronized (ourAnnots) + { + for (AlignmentAnnotation aa : ourAnnots) + { + alignment.deleteAnnotation(aa, true); + } + } + } + } + + @Override + public boolean isDeletable() + { + return true; + } + + @Override + public void startUp() throws IOException + { + if (viewport.isClosed()) + { + return; + } + /* What "bySequence" means in this context and + * what is the SelectionGroup and why is it only relevant when + * not dealing with alignment analysis? */ + var bySequence = !operation.isAlignmentAnalysis(); + sequences = prepareInput(viewport.getAlignment(), + bySequence ? viewport.getSelectionGroup() : null); + if (sequences == null) + { - Cache.log.info("Sequences for analysis service were null"); ++ Console.info("Sequences for analysis service were null"); + return; + } + if (!checkInputSequencesValid(sequences)) + { - Cache.log.info("Sequences for analysis service were not valid"); ++ Console.info("Sequences for analysis service were not valid"); + } - Cache.log.debug(format("submitting %d sequences to %s", sequences.size(), ++ Console.debug(format("submitting %d sequences to %s", sequences.size(), + service.getName())); + job = new WSJob(service.getProviderName(), service.getName(), + service.getHostName()); + // Should this part be moved out of this class to one of the gui + // classes? + if (progressIndicator != null) + { + job.addPropertyChangeListener("status", new ProgressBarUpdater(progressIndicator)); + progressIndicator.registerHandler(job.getUid(), new IProgressIndicatorHandler() + { + @Override + public boolean cancelActivity(long id) + { + calcMan.cancelWorker(AnnotationServiceWorker.this); + return true; + } + + @Override + public boolean canCancel() + { + return isDeletable(); + } + }); + } + String jobId = service.submit(sequences, args); + job.setJobId(jobId); - Cache.log.debug(format("Service %s: submitted job id %s", ++ Console.debug(format("Service %s: submitted job id %s", + service.getHostName(), jobId)); + } + + private List prepareInput(AlignmentI alignment, + AnnotatedCollectionI inputSeqs) + { + if (alignment == null || alignment.getWidth() <= 0 || + alignment.getSequences() == null) + return null; + if (alignment.isNucleotide() && !operation.isNucleotideOperation()) + return null; + if (!alignment.isNucleotide() && !operation.isProteinOperation()) + return null; + if (inputSeqs == null || inputSeqs.getWidth() <= 0 || + inputSeqs.getSequences() == null || inputSeqs.getSequences().size() < 1) + inputSeqs = alignment; + + List seqs = new ArrayList<>(); + final boolean submitGaps = operation.isAlignmentAnalysis(); + final int minlen = 10; + int ln = -1; // I think this variable is redundant + if (!operation.isAlignmentAnalysis()) + seqNames = new HashMap<>(); + start = inputSeqs.getStartRes(); + end = inputSeqs.getEndRes(); + // TODO: URGENT! unify with JPred / MSA code to handle hidden regions + // correctly + // TODO: push attributes into WsJob instance (so they can be safely + // persisted/restored + for (SequenceI sq : inputSeqs.getSequences()) + { + int sqlen; + // is it trying to find the length of a sequence excluding gaps? + if (!operation.isAlignmentAnalysis()) + // why starting at positions to the right from the end/start? + sqlen = sq.findPosition(end + 1) - sq.findPosition(start + 1); + else + sqlen = sq.getEnd() - sq.getStart(); + if (sqlen >= minlen) + { + String newName = SeqsetUtils.unique_name(seqs.size()); + if (seqNames != null) + { + seqNames.put(newName, sq); + } + SequenceI seq; + if (submitGaps) + { + seq = new Sequence(newName, sq.getSequenceAsString()); + seqs.add(seq); + if (gapMap == null || gapMap.length < seq.getLength()) + { + boolean[] tg = gapMap; + gapMap = new boolean[seq.getLength()]; + System.arraycopy(tg, 0, gapMap, 0, tg.length); + for (int p = tg.length; p < gapMap.length; p++) + { + gapMap[p] = false; // init as a gap + } + } + for (int apos : sq.gapMap()) + { + char sqc = sq.getCharAt(apos); + boolean isStandard = sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20 + : ResidueProperties.nucleotideIndex[sqc] < 5; + if (!operation.getFilterNonStandardSymbols() || isStandard) + { + gapMap[apos] = true; + } + } + } + else + { + // TODO: add ability to exclude hidden regions + String sqstring = sq.getSequenceAsString(start, end + 1); + seq = new Sequence(newName, + AlignSeq.extractGaps(jalview.util.Comparison.GapChars, sqstring)); + seqs.add(seq); + // for annotation need to also record map to sequence start/end + // position in range + // then transfer back to original sequence on return. + } + ln = Integer.max(seq.getLength(), ln); + } + } + if (operation.getNeedsAlignedSequences() && submitGaps) + { + int realw = 0; + for (int i = 0; i < gapMap.length; i++) + { + if (gapMap[i]) + { + realw++; + } + } + // try real hard to return something submittable + // TODO: some of AAcon measures need a minimum of two or three amino + // acids at each position, and AAcon doesn't gracefully degrade. + for (int p = 0; p < seqs.size(); p++) + { + SequenceI sq = seqs.get(p); + // strip gapped columns + char[] padded = new char[realw]; + char[] orig = sq.getSequence(); + for (int i = 0, pp = 0; i < realw; pp++) + { + if (gapMap[pp]) + { + if (orig.length > pp) + { + padded[i++] = orig[pp]; + } + else + { + padded[i++] = '-'; + } + } + } + seqs.set(p, new Sequence(sq.getName(), new String(padded))); + } + } + return seqs; + } + + private boolean checkInputSequencesValid(List sequences) + { + int nvalid = 0; + boolean allowProtein = operation.isProteinOperation(), + allowNucleotides = operation.isNucleotideOperation(); + for (SequenceI sq : sequences) + { + if (sq.getStart() <= sq.getEnd() && + (sq.isProtein() ? allowProtein : allowNucleotides)) + { + nvalid++; + } + } + return nvalid >= operation.getMinSequences(); + } + + @Override + public boolean poll() throws IOException + { + if (!job.getStatus().isDone() && !job.getStatus().isFailed()) + { - Cache.log.debug(format("Polling job %s", job)); ++ Console.debug(format("Polling job %s", job)); + try + { + service.updateProgress(job); + exceptionCount = MAX_RETRY; + } catch (IOException e) + { - Cache.log.error(format("Polling job %s failed.", job), e); ++ Console.error(format("Polling job %s failed.", job), e); + if (--exceptionCount <= 0) + { + job.setStatus(WSJobStatus.SERVER_ERROR); - Cache.log.warn(format("Attempts limit exceeded. Dropping job %s.", job)); ++ Console.warn(format("Attempts limit exceeded. Dropping job %s.", job)); + } + } catch (OutOfMemoryError e) + { + job.setStatus(WSJobStatus.BROKEN); - Cache.log.error(format("Out of memory when retrieving job %s", job), e); ++ Console.error(format("Out of memory when retrieving job %s", job), e); + } + } + return job.getStatus().isDone() || job.getStatus().isFailed(); + } + + @Override + public void cancel() + { + try + { + service.cancel(job); + } catch (IOException e) + { - Cache.log.error(format("Failed to cancel job %s.", job), e); ++ Console.error(format("Failed to cancel job %s.", job), e); + } + } + + @Override + public void done() + { - Cache.log.debug(format("Polling loop exited, job %s is %s", job, job.getStatus())); ++ Console.debug(format("Polling loop exited, job %s is %s", job, job.getStatus())); + if (!job.getStatus().isCompleted()) + { + return; + } + List outputAnnotations = null; + try + { + outputAnnotations = operation.annotationSupplier + .getResult(job, sequences, viewport); + } catch (IOException e) + { - Cache.log.error(format("Couldn't retrieve features for job %s.", job), e); ++ Console.error(format("Couldn't retrieve features for job %s.", job), e); + } + if (outputAnnotations != null) - Cache.log.debug(format("Obtained %d annotation rows.", outputAnnotations.size())); ++ Console.debug(format("Obtained %d annotation rows.", outputAnnotations.size())); + else - Cache.log.debug("Obtained no annotations."); ++ Console.debug("Obtained no annotations."); + Map featureColours = new HashMap<>(); + Map featureFilters = new HashMap<>(); + FeaturesFile featuresFile; + try + { + // I think there should be a better way for obtaining features + // Are the features added to the sequences here? + featuresFile = operation.featuresSupplier.getResult(job, sequences, viewport); + if (featuresFile != null) + { + Alignment aln = new Alignment(sequences.toArray(new SequenceI[0])); + // I do nothing with the featureFilters object + featuresFile.parse(aln, featureColours, true); + } + } catch (IOException e) + { - Cache.log.error(format("Couldn't retrieve features for job %s", job), e); ++ Console.error(format("Couldn't retrieve features for job %s", job), e); + } - Cache.log.debug(format("There are %d feature colours and %d filters.", ++ Console.debug(format("There are %d feature colours and %d filters.", + featureColours.size(), featureFilters.size())); + if (outputAnnotations != null) + { + for (AlignmentAnnotation aa : outputAnnotations) + { + if (aa.getCalcId() == null || aa.getCalcId().equals("")) + { + aa.setCalcId(service.getName()); + } + // Can't services other than alignment analysis be interactive? + // What's the point of storing that information in the annotation? + aa.autoCalculated = operation.isAlignmentAnalysis() && operation.isInteractive(); + } + updateResultAnnotation(outputAnnotations); + if (transferSequenceFeatures) + { - Cache.log.debug(format("Updating feature display settings and transferring" ++ Console.debug(format("Updating feature display settings and transferring" + + "features fron job %s at %s", job, service.getHostName())); + viewport.applyFeaturesStyle(new FeatureSettingsAdapter() + { + @Override + public FeatureColourI getFeatureColour(String type) + { + return featureColours.get(type); + } + + @Override + public FeatureMatcherSetI getFeatureFilters(String type) + { + return featureFilters.get(type); + } + + @Override + public boolean isFeatureDisplayed(String type) + { + return featureColours.containsKey(type); + } + }); + if (frame.alignPanel == alignPanel) + { + viewport.setShowSequenceFeatures(true); + frame.setMenusForViewport(); + } + } + } - Cache.log.debug("Annotation service task finished."); ++ Console.debug("Annotation service task finished."); + } + + // What is the purpose of this method? + // When is it called (apart from the above)? + private void updateResultAnnotation(List annotations) + { + var currentAnnotations = Objects.requireNonNullElse( + viewport.getAlignment().getAlignmentAnnotation(), + new AlignmentAnnotation[0]); + List newAnnots = new ArrayList<>(); + // what is the graph group and why starting from 1? + int graphGroup = 1; + for (AlignmentAnnotation alna : currentAnnotations) + { + graphGroup = Integer.max(graphGroup, alna.graphGroup); + } + for (AlignmentAnnotation ala : annotations) + { + if (ala.graphGroup > 0) + { + ala.graphGroup += graphGroup; + } + + // stores original sequence, in what case it ends up as null? + SequenceI aseq = null; + if (ala.sequenceRef != null) + { + SequenceI seq = seqNames.get(ala.sequenceRef.getName()); + aseq = seq; + while (seq.getDatasetSequence() != null) + { + seq = seq.getDatasetSequence(); + } + } + Annotation[] resAnnot = ala.annotations; + Annotation[] gappedAnnot = new Annotation[Math + .max(viewport.getAlignment().getWidth(), gapMap.length)]; + // is it adding gaps which were previously removed to the annotation? + for (int p = 0, ap = start; ap < gappedAnnot.length; ap++) + { + if (gapMap != null && gapMap.length > ap && !gapMap[ap]) + { + gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN); + } + else if (p < resAnnot.length) + { + gappedAnnot[ap] = resAnnot[p++]; + } + } + // replacing sequence with the original one? + ala.sequenceRef = aseq; + ala.annotations = gappedAnnot; + AlignmentAnnotation newAnnot = viewport.getAlignment() + .updateFromOrCopyAnnotation(ala); + if (aseq != null) + { + aseq.addAlignmentAnnotation(newAnnot); + newAnnot.adjustForAlignment(); + AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(newAnnot, + newAnnot.label, newAnnot.getCalcId()); + } + newAnnots.add(newAnnot); + } + + for (SequenceI sq : sequences) + { + // what are DBRefs? why are they relevant here? + if (!sq.getFeatures().hasFeatures() && + (sq.getDBRefs() == null || sq.getDBRefs().size() == 0)) + { + continue; + } + transferSequenceFeatures = true; + SequenceI seq = seqNames.get(sq.getName()); + SequenceI dseq; + ContiguousI seqRange = seq.findPositions(start, end); + + while ((dseq = seq).getDatasetSequence() != null) + { + seq = seq.getDatasetSequence(); + } + List sourceRange = new ArrayList<>(); + if (gapMap != null && gapMap.length > end) + { + int lastcol = start, col = start; + do + { + if (col == end || !gapMap[col]) + { + if (lastcol <= col - 1) + { + seqRange = seq.findPositions(lastcol, col); + sourceRange.add(seqRange); + } + lastcol = col + 1; + } + } while (++col < end); + } + else + { + sourceRange.add(seq.findPositions(start, end)); + } + int i = 0; + int sourceStartEnd[] = new int[sourceRange.size() * 2]; + for (ContiguousI range : sourceRange) + { + sourceStartEnd[i++] = range.getBegin(); + sourceStartEnd[i++] = range.getEnd(); + } + Mapping mp = new Mapping(new MapList(sourceStartEnd, + new int[] { seq.getStart(), seq.getEnd() }, 1, 1)); + dseq.transferAnnotation(sq, mp); + } + updateOurAnnots(newAnnots); + } + + protected void updateOurAnnots(List annots) + { + List our = ourAnnots; + ourAnnots = Collections.synchronizedList(annots); + AlignmentI alignment = viewport.getAlignment(); + if (our != null) + { + if (our.size() > 0) + { + for (AlignmentAnnotation an : our) + { + if (!ourAnnots.contains(an)) + { + // remove the old annotation + alignment.deleteAnnotation(an); + } + } + } + our.clear(); + } + // validate rows and update Alignment state + synchronized (ourAnnots) + { + for (AlignmentAnnotation an : ourAnnots) + { + viewport.getAlignment().validateAnnotation(an); + } + } + // TODO: may need a menu refresh after this + // af.setMenusForViewport(); + alignPanel.adjustAnnotationHeight(); + } + } diff --cc src/jalview/ws2/slivka/SlivkaWSDiscoverer.java index 0000000,c0c7502..e016fa1 mode 000000,100644..100644 --- a/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java +++ b/src/jalview/ws2/slivka/SlivkaWSDiscoverer.java @@@ -1,0 -1,209 +1,210 @@@ + package jalview.ws2.slivka; + + import java.io.IOException; + import java.net.MalformedURLException; + import java.net.URL; + import java.util.*; + import java.util.concurrent.*; + + import jalview.bin.Cache; ++import jalview.bin.Console; + import jalview.ws2.*; + import jalview.ws2.operations.AlignmentOperation; + import jalview.ws2.operations.AnnotationOperation; + import jalview.ws2.operations.Operation; + import uk.ac.dundee.compbio.slivkaclient.SlivkaClient; + import uk.ac.dundee.compbio.slivkaclient.SlivkaService; + + public class SlivkaWSDiscoverer implements WebServiceDiscoverer + { + private static final String SLIVKA_HOST_URLS = "SLIVKSHOSTURLS"; + + private static final String DEFAULT_URL = "https://www.compbio.dundee.ac.uk/slivka/"; + + private static SlivkaWSDiscoverer instance = null; + + private List services = List.of(); + + private SlivkaWSDiscoverer() + { + } + + public static SlivkaWSDiscoverer getInstance() + { + if (instance == null) + { + instance = new SlivkaWSDiscoverer(); + } + return instance; + } + + @Override + public List getUrls() + { + String surls = Cache.getDefault(SLIVKA_HOST_URLS, DEFAULT_URL); + String urls[] = surls.split(","); + ArrayList valid = new ArrayList<>(urls.length); + for (String url : urls) + { + try + { + new URL(url); + valid.add(url); + } catch (MalformedURLException e) + { - Cache.log.warn("Problem whilst trying to make a URL from '" ++ Console.warn("Problem whilst trying to make a URL from '" + + Objects.toString(url, "") + "'. " + + "This was probably due to malformed comma-separated-list " + + "in the " + SLIVKA_HOST_URLS + + " entry of ${HOME}/.jalview_properties"); - Cache.log.debug("Exception occurred while reading url list", e); ++ Console.debug("Exception occurred while reading url list", e); + } + } + return valid; + } + + @Override + public void setUrls(List wsUrls) + { + if (wsUrls != null && !wsUrls.isEmpty()) + { + Cache.setProperty(SLIVKA_HOST_URLS, String.join(",", wsUrls)); + } + else + { + Cache.removeProperty(SLIVKA_HOST_URLS); + } + } + + @Override + public boolean testUrl(URL url) + { + return getStatusForUrl(url.toString()) == STATUS_OK; + } + + @Override + public int getStatusForUrl(String url) + { + try + { + List services = new SlivkaClient(url).getServices(); + return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK; + } catch (IOException e) + { - Cache.log.error("Slivka could not retrieve services list from " + url, ++ Console.error("Slivka could not retrieve services list from " + url, + e); + return STATUS_INVALID; + } + } + + public List getServices() + { + return Collections.unmodifiableList(services); + } + + public boolean hasServices() + { + return !isRunning() && services.size() > 0; + } + + public boolean isRunning() + { + for (Future task : discoveryTasks) + { + if (!task.isDone()) + { + return true; + } + } + return false; + } + + public boolean isDone() + { + return !isRunning() && discoveryTasks.size() > 0; + } + + private Vector> discoveryTasks = new Vector<>(); + + @Override + public CompletableFuture startDiscoverer() + { + CompletableFuture task = CompletableFuture + .supplyAsync(() -> { + reloadServices(); + return SlivkaWSDiscoverer.this; + }); + task.thenRun(() -> fireServicesChanged(getServices())); + discoveryTasks.add(task); + return task; + } + + private List reloadServices() + { - Cache.log.info("Reloading Slivka services"); ++ Console.info("Reloading Slivka services"); + fireServicesChanged(Collections.emptyList()); + ArrayList allServices = new ArrayList<>(); + for (String url : getUrls()) + { + SlivkaClient client = new SlivkaClient(url); + List services; + try + { + services = client.getServices(); + } catch (IOException e) + { - Cache.log.error("Unable to fetch services from " + url, e); ++ Console.error("Unable to fetch services from " + url, e); + continue; + } + for (SlivkaService service : services) + { + SlivkaWebService instance = new SlivkaWebService(client, service); + for (String classifier : service.classifiers) + { + String[] path = classifier.split("\\s*::\\s*"); + if (path.length >= 3 && path[0].toLowerCase().equals("operation") + && path[1].toLowerCase().equals("analysis")) + { + Operation op = null; + switch (path[path.length - 1].toLowerCase()) + { + case "sequence alignment analysis (conservation)": + AnnotationOperation anop; + op = anop = new AnnotationOperation(instance, + instance::getAnnotations, instance::getFeaturesFile, "Conservation"); + anop.setAlignmentAnalysis(true); + anop.setInteractive(true); + break; + case "protein sequence analysis": + op = new AnnotationOperation(instance, instance::getAnnotations, + instance::getFeaturesFile, "Protein Disorder"); + break; + case "multiple sequence alignment": + op = new AlignmentOperation(instance, instance::getAlignment); + break; + } + if (op != null) + { + instance.addOperation(op); + break; + } + } + } + if (instance.operations.size() > 0) + { + allServices.add(instance); + } + } + } + this.services = allServices; - Cache.log.info("Reloading slivka services finished"); ++ Console.info("Reloading slivka services finished"); + return allServices; + } + + @Override + public String getErrorMessages() + { + return ""; + } + + } diff --cc src/jalview/ws2/slivka/SlivkaWebService.java index 0000000,4c58b61..cf03cbe mode 000000,100644..100644 --- a/src/jalview/ws2/slivka/SlivkaWebService.java +++ b/src/jalview/ws2/slivka/SlivkaWebService.java @@@ -1,0 -1,310 +1,311 @@@ + package jalview.ws2.slivka; + + import java.io.ByteArrayInputStream; + import java.io.ByteArrayOutputStream; + import java.io.IOException; + import java.io.InputStream; + import java.util.ArrayList; + import java.util.Arrays; + import java.util.Collection; + import java.util.EnumMap; + import java.util.HashSet; + import java.util.List; + import java.util.Set; + + import jalview.api.AlignViewportI; + import jalview.bin.Cache; ++import jalview.bin.Console; + import jalview.datamodel.Alignment; + import jalview.datamodel.AlignmentAnnotation; + import jalview.datamodel.AlignmentI; + import jalview.datamodel.SequenceI; + import jalview.io.AnnotationFile; + import jalview.io.DataSourceType; + import jalview.io.FeaturesFile; + import jalview.io.FileFormat; + import jalview.io.FileFormatI; + import jalview.io.FormatAdapter; + import jalview.ws.gui.WsJob; + import jalview.ws.params.ArgumentI; + import jalview.ws.params.ParamDatastoreI; + import jalview.ws.params.WsParamSetI; + import jalview.ws.slivkaws.SlivkaDatastore; + import jalview.ws2.WebServiceI; + import jalview.ws2.operations.Operation; + import jalview.ws2.ResultSupplier; + import jalview.ws2.WSJob; + import jalview.ws2.WSJobStatus; + import javajs.http.ClientProtocolException; + import uk.ac.dundee.compbio.slivkaclient.Job; + import uk.ac.dundee.compbio.slivkaclient.Parameter; + import uk.ac.dundee.compbio.slivkaclient.RemoteFile; + import uk.ac.dundee.compbio.slivkaclient.SlivkaClient; + import uk.ac.dundee.compbio.slivkaclient.SlivkaService; + + public class SlivkaWebService implements WebServiceI + { + protected final SlivkaClient client; + + protected final SlivkaService service; + + protected SlivkaDatastore store = null; + + protected final ArrayList operations = new ArrayList<>(); + + protected int typeFlags = 0; + + protected static final EnumMap statusMap = new EnumMap<>( + Job.Status.class); + { + statusMap.put(Job.Status.PENDING, WSJobStatus.SUBMITTED); + statusMap.put(Job.Status.REJECTED, WSJobStatus.INVALID); + statusMap.put(Job.Status.ACCEPTED, WSJobStatus.QUEUED); + statusMap.put(Job.Status.QUEUED, WSJobStatus.QUEUED); + statusMap.put(Job.Status.RUNNING, WSJobStatus.RUNNING); + statusMap.put(Job.Status.COMPLETED, WSJobStatus.FINISHED); + statusMap.put(Job.Status.INTERRUPTED, WSJobStatus.CANCELLED); + statusMap.put(Job.Status.DELETED, WSJobStatus.CANCELLED); + statusMap.put(Job.Status.FAILED, WSJobStatus.FAILED); + statusMap.put(Job.Status.ERROR, WSJobStatus.SERVER_ERROR); + statusMap.put(Job.Status.UNKNOWN, WSJobStatus.UNKNOWN); + } + + public SlivkaWebService(SlivkaClient client, SlivkaService service) + { + this.client = client; + this.service = service; + } + + @Override + public String getHostName() + { + return client.getUrl().toString(); + } + + @Override + public String getProviderName() + { + return "slivka"; + } + + @Override + public String getName() + { + return service.getName(); + } + + @Override + public String getDescription() + { + return service.getDescription(); + } + + @Override + public List getOperations() + { + return operations; + } + + void addOperation(Operation operation) + { + operations.add(operation); + } + + void removeOperation(Operation operation) + { + operations.remove(operation); + } + + @Override + public boolean hasParameters() + { + return getParamStore().getServiceParameters().size() > 0; + } + + @Override + public ParamDatastoreI getParamStore() + { + if (store == null) + { + store = new SlivkaDatastore(service); + } + return store; + } + + @Override + public String submit(List sequences, List args) + throws IOException + { + var request = new uk.ac.dundee.compbio.slivkaclient.JobRequest(); + for (Parameter param : service.getParameters()) + { + if (param instanceof Parameter.FileParameter) + { + // if finds a file input, gives it sequences stream + Parameter.FileParameter fileParam = (Parameter.FileParameter) param; + FileFormat format; + switch (fileParam.getMediaType()) + { + case "application/pfam": + format = FileFormat.Pfam; + break; + case "application/stockholm": + format = FileFormat.Stockholm; + break; + case "application/clustal": + format = FileFormat.Clustal; + break; + case "application/fasta": + default: + format = FileFormat.Fasta; + break; + } + InputStream stream = new ByteArrayInputStream(format.getWriter(null) + .print(sequences.toArray(new SequenceI[0]), false) + .getBytes()); + request.addFile(param.getId(), stream); + } + } + if (args != null) + { + for (ArgumentI arg : args) + { + // multiple choice field names are name$number to avoid duplications + // the number is stripped here + String paramId = arg.getName().split("\\$", 2)[0]; + Parameter param = service.getParameter(paramId); + if (param instanceof Parameter.FlagParameter) + { + if (arg.getValue() != null && !arg.getValue().isBlank()) + request.addData(paramId, true); + else + request.addData(paramId, false); + } + else + { + request.addData(paramId, arg.getValue()); + } + } + } + var job = service.submitJob(request); + return job.getId(); + } + + @Override + public void updateProgress(WSJob job) throws IOException + { + var slivkaJob = client.getJob(job.getJobId()); + job.setStatus(statusMap.get(slivkaJob.getStatus())); + Collection files = slivkaJob.getResults(); + for (RemoteFile f : files) + { + if (f.getLabel().equals("log")) + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + f.writeTo(stream); + job.setLog(stream.toString("UTF-8")); + } + else if (f.getLabel().equals("error-log")) + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + f.writeTo(stream); + job.setErrorLog(stream.toString("UTF-8")); + } + } + } + + @Override + public void cancel(WSJob job) throws IOException + { + job.setStatus(WSJobStatus.CANCELLED); - Cache.log.warn("Slivka does not support job cancellation yet."); ++ Console.warn("Slivka does not support job cancellation yet."); + } + + @Override + public boolean handleSubmissionError(WSJob job, Exception ex) + { + if (ex instanceof ClientProtocolException) + { - Cache.log.error("Job submission failed due to exception.", ex); ++ Console.error("Job submission failed due to exception.", ex); + return true; + } + return false; + } + + @Override + public boolean handleCollectionError(WSJob job, Exception ex) + { + // TODO Auto-generated method stub + return false; + } + + public AlignmentI getAlignment(WSJob job, List dataset, + AlignViewportI viewport) throws IOException + { + Collection files; + var slivkaJob = client.getJob(job.getJobId()); + files = slivkaJob.getResults(); + for (RemoteFile f : files) + { + if (f.getMediaType().equals("application/clustal")) + { + return new FormatAdapter().readFile(f.getContentUrl().toString(), + DataSourceType.URL, FileFormat.Clustal); + } + else if (f.getMediaType().equals("application/fasta")) + { + return new FormatAdapter().readFile(f.getContentUrl().toString(), + DataSourceType.URL, FileFormat.Fasta); + } + } + return null; + } + + public FeaturesFile getFeaturesFile(WSJob job, + List dataset, AlignViewportI viewport) throws IOException + { + var slivkaJob = client.getJob(job.getJobId()); + Collection files = slivkaJob.getResults(); + for (RemoteFile f : files) + { + if (f.getMediaType().equals("application/jalview-features")) + { + return new FeaturesFile(f.getContentUrl().toString(), DataSourceType.URL); + } + } + return null; + } + + public List getAnnotations(WSJob job, + List dataset, AlignViewportI viewport) throws IOException + { + var slivkaJob = client.getJob(job.getJobId()); + Collection files = slivkaJob.getResults(); + for (RemoteFile f : files) + { + if (f.getMediaType().equals("application/jalview-annotations")) + { + Alignment aln = new Alignment(dataset.toArray(new SequenceI[0])); + AnnotationFile af = new AnnotationFile(); + boolean valid = af.readAnnotationFileWithCalcId(aln, service.getId(), + f.getContentUrl().toString(), DataSourceType.URL); + if (valid) + { + return Arrays.asList(aln.getAlignmentAnnotation()); + } + else + { + throw new IOException("Unable to read annotations from file " + + f.getContentUrl().toString()); + } + } + } + return null; + } + + @Override + public String toString() + { + return String.format("SlivkaWebService[%s]", getName()); + } + }